SwiftUI: How to create a powerful Marquee?
By: Catch Zeng
Tags: Marquee, Swiftui, Scrolling Text, Gallery

The principle is that the content view moves from one end of the marquee to the other, and the loop repeats after completion.

Steps

The first step is to get the width of the marquee and the content view. On this point, you can use GeometryReader and PreferenceKey to achieve.

GeometryReader provides us with an input value telling us the width and height we have available, and we can then use that with whatever calculations we need.

struct ContentView: View {

var body: some View {

GeometryReader { geometry in

Text("width: \(geometry.size.width)")

.frame(width: geometry.size.width, height: 50)

.background(Color.yellow)

}

}

}

As we know, SwiftUI has the environment concept which we can use to pass data down into a view hierarchy. Parent views share their environment with child views and subscribe to the changes. But sometimes we need to pass data up from child's view to the parent's view, and this is where preferences shine.

Because the marquee needs to know the width of the content view to make animation, here you need to use PreferenceKey to achieve it.









var body: some View {

GeometryReader { geometry in

VStack {

Text(text)

.background(GeometryBackground())

.background(Color.yellow)



Text("text width: \(textWidth)")



Button(action: {

self.text = "\(Date())"

}, label: {

Text("change text")

})

}

}

// Listen content width changes

.onPreferenceChange(WidthKey.self, perform: { value in

self.textWidth = value

})

}

} struct ContentView: View { @State var text: String = "\(Date())" @State var textWidth: CGFloat = 0var body: some View {GeometryReader { geometry inVStack {Text(text).background(GeometryBackground()).background(Color.yellow)Text("text width: \(textWidth)")Button(action: {self.text = "\(Date())"}, label: {Text("change text")})// Listen content width changes.onPreferenceChange(WidthKey.self, perform: { value inself.textWidth = value}) struct GeometryBackground: View {

var body: some View {

GeometryReader { geometry in

return Color.clear.preference(key: WidthKey.self, value: geometry.size.width)

}

}

} struct WidthKey: PreferenceKey {

static var defaultValue = CGFloat(0) static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {

value = nextValue()

} typealias Value = CGFloat

}

The second step is to realize the offset animation.



struct ContentView : View { @State private var offset: CGFloat = 0 var body: some View {

Text("offset animation")

.offset(x: offset, y: 0)

.onAppear {

withAnimation(Animation.linear(duration: 1).repeatForever(autoreverses: true)) {

self.offset = 100

}

}.background(Color.yellow)

}

}

The third step is to use ViewBuilder to support any content view.



struct ContentView : View { @State private var offset: CGFloat = 0 var body: some View {

ViewBuilderView {

Text("content view")

}

}

}

private var content: () -> Content



init(

self.content = content

}



public var body: some View {

VStack {

Text("---")

content()

.background(Color.yellow)

Text("---")

}.background(Color.blue)

}

} struct ViewBuilderView : View where Content : View {private var content: () -> Contentinit( @ViewBuilder content: @escaping () -> Content) {self.content = contentpublic var body: some View {VStack {Text("---")content().background(Color.yellow)Text("---")}.background(Color.blue)

According to the above steps, you can complete the marquee. The detailed code is at https://github.com/SwiftUIKit/Marquee. See you!