SwiftUI - Sliding Text animation and positioning
Asked Answered
M

3

10

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

Metropolitan answered 3/9, 2020 at 15:6 Comment(1)
Making the GeometryReader bigger. it makes the animation with even more padding...Metropolitan
T
8

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)
    }
}
Tattan answered 3/9, 2020 at 17:58 Comment(3)
Wow that makes it a lot easier!! thanks! Do you have an idea why when you rotate the screen to the left or right, it makes the whole text frame moving between the previous position and the new one too?Metropolitan
Alright, I got it to work properly by adjusting with what you showed me and my own code. I shared the code in my original post. Thank you so much for your help @Asperi! Really appreciated!Metropolitan
Enlightened indenting.Sexlimited
O
2

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

Olshausen answered 3/9, 2020 at 18:58 Comment(0)
M
0

Using some inspiration from you guys, I end up doing something way simpler by using only the Animation functions/properties.

import SwiftUI

struct SlidingText: View {
    let geometryProxy: GeometryProxy
    @Binding var text: String
    let font: Font

    @State private var animateSliding: Bool = false
    private let slideDelay: Double = 3
    private let slideDuration: Double = 6

    private var isTextLargerThanView: Bool {
        if text.size(forWidth: geometryProxy.size.width, andFont: font).width < geometryProxy.size.width {
            return false
        }
        return true
    }

    var body: some View {
        ZStack(alignment: .leading, content: {
            VStack(content: {
                Text(text)
                    .font(self.font)
                    .foregroundColor(.white)
            })
                .id("SlidingText-Animation")
                .fixedSize()
                .animation(isTextLargerThanView ? Animation.linear(duration: slideDuration).delay(slideDelay).repeatForever(autoreverses: true) : nil)
                .frame(width: geometryProxy.size.width,
                       alignment: isTextLargerThanView ? (animateSliding ? .trailing : .leading) : .center)
                .onAppear(perform: {
                    self.animateSliding.toggle()
                })
        })
            .clipped()
    }
}

And I call the SlidingView

GeometryReader(content: { geometry in
                    SlidingText(geometryProxy: geometry,
                                text: self.$playerViewModel.seasonByline,
                                font: .custom("FoundersGrotesk-RegularRegular", size: 15))
                })
                    .frame(height: 20)
                    .fixedSize(horizontal: false, vertical: true)

Thank you again for your help!

Metropolitan answered 10/9, 2020 at 16:14 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.