Last active
August 11, 2021 11:18
-
-
Save alexdremov/b1a16f5adc47c6a59e3625f0b980ff7e to your computer and use it in GitHub Desktop.
Swift remastered minimal snap carousel View
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// SnapCarousel.swift | |
// Spontanea | |
// | |
// Created by Alex Dremov on 10.08.2021. | |
// | |
import Foundation | |
import SwiftUI | |
struct Card: Decodable, Hashable, Identifiable { | |
var id: Int | |
var name: String = "" | |
} | |
struct SnapCarousel: View { | |
@State var state = CarouselState() | |
var body: some View { | |
let items = [ | |
Card(id: 0, name: "Hey"), | |
Card(id: 1, name: "Ho"), | |
Card(id: 2, name: "Lets"), | |
Card(id: 3, name: "Go") | |
] | |
let width = UIConfig.screenWidth | |
state.cardsNum = items.count | |
return Carousel(cardWidth: width, | |
state: $state){ | |
ForEach(items, id:\.id) { item in | |
Text(item.name) | |
.padding() | |
.background(Color.black) | |
.foregroundColor(Color.white) | |
.cornerRadius(8) | |
.shadow(color: Color.black, radius: 4, x: 0, y: 4) | |
.frame(width: width, height: 100, alignment: .center) | |
.transition(AnyTransition.slide) | |
.animation(.spring()) | |
} | |
} | |
} | |
} | |
public class CarouselState: ObservableObject { | |
@Published var cardsNum: Int = 0 | |
@Published var activeCard: Int = 0 | |
} | |
struct Carousel<Content : View>: View { | |
@State var offset: CGFloat = 0 | |
@Binding var state: CarouselState | |
@State var screenDrag: Float = 0.0 | |
var onSnap: ()->() = {} | |
let cardWidth: CGFloat | |
let items: Content | |
@inline(__always) func updateOffset() { | |
self.offset = self.cardWidth * CGFloat(self.state.activeCard) + CGFloat(self.screenDrag) | |
} | |
@inline(__always) func updateOffset(_ loc: Int) { | |
self.offset = self.cardWidth * CGFloat(loc) + CGFloat(self.screenDrag) | |
} | |
@inlinable public init( | |
cardWidth: CGFloat, | |
state: Binding<CarouselState>, | |
onSnap: @escaping ()->() = {}, | |
@ViewBuilder items: () -> Content) { | |
self.items = items() | |
self.cardWidth = cardWidth | |
self._state = state | |
self.onSnap = onSnap | |
} | |
var body: some View { | |
return Group{ | |
HStack(alignment: .center, spacing: 0) { | |
items | |
.frame(minWidth: 0, | |
maxWidth: .infinity, | |
alignment: .leading) | |
} | |
.frame(width: cardWidth * CGFloat(state.cardsNum), alignment: .leading) | |
} | |
.offset(x: -offset + CGFloat(screenDrag), y: 0) | |
.frame(width: cardWidth, alignment: .leading) | |
.contentShape(Rectangle()) | |
.simultaneousGesture(DragGesture(minimumDistance: 0) | |
.onChanged { currentState in | |
self.screenDrag = Float(currentState.translation.width) | |
}.onEnded { value in | |
self.screenDrag = 0 | |
if self.state.cardsNum != 0 { | |
if (value.translation.width < -50) { | |
self.state.activeCard = (self.state.activeCard + 1) % self.state.cardsNum | |
onSnap() | |
} | |
if (value.translation.width > 50) { | |
self.state.activeCard = (self.state.cardsNum + self.state.activeCard - 1) % self.state.cardsNum | |
onSnap() | |
} | |
} | |
}) | |
.onReceive(state.$activeCard, perform: { activeCard in | |
updateOffset(activeCard) | |
}) | |
.onReceive(state.$cardsNum, perform: { activeCard in | |
updateOffset(activeCard) | |
}) | |
} | |
} | |
struct SnapCarousel_Previews: PreviewProvider { | |
static var previews: some View { | |
SnapCarousel() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment