【iOSDC2024 #LT】5. How to implement AutoScrollView
This article summarizes the implementation of animations in the presentation slides for the LT session, A Digital Piano Came to My House, So I Made an App to Connect with It!, presented at iOSDC2024.
The previous article was “4. Creating Slide Animations with SwiftUI”.
This article focuses on the implementation of an auto-scrolling text UI, similar to an electric billboard, featured at the bottom of the video below.
Since the LT session was only 5 minutes long, I implemented this view to stream information like a self-introduction and supplemental details at the bottom of the slides. Initially, I tried implementing it by trial and error and thought it was working well, but with long texts of several hundred characters, the scrolling became choppy. After some trial and error, I finally came up with an implementation that worked smoothly for this 5-minute LT, and I’ll share the method here.
Implementation of AutoScrollTextView
First, let’s review the implementation approach and constraints.
Initially, I thought about placing Text in a ScrollView and using offset animations with Timer or withAnimation to move it gradually. However, this method didn’t produce smooth scrolling, and when inserting a long text into a single Text, the animation became erratic. So, I realized I needed to split the text into smaller String units and distribute them across multiple Text elements.
That’s when I found a promising solution in TimelineView.
Below is a sample code from Apple’s documentation.
TimelineView(.periodic(from: startDate, by: 1.0)) { context in
AnalogTimerView(
date: context.date,
showSeconds: context.cadence <= .seconds)
}
TimelineView redraws its content at regular intervals based on a specified TimelineSchedule.
For example, if you specify .periodic(from: startDate, by: 1.0), it triggers every second starting from startDate, making it useful for implementing clock UIs.
This time, I used .animation, which triggers once per frame (the number of frames per second depends on the device and app performance). Internally, it may be using CADisplayLink.
Using this, you can calculate the elapsed time from the startDate to the current date obtained from the context, determine the progress if you have set a total animation time, and apply smooth animations to the View accordingly.
The approach is to horizontally align the Text elements in an HStack and then gradually offset them based on values calculated from TimelineView‘s content.date.
At this point, it seemed like using LazyHStack would suffice, but it wasn’t that simple.
Since this LT is only 5 minutes long, I needed the text to scroll perfectly within that time frame (It’s quite challenging to adjust the speed so that all the text scrolls through in exactly 5 minutes).
Therefore, I had to calculate the total width of the text considering the font, and move it accordingly with offsets. Also, starting with the first text right at the leading edge of the View causes the text to flow off the screen immediately, making it unreadable. So, I wanted the text to start slightly off-screen to the right.
Considering these conditions, implementing this with a standard HStack would be complex, requiring extensive use of GeometryReader or similar techniques.
The width of the text can be calculated using the size function from NSString in UIKit/AppKit.
But considering the other requirements, even with GeometryReader, it would still be a complex implementation.
At that point, I remembered the Layout Protocol I used when implementing PianoUI.
The Layout Protocol overview is explained in the PianoUI implementation article.
By using this, I could horizontally arrange multiple Text elements, split into appropriate text units, just like piano keys, and set the initial position to bounds.width + α, so the text starts off-screen to the right.
In the end, I successfully implemented this using the Layout Protocol, and the code is provided below.
The basic implementation is similar to PianoView, but the key difference is caching the View sizes.
In contrast to PianoView, where subViews were piano key Views with uniform sizes, the AutoScrollTextView subViews are text elements, and their sizes calculated using the size function are likely to vary.
Therefore, calculating sizes and positions for each Text every time placeSubviews runs would hurt performance. That’s why, in this case, I used the cache feature of the Layout Protocol, which I didn’t use in PianoView.
The cache feature is described in Apple’s official documentation (Improve layout efficiency with a cache).
The Layout Protocol defines an optional function makeCache(subviews: Subviews) -> CacheData.
This function runs when the Layout is first created, performing size calculations and storing the results in a cache.
The cache can be used in sizeThatFits and placeSubviews functions, and if needed, you can update the cache with the updateCache function (you would need to define this if the subViews in the Layout change dynamically).
In the case of the AutoScrollTextView, here’s how I defined the cache struct and the makeCache function.
fileprivate struct Cache {
var widths: [CGFloat]
var startPoints: [CGFloat]
var totolWidth: CGFloat
}
fileprivate func makeCache(subviews: Subviews) -> Cache {
var startPoint: CGFloat = 0
var startPoints: [CGFloat] = []
var widths: [CGFloat] = []
subviews.forEach { subView in
let width = subView.sizeThatFits(.unspecified).width
startPoints.append(startPoint)
startPoint += width
widths.append(width)
}
return Cache(widths: widths, startPoints: startPoints, totolWidth: startPoint)
}
The Cache struct holds the width of each Text, the total width, and the x-coordinate positions of all the Text elements when aligned horizontally.
These calculations are all done initially in the makeCache function and stored in the cache, which improves performance by avoiding repeated calculations in the sizeThatFits and placeSubviews functions.
Conclusion
This article summarized the implementation of the AutoScrollTextView, which scrolls text horizontally automatically.
After working on this implementation, I found that the combination of TimelineView and Layout Protocol is a highly versatile tool for adding animations to complex layouts, and there are likely many other ways to use it.
The next article is “6. Extra support for visionOS of PianoUI”.