Currently i am trying really hard to create experience like the Apple fitness app. So the main view is a single day and the user can swipe between days. The week would be displayed in the toolbar and provide a shortcut to scroll to the right day.
I had many attempts at solving this and it can work. You can create such an interface with SwiftUI. However, changing the data on every scroll makes limiting view updates hard and additionally the updates are not related to my code directly. Instruments show me long updates, but they belong to SwiftUI and all the advice i found does not apply or help.
struct ContentView: View {
@State var journey = JourneyPrototype(selection: 0)
@State var position: Int? = 0
var body: some View {
ScrollView(.horizontal) {
LazyHStack(spacing: 0) {
ForEach(journey.collection, id: \.self) { index in
Listing(index: index)
.id(index)
}
}
.scrollTargetLayout()
}
.scrollTargetBehavior(.paging)
.scrollPosition(id: $position)
.onChange(of: position) { oldValue, newValue in
journey.selection = newValue ?? 0
journey.update()
}
.onScrollPhaseChange { oldPhase, newPhase in
if newPhase == .idle {
journey.commit()
}
}
}
}
struct Listing: View {
var index: Int
var body: some View {
List {
Section {
Text("Title")
.font(.largeTitle)
.padding()
}
Section {
Text("\(index)")
.font(.largeTitle)
.padding()
}
Section {
Text("1 ")
Text("2 ")
Text("3 ")
Text("4 ")
Text("5 ")
Text("6 ")
}
}
.containerRelativeFrame(.horizontal)
}
}
@Observable
class JourneyPrototype {
var selection: Int
var collection: [Int]
var nextUp: [Int]?
init(selection: Int) {
self.selection = selection
self.collection = [selection]
Task {
self.collection = [-2,-1,0,1,2]
}
}
func update() {
self.nextUp = [
self.selection - 2,
self.selection - 1,
selection,
self.selection + 1,
self.selection + 2
]
}
func commit() {
self.collection = self.nextUp ?? self.collection
self.nextUp = nil
}
}
#Preview {
ContentView()
}
There are some major Problem with this abstracted prototype
- ScrollView has no good trigger for the update, because if i update on change of the position, it will update much more than once. Thats why i had to split calculation and applying the diff
- The LazyHStack is not optimal, because there are only 5 View in the example, but using HStack breaks the scrollPosition
- Each scroll updates all List, despite changing only 2 numbers in the array. AI recommended to append and remove, which does nothing about the updates.
- In my actual Code i do this with Identifiable data and the Problem is the same. So the data itself is not the problem?
Please consider, this is just the rough prototype to explain the problem, i am aware that an array of Ints is not ideal here, but the problem is the same in Instruments and much shorter to post.
Why am i posting this? Scrolling through dynamic data is required for many apps, but there is no proper solution to this online. Github and Blogs are fine with showing a progress indicator and letting the user wait, some probably perform worse than this prototype. Other solutions require UIKit like using a UIPageViewController. But even using this i run in small hitches related to layout.
Important consideration, my data for the scrollview is too big to be calculated upfront. 100 years of days that are calculated for my domain logic take too long, so i have no network request, but the need to only act on a smaller window of data.
Instruments shows long update for one scroll action tested on a iPhone SE 2nd generation
- ListRepresentable has 7 updates and takes 17ms
- LazySubViewPlacements<LazyHStackLayout> has 2 updates and takes 8ms
- Other long updates are too verbose to include
I would be very grateful for any help.