SwiftUI: How to create a powerful Marquee?

Catch Zeng
3 min readNov 24, 2020

Many apps need to use the marquee view. In UIKit we can use https://github.com/cbpowell/MarqueeLabel, but there is no ready-made library in SwiftUI.

Let’s make a powerful marquee together.

What kind of marquee is powerful?

  • It must support any content view (MarqueeLabel only supports text).
  • It can customize the animation duration, autoreverses, direction, etc.
  • It can be used in any combination.

Marquee animation principle

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.

struct ContentView: View {
@State var text: String = "\(Date())"
@State var textWidth: CGFloat = 0

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 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")
}
}
}
struct ViewBuilderView<Content> : View where Content : View {
private var content: () -> Content

init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}

public 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!

--

--