SwiftUI's scroll behavior is vexing.
So, using iOS 18 newest APIs...
If I use a custom id tag and scrollTo on its own, it works fine (actually not fine, it often scrolls to the wrong place with LazyVStack, but thats another topic)
However, if I also want to use onScrollVisibilityChange on one of my items in my ForEach and if I do, it just stops working. Everything still fires, but nothing scrolls.
I need to know when something comes on screen, and I also want to programmatically scroll to places.
It turns out, with custom ids, they must be added after the onScrollVisibilityChange modifier. Does anyone know why? This behavior is very strange, I understand view modifiers wrap each subsequent one, but one modifier shouldn't negate the custom id of another.
You can reproduce with the following code.
struct TestItem: Identifiable, Hashable {
let id: UUID = UUID()
let chapter: Int
let color: Color
static let preview: [TestItem] = [TestItem(chapter: 1, color: .red), TestItem(chapter: 2, color: .green), TestItem(chapter: 3, color: .blue)]
}
struct ScrollTest: View {
let items = TestItem.preview
@State private var scrollPosition = ScrollPosition(idType: Int.self)
var body: some View {
ScrollView {
LazyVStack {
ForEach(items) { item in
ZStack {
Rectangle()
.fill(item.color)
.frame(height: 900)
Text("\(item.chapter)")
.font(.system(size: 90, weight: .bold))
}
.id(item.chapter)
// if the custom id tag is added BEFORE onScrollVisibilityChange modifier, scrollTo will not work
.onScrollVisibilityChange(threshold: 0.02) { visible in
if visible {
print("Item: \(item.chapter) is visible")
}
}
// if the custom id is instead added AFTER, it will work fine (if you don't use custom id tags then it doesn't matter to begin with)
// Why does it need to be this way? Why does the onScrollVisibilityChange affect the custom id?
// .id(item.chapter)
}
}
.scrollTargetLayout()
}
.scrollPosition($scrollPosition, anchor: .top)
.task {
// on start, scroll to second chapter item
scrollPosition.scrollTo(id: 2, anchor: .top)
}
}
}
