-
-
Save diogoos/90d9b33ce32f20b4a57c638c89c53436 to your computer and use it in GitHub Desktop.
A carousel that snap items in place built on SwiftUI
This file contains hidden or 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
| // | |
| // 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) | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.