Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI - Sliding Text animation and positioning

On my journey to learn more about SwiftUI, I am still getting confused with positioning my element in a ZStack.

The goal is "simple", I wanna have a text that will slide within a defined area if the text is too long. Let's say I have an area of 50px and the text takes 100. I want the text to slide from right to left and then left to right.

Currently, my code looks like the following:

struct ContentView: View {
    @State private var animateSliding: Bool = false
    private let timer = Timer.publish(every: 1, on: .current, in: .common).autoconnect()
    private let slideDuration: Double = 3
    private let text: String = "Hello, World! My name is Oleg and I would like this text to slide!"

    var body: some View {
        GeometryReader(content: { geometry in
            VStack(content: {
                Text("Hello, World! My name is Oleg!")
                    .font(.system(size: 20))
                    .id("SlidingText-Animation")
                    .alignmentGuide(VerticalAlignment.center, computeValue: { $0[VerticalAlignment.center] })
                    .position(y: geometry.size.height / 2 + self.textSize(fromWidth: geometry.size.width).height / 2)
                    .fixedSize()
                    .background(Color.red)
                    .animation(Animation.easeInOut(duration: 2).repeatForever())
                    .position(x: self.animateSliding ? -self.textSize(fromWidth: geometry.size.width).width : self.textSize(fromWidth: geometry.size.width).width)
                    .onAppear(perform: {
                        self.animateSliding.toggle()
                    })
            })
                .background(Color.yellow)
                .clipShape(Rectangle())
        })
            .frame(width: 200, height: 80)
    }

    func textSize(fromWidth width: CGFloat, fontName: String = "System Font", fontSize: CGFloat = UIFont.systemFontSize) -> CGSize {
        let text: UILabel = .init()
        text.text = self.text
        text.numberOfLines = 0
        text.font = UIFont.systemFont(ofSize: 20) // (name: fontName, size: fontSize)
        text.lineBreakMode = .byWordWrapping
        return text.sizeThatFits(CGSize.init(width: width, height: .infinity))
    }
}

enter image description here

Do you have any suggestion how to center the Text in Vertically in its parent and do the animation that starts at the good position?

Thank you for any future help, really appreciated!

EDIT:

I restructured my code, and change a couple things I was doing.

struct SlidingText: View {
    let geometryProxy: GeometryProxy

    @State private var animateSliding: Bool = false
    private let timer = Timer.publish(every: 1, on: .current, in: .common).autoconnect()
    private let slideDuration: Double = 3
    private let text: String = "Hello, World! My name is Oleg and I would like this text to slide!"

    var body: some View {
        ZStack(alignment: .leading, content: {
            Text(text)
                .font(.system(size: 20))
//                .lineLimit(1)
                .id("SlidingText-Animation")
                .fixedSize(horizontal: true, vertical: false)
                .background(Color.red)
                .animation(Animation.easeInOut(duration: slideDuration).repeatForever())
                .offset(x: self.animateSliding ? -textSize().width : textSize().width)
                .onAppear(perform: {
                    self.animateSliding.toggle()
                })
        })
            .frame(width: self.geometryProxy.size.width, height: self.geometryProxy.size.height)
            .clipShape(Rectangle())
            .background(Color.yellow)
    }

    func textSize(fontName: String = "System Font", fontSize: CGFloat = 20) -> CGSize {
        let text: UILabel = .init()
        text.text = self.text
        text.numberOfLines = 0
        text.font = UIFont(name: fontName, size: fontSize)
        text.lineBreakMode = .byWordWrapping
        let textSize = text.sizeThatFits(CGSize(width: self.geometryProxy.size.width, height: .infinity))
        print(textSize.width)
        return textSize
    }
}

struct ContentView: View {
    var body: some View {
        GeometryReader(content: { geometry in
            SlidingText(geometryProxy: geometry)
        })
            .frame(width: 200, height: 40)

    }
}

Now the animation looks pretty good, except that I have padding on both right and left which I don't understand why.

enter image description here

Edit2: I also notice by changing the text.font = UIFont.systemFont(ofSize: 20) by text.font = UIFont.systemFont(ofSize: 15) makes the text fits correctly. I don't understand if there is a difference between the system font from SwiftUI or UIKit. It shouldn't ..

Final EDIT with Solution in my case:

struct SlidingText: View {
    let geometryProxy: GeometryProxy
    @Binding var text: String
    @Binding var fontSize: CGFloat

    @State private var animateSliding: Bool = false
    private let timer = Timer.publish(every: 5, on: .current, in: .common).autoconnect()
    private let slideDuration: Double = 3

    var body: some View {
            ZStack(alignment: .leading, content: {
                VStack {
                    Text(text)
                        .font(.system(size: self.fontSize))
                        .background(Color.red)
                }
                .fixedSize()
                .frame(width: geometryProxy.size.width, alignment: animateSliding ? .trailing : .leading)
                .clipped()
                .animation(Animation.linear(duration: slideDuration))
                .onReceive(timer) { _ in
                    self.animateSliding.toggle()
                }
            })
                .frame(width: self.geometryProxy.size.width, height: self.geometryProxy.size.height)
                .clipShape(Rectangle())
                .background(Color.yellow)
    }
}

struct ContentView: View {
    @State var text: String = "Hello, World! My name is Oleg and I would like this text to slide!"
    @State var fontSize: CGFloat = 20

    var body: some View {
        GeometryReader(content: { geometry in
            SlidingText(geometryProxy: geometry, text: self.$text, fontSize: self.$fontSize)
        })
            .frame(width: 400, height: 40)
        .padding(0)

    }
}

Here's the result visually.

enter image description here

like image 593
Oleg G. Avatar asked Sep 03 '20 15:09

Oleg G.


2 Answers

Here is a possible simple approach - the idea is just as simple as to change text alignment in container, anything else can be tuned as usual.

Demo prepared & tested with Xcode 12 / iOS 14

Update: retested with Xcode 13.3 / iOS 15.4

demo

struct DemoSlideText: View {
    let text = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor"

    @State private var go = false
    var body: some View {
        VStack {
            Text(text)
        }
        .fixedSize()
        .frame(width: 300, alignment: go ? .trailing : .leading)
        .clipped()
        .onAppear { self.go.toggle() }
        .animation(Animation.linear(duration: 5).repeatForever(autoreverses: true), value: go)
    }
}
like image 54
Asperi Avatar answered Nov 18 '22 16:11

Asperi


One way to do this is by using the "PreferenceKey" protocol, in conjunction with the "preference" and "onPreferenceChange" modifiers.

It's SwiftUI's way to send data from child Views to parent Views.

Code:

struct ContentView: View {
    
    @State private var offset: CGFloat = 0.0
    
    private let text: String = "Hello, World! My name is Oleg and I would like this text to slide!"
    
    var body: some View {
        // Capturing the width of the screen
        GeometryReader { screenGeo in
            ZStack {
                Color.yellow
                    .frame(height: 50)
                
                Text(text)
                    .fixedSize()
                    .background(
                        // Capturing the width of the text
                        GeometryReader { geo in
                            Color.red
                                // Sending width difference to parent View
                                .preference(key: MyTextPreferenceKey.self, value: screenGeo.size.width - geo.frame(in: .local).width
                                )
                        }
                    )
            }
            .offset(x: self.offset)
            // Receiving width from child view
            .onPreferenceChange(MyTextPreferenceKey.self, perform: { width in
                    withAnimation(Animation.easeInOut(duration: 1).repeatForever()) {
                        self.offset = width
                    }
            })
        }
    }
}

// MARK: PreferenceKey
struct MyTextPreferenceKey: PreferenceKey {
    static var defaultValue: CGFloat = 0.0
    
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }
}

Result:

enter image description here

like image 20
Nikos Polychronakis Avatar answered Nov 18 '22 17:11

Nikos Polychronakis