Concept i try to create is two ScrollViews that would sync with each other, when position in one ScrollView changes(via scrolling), position in second ScrollView also changes.
In this example would be expected that when one ScrollView position changes, second ScrollView also updates position, however it does not happen, despite albumScrollId
being updated in both views.
There is button, when pressed will set random albumScrollId
, then both ScrollViews update to same position as expected.
Have no idea why this is not working.
import SwiftUI
struct Album: Identifiable{
let id = UUID()
let name: String
}
struct ScrollContentView: View {
@State var albumScrollId: Album.ID?
@State var albums: [Album] = []
var body: some View {
VStack{
Button("Scroll random"){
albumScrollId = albums.randomElement()?.id
}.buttonStyle(.borderedProminent).padding()
Scroller(albumScrollId: $albumScrollId, albums: albums)
Scroller(albumScrollId: $albumScrollId, albums: albums)
}.onAppear{
albums = [
Album(name: "1"),
Album(name: "2"),
Album(name: "3")
]
albumScrollId = albums[0].id
}
}
}
struct Scroller: View {
@Binding var albumScrollId: Album.ID?
let albums: [Album]
var body: some View {
VStack{
Text("\(albumScrollId?.uuidString ?? "")")
ScrollView(.horizontal){
HStack(spacing: 0){
ForEach(albums){album in
Text("album: \(album.name)").font(.title)
.containerRelativeFrame(.horizontal, alignment: .center)
}
} .scrollTargetLayout()
}.scrollTargetBehavior(.paging)
.scrollIndicators(.never)
.scrollPosition(id: $albumScrollId, anchor: .center)
}
}
}
It would seem that when a ScrollView
updates the binding as a consequence of a scroll action, the change does not cause the other ScrollView
to update. This may be because a ScrollView
interacts with the binding in some internal way. So although the Text
showing the current value does get updated for the "other" Scroller
, the associated ScrollView
ignores the change, perhaps because it thinks it is already positioned at the new id.
One way to fix is to use two state variables so that each ScrollView
has a dedicated binding. Then use .onChange
handlers to detect and propagate changes between them:
struct ScrollContentView: View {
@State var albumScroll1Id: Album.ID?
@State var albumScroll2Id: Album.ID?
@State var albums: [Album] = []
var body: some View {
VStack{
Button("Scroll random"){
albumScroll1Id = albums.randomElement()?.id
}
.buttonStyle(.borderedProminent).padding()
Scroller(albumScrollId: $albumScroll1Id, albums: albums)
Scroller(albumScrollId: $albumScroll2Id, albums: albums)
}
.onAppear{
albums = [
Album(name: "1"),
Album(name: "2"),
Album(name: "3")
]
albumScroll1Id = albums[0].id
}
.onChange(of: albumScroll1Id) { oldVal, newVal in
if albumScroll2Id != newVal {
albumScroll2Id = newVal
}
}
.onChange(of: albumScroll2Id) { oldVal, newVal in
if albumScroll1Id != newVal {
albumScroll1Id = newVal
}
}
}
}
Btw, I tried using a computed binding for albumScroll2Id
but this didn't work either. Again, presumably to do with the way that the ScrollView
is interacting with the binding.