Skip to content

Instantly share code, notes, and snippets.

@danwood
Last active March 8, 2025 15:35
Show Gist options
  • Save danwood/2375a89157a9f0f165c6a6223fa0ee62 to your computer and use it in GitHub Desktop.
Save danwood/2375a89157a9f0f165c6a6223fa0ee62 to your computer and use it in GitHub Desktop.
SwiftUI synchronized ScrollViews — based on code from Phil Zakharchenko's blog
struct MultiColumnSyncedScrollDemo: View {
let scrollViewCount: Int = 5
@State private var scrollPositions: [ScrollPosition] = [] // Initialized in onAppear, so don't use until body prematurely!
@State private var indexScrolledByUser: Int?
@State private var colors: [Color] = [] // Initially empty
let sampleArrayCount = 40
@ViewBuilder func sampleContent() -> some View {
ForEach(1..<sampleArrayCount, id: \.self) { n in
Text("\(n)")
.frame(width: 160, height: 50)
.background(colors.count > 0 ? colors[n] : Color.clear)
}
}
var body: some View {
HStack {
// This won't show any contents until scrollPositions
// has been initialized (with same count as scrollViewCount)
ForEach(0..<scrollPositions.count, id: \.self) { thisScrollIndex in
ScrollView {
sampleContent()
}
.scrollPosition($scrollPositions[thisScrollIndex])
.onScrollGeometryChange(for: CGFloat.self) { geometry in
geometry.contentOffset.y + geometry.contentInsets.top
} action: { oldValue, newValue in
guard oldValue != newValue else { return }
for i in scrollPositions.indices where i != indexScrolledByUser {
scrollPositions[i].scrollTo(y: newValue)
}
}
.onScrollPhaseChange { _, newPhase in
if newPhase.isScrolling {
indexScrolledByUser = thisScrollIndex
}
}
}
}
.onAppear {
scrollPositions = Array(repeating: ScrollPosition(), count: scrollViewCount)
for _ in 0..<sampleArrayCount {
colors.append(Color.random)
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment