Skip to content

Instantly share code, notes, and snippets.

@levochkaa
Created August 23, 2023 13:50
Show Gist options
  • Save levochkaa/38d5203c8b2f14943d261df620092a26 to your computer and use it in GitHub Desktop.
Save levochkaa/38d5203c8b2f14943d261df620092a26 to your computer and use it in GitHub Desktop.
CarouselView in SwiftUI
import Combine
import SwiftUI
// https://stackoverflow.com/questions/58896661/swiftui-create-image-slider-with-dots-as-indicators
struct CarouselView<Content>: View where Content: View {
let maxIndex: Int
var content: () -> Content
@State private var offset = CGFloat.zero
@State private var dragging = false
@State var index = 0
@State var cancelDrag: AnyCancellable?
init(count: Int?, @ViewBuilder content: @escaping () -> Content) {
if count != nil {
self.maxIndex = count! - 1
} else {
if let value = Mirror(reflecting: content()).descendant("value") {
let lessonMirror = Mirror(reflecting: value)
self.maxIndex = lessonMirror.children.count - 1
} else {
self.maxIndex = 0
}
}
self.content = content
}
func onEnded(_ value: _ChangedGesture<DragGesture>.Value, geometry: GeometryProxy) {
cancelDrag?.cancel()
let predictedEndOffset = -CGFloat(index) * geometry.size.width + value.predictedEndTranslation.width
let predictedIndex = Int(round(predictedEndOffset / -(geometry.size.width + 0.0001)))
index = clampedIndex(from: predictedIndex)
withAnimation(.easeOut) { // animation only on false!
dragging = false
}
}
var body: some View {
ZStack(alignment: .bottom) {
GeometryReader { geometry in
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 0) {
content()
.frame(width: geometry.size.width, height: geometry.size.height)
.clipped()
}.allowsHitTesting(false)
}
.content
.offset(x: offset(in: geometry), y: 0)
.frame(width: geometry.size.width, height: geometry.size.height, alignment: .leading)
.clipped()
.overlay {
Rectangle()
.opacity(0.00001)
.gesture(
DragGesture(minimumDistance: 20)
.onChanged { value in
dragging = true
offset = -CGFloat(index) * geometry.size.width
+ max(min(value.translation.width, geometry.size.width), -geometry.size.width)
let just = Just<Bool>(true).delay(for: .seconds(1), scheduler: RunLoop.main)
cancelDrag?.cancel()
cancelDrag = just.sink { _ in
onEnded(value, geometry: geometry)
}
}
.onEnded { value in
onEnded(value, geometry: geometry)
},
including: .gesture
)
}
}
if maxIndex > 0 {
PageIndicator(index: $index, maxIndex: maxIndex)
}
}
}
func offset(in geometry: GeometryProxy) -> CGFloat {
if dragging {
return max(min(offset, 0), -CGFloat(maxIndex) * geometry.size.width)
} else {
return -CGFloat(index) * geometry.size.width
}
}
func clampedIndex(from predictedIndex: Int) -> Int {
let newIndex = min(max(predictedIndex, index - 1), index + 1)
return max(min(newIndex, maxIndex), 0)
}
}
struct PageIndicator: View {
@Binding var index: Int
let maxIndex: Int
var body: some View {
HStack(spacing: 8) {
ForEach(0...maxIndex, id: \.self) { currentIndex in
ZStack {
Circle()
.fill(.white)
.frame(width: 12, height: 12)
if currentIndex == index {
Circle()
.fill(.blue)
.frame(width: 9.2, height: 9.2)
} else {
Circle()
.stroke(.blue, lineWidth: 1.2)
.frame(width: 8, height: 8)
}
}
}
}
.padding(15)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment