Skip to content

Instantly share code, notes, and snippets.

@diogoos
Forked from xtabbas/SnapCarousel.swift
Last active March 25, 2024 15:40
Show Gist options
  • Select an option

  • Save diogoos/90d9b33ce32f20b4a57c638c89c53436 to your computer and use it in GitHub Desktop.

Select an option

Save diogoos/90d9b33ce32f20b4a57c638c89c53436 to your computer and use it in GitHub Desktop.
A carousel that snap items in place built on SwiftUI
//
// CarouselView.swift
//
// Created by xtabbas on 5/7/20.
// Copyright © 2020 xtadevs. All rights reserved.
// Original source available at
// https://gist.github.com/xtabbas/97b44b854e1315384b7d1d5ccce20623
//
// Modified by Diogo Silva on 30/03/21
// Modified source available at
// https://gist.github.com/daemonleaf/90d9b33ce32f20b4a57c638c89c53436
import SwiftUI
struct CarouselConfig {
var spacing: CGFloat
var cardHeight: CGFloat
var overlapSpacing: CGFloat
var cardWidth: CGFloat { UIScreen.main.bounds.width - (overlapSpacing*2) - (spacing*2) }
var leftPadding: CGFloat { overlapSpacing + spacing }
var totalMovement: CGFloat { cardWidth + spacing }
static let `default`: Self = CarouselConfig(spacing: 16, cardHeight: 280, overlapSpacing: 16)
}
public class CarouselModel: ObservableObject {
@Published var activeCard: Int = 1
@Published var screenDrag: Float = 0.0
}
struct Carousel<ItemView : View> : View {
let viewForItem: (Int) -> ItemView
let itemCount: Int
let config: CarouselConfig
@GestureState var isDetectingLongPress = false
@ObservedObject var state: CarouselModel = CarouselModel()
@inlinable public init(items: Int,
_ config: CarouselConfig = .default,
@ViewBuilder _ viewForItem: @escaping (Int) -> ItemView) {
self.viewForItem = viewForItem
self.itemCount = items
self.config = config
}
var body: some View {
let totalSpacing = (CGFloat(itemCount) - 1) * config.spacing
let totalCanvasWidth = (config.cardWidth * CGFloat(itemCount)) + totalSpacing
let xOffsetToShift = (totalCanvasWidth - UIScreen.main.bounds.width) / 2
let activeOffset = xOffsetToShift + (config.leftPadding) - (config.totalMovement * CGFloat(state.activeCard))
let nextOffset = xOffsetToShift + (config.leftPadding) - (config.totalMovement * CGFloat(state.activeCard) + 1)
let calcOffset = activeOffset != nextOffset ? activeOffset + CGFloat(state.screenDrag) : CGFloat(activeOffset)
return HStack(alignment: .center, spacing: config.spacing) {
ForEach((0..<itemCount), id: \.self) { i in
viewForItem(i)
}
}
.offset(x: calcOffset, y: 0)
.gesture(DragGesture().updating($isDetectingLongPress) { currentState, gestureState, transaction in
state.screenDrag = Float(currentState.translation.width)
}.onEnded { value in
state.screenDrag = 0
if value.translation.width < -50 && state.activeCard < itemCount - 1 {
state.activeCard += 1
let impactMed = UIImpactFeedbackGenerator(style: .medium)
impactMed.impactOccurred()
}
if value.translation.width > 50 && state.activeCard > 0 {
if state.activeCard - 1 < 0 { return }
state.activeCard -= 1
let impactMed = UIImpactFeedbackGenerator(style: .medium)
impactMed.impactOccurred()
}
})
}
}
extension View {
func carouselItem(_ config: CarouselConfig = .default) -> some View {
return self
.frame(width: UIScreen.main.bounds.width - (config.overlapSpacing*2) - (config.spacing*2),
height: config.cardHeight)
.cornerRadius(5)
.animation(.spring())
.transition(.slide)
}
}
@diogoos
Copy link
Copy Markdown
Author

diogoos commented Mar 30, 2021

Usage example:

Carousel(items: 5) { item in
    RoundedRectangle(cornerRadius: 5)
         .fill(Color.pink)
         .overlay(Text(String(item)).font(.title).foregroundColor(.white))
         .carouselItem()
}
.padding(.top)

image carousel

@knurldamer
Copy link
Copy Markdown

This was really useful getting a carousel into my project quickly; big thanks to the authors! In the end I've settled on the SwiftUIPager package. It's got more of the control I need (programatic transition), fewer gesture issues (my carousel is in a tab and contains a scrollview) and is highly configurable. This, obviously, at the expense of a larger package. I also found the syntax slightly more straight-forward.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment