Found a workaround that works for me on both OSX and iOS. I suspected the bug was happening from scrollTo() incorrectly calculating the offset of the particular view in the reference frame of the scrollview, as I was getting different scroll positions depending on how far down the list the scrollTo view was.
Previously the scrollview content was composed of different container views, with subviews inside those. The workaround was to flatten those container views so the requested view IDs to scrollTo are all flat at the top level of the scroll view content.
ScrollView setup:
ScrollViewReader { proxy in
GeometryReader { geometry in
ScrollView(showsIndicators: false) {
VStack(alignment: .leading, spacing: 17) {
ForEach(levelSelect.worlds) { world in
flatRowItems(world: world)
}
}
}
.onAppear {
if let id = levelSelect.selectedLevelID {
proxy.scrollTo(id, anchor: .center)
}
}
.onChange(of: levelSelect.selectedLevelID) { selectedLevelID in
guard let selectedLevelID = selectedLevelID else { return }
if !levelSelect.isInitialSelection {
withAnimation {
proxy.scrollTo(selectedLevelID, anchor: nil)
}
} else {
levelSelect.isInitialSelection = false
proxy.scrollTo(selectedLevelID, anchor: .center)
}
}
}
}
Flattened scrollview content using @ViewBuilder:
@ViewBuilder func flatRowItems(world: World) -> some View {
Group { // previously had a WorldRow which contained the following content as child views
if world.locked {
LockedWorldRow(world: world)
} else {
UnlockedWorldRow(world: world) // previously UnlockedWorldRow contained LevelRows inside
ForEach(world.levels) { level in
LevelRow(levelItem: level)
.id(level.id)
}
}
}
}