Skip to content

Instantly share code, notes, and snippets.

@cjnevin
Last active May 4, 2023 14:27
Show Gist options
  • Save cjnevin/fc416e31cfcfb7f5f60a1fb401926f79 to your computer and use it in GitHub Desktop.
Save cjnevin/fc416e31cfcfb7f5f60a1fb401926f79 to your computer and use it in GitHub Desktop.
Infinite Horizontal ScrollView in SwiftUI (no bounce behaviour)
import SwiftUI
struct ContentView: View {
@State var offset: CGFloat = 190
@State var endOffset: CGFloat = 190
@State var items: [Int] = [
1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19
]
var itemSpacing: CGFloat = 10
var itemWidth: CGFloat = 180
var itemWidthWithSpacing: CGFloat {
itemWidth + itemSpacing
}
var itemHeightMultiplier: CGFloat = 0.7
var itemHeight: CGFloat {
itemWidth * itemHeightMultiplier
}
var contentWidth: CGFloat {
CGFloat(items.count) * (itemWidth + itemSpacing)
}
func position(at index: Int) -> CGPoint {
var x = offset + (CGFloat(index) * itemWidthWithSpacing)
if x < 0 {
x += contentWidth
}
// Wrap items when offset is greater than content width
// Subtract half item width to get center position
let halfItemWidth = itemWidthWithSpacing / 2
let proposedX = x.truncatingRemainder(dividingBy: contentWidth) - halfItemWidth
let proposedY = itemHeight / 2
return CGPoint(x: proposedX, y: proposedY)
}
func isValidPosition(at index: Int, proxy: GeometryProxy) -> Bool {
(-itemWidth...proxy.size.width + itemWidth / 2) ~= position(at: index).x
}
var body: some View {
VStack {
Text("Header")
GeometryReader { reader in
ZStack {
ForEach($items.indices, id: \.description) { index in
if isValidPosition(at: index, proxy: reader) {
ItemView(text: "\(items[index])")
.frame(width: itemWidth, height: itemHeight)
.position(position(at: index))
}
}
}
}
.frame(height: itemHeight)
.gesture(
DragGesture()
.onChanged({ gesture in
offset = endOffset + gesture.translation.width
})
.onEnded({ gesture in
// Avoid overflow by truncating value based on contentWidth
endOffset = offset.truncatingRemainder(dividingBy: contentWidth)
})
)
Text("Footer")
Spacer()
}
}
}
struct ItemView: View {
let text: String
var body: some View {
ZStack {
Rectangle().fill(Color.blue)
Text(text)
}
.cornerRadius(10)
.onAppear {
print("appeared \(text)")
}
.onDisappear {
print("disappeared \(text)")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment