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))
}
}
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.
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.
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
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)
}
}
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.
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()
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With