Skip to content

Instantly share code, notes, and snippets.

@alexanderkhitev
Forked from xtabbas/SnapCarousel.swift
Created February 22, 2023 14:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alexanderkhitev/b10e3c7da87946a1ab5d7ffba2d7aaea to your computer and use it in GitHub Desktop.
Save alexanderkhitev/b10e3c7da87946a1ab5d7ffba2d7aaea to your computer and use it in GitHub Desktop.
A carousel that snap items in place build on top of SwiftUI
//
// SnapCarousel.swift
// prototype5
//
// Created by xtabbas on 5/7/20.
// Copyright © 2020 xtadevs. All rights reserved.
//
import SwiftUI
struct SnapCarousel: View {
@EnvironmentObject var UIState: UIStateModel
var body: some View {
let spacing: CGFloat = 16
let widthOfHiddenCards: CGFloat = 32 /// UIScreen.main.bounds.width - 10
let cardHeight: CGFloat = 279
let items = [
Card(id: 0, name: "Hey"),
Card(id: 1, name: "Ho"),
Card(id: 2, name: "Lets"),
Card(id: 3, name: "Go")
]
return Canvas {
/// TODO: find a way to avoid passing same arguments to Carousel and Item
Carousel(
numberOfItems: CGFloat(items.count),
spacing: spacing,
widthOfHiddenCards: widthOfHiddenCards
) {
ForEach(items, id: \.self.id) { item in
Item(
_id: Int(item.id),
spacing: spacing,
widthOfHiddenCards: widthOfHiddenCards,
cardHeight: cardHeight
) {
Text("\(item.name)")
}
.foregroundColor(Color.white)
.background(Color("surface"))
.cornerRadius(8)
.shadow(color: Color("shadow1"), radius: 4, x: 0, y: 4)
.transition(AnyTransition.slide)
.animation(.spring())
}
}
}
}
}
struct Card: Decodable, Hashable, Identifiable {
var id: Int
var name: String = ""
}
public class UIStateModel: ObservableObject {
@Published var activeCard: Int = 0
@Published var screenDrag: Float = 0.0
}
struct Carousel<Items : View> : View {
let items: Items
let numberOfItems: CGFloat //= 8
let spacing: CGFloat //= 16
let widthOfHiddenCards: CGFloat //= 32
let totalSpacing: CGFloat
let cardWidth: CGFloat
@GestureState var isDetectingLongPress = false
@EnvironmentObject var UIState: UIStateModel
@inlinable public init(
numberOfItems: CGFloat,
spacing: CGFloat,
widthOfHiddenCards: CGFloat,
@ViewBuilder _ items: () -> Items) {
self.items = items()
self.numberOfItems = numberOfItems
self.spacing = spacing
self.widthOfHiddenCards = widthOfHiddenCards
self.totalSpacing = (numberOfItems - 1) * spacing
self.cardWidth = UIScreen.main.bounds.width - (widthOfHiddenCards*2) - (spacing*2) //279
}
var body: some View {
let totalCanvasWidth: CGFloat = (cardWidth * numberOfItems) + totalSpacing
let xOffsetToShift = (totalCanvasWidth - UIScreen.main.bounds.width) / 2
let leftPadding = widthOfHiddenCards + spacing
let totalMovement = cardWidth + spacing
let activeOffset = xOffsetToShift + (leftPadding) - (totalMovement * CGFloat(UIState.activeCard))
let nextOffset = xOffsetToShift + (leftPadding) - (totalMovement * CGFloat(UIState.activeCard) + 1)
var calcOffset = Float(activeOffset)
if (calcOffset != Float(nextOffset)) {
calcOffset = Float(activeOffset) + UIState.screenDrag
}
return HStack(alignment: .center, spacing: spacing) {
items
}
.offset(x: CGFloat(calcOffset), y: 0)
.gesture(DragGesture().updating($isDetectingLongPress) { currentState, gestureState, transaction in
self.UIState.screenDrag = Float(currentState.translation.width)
}.onEnded { value in
self.UIState.screenDrag = 0
if (value.translation.width < -50) {
self.UIState.activeCard = self.UIState.activeCard + 1
let impactMed = UIImpactFeedbackGenerator(style: .medium)
impactMed.impactOccurred()
}
if (value.translation.width > 50) {
self.UIState.activeCard = self.UIState.activeCard - 1
let impactMed = UIImpactFeedbackGenerator(style: .medium)
impactMed.impactOccurred()
}
})
}
}
struct Canvas<Content : View> : View {
let content: Content
@EnvironmentObject var UIState: UIStateModel
@inlinable init(@ViewBuilder _ content: () -> Content) {
self.content = content()
}
var body: some View {
content
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
.background(Color.white.edgesIgnoringSafeArea(.all))
}
}
struct Item<Content: View>: View {
@EnvironmentObject var UIState: UIStateModel
let cardWidth: CGFloat
let cardHeight: CGFloat
var _id: Int
var content: Content
@inlinable public init(
_id: Int,
spacing: CGFloat,
widthOfHiddenCards: CGFloat,
cardHeight: CGFloat,
@ViewBuilder _ content: () -> Content
) {
self.content = content()
self.cardWidth = UIScreen.main.bounds.width - (widthOfHiddenCards*2) - (spacing*2) //279
self.cardHeight = cardHeight
self._id = _id
}
var body: some View {
content
.frame(width: cardWidth, height: _id == UIState.activeCard ? cardHeight : cardHeight - 60, alignment: .center)
}
}
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