-
Star
(110)
You must be signed in to star a gist -
Fork
(15)
You must be signed in to fork a gist
-
-
Save xtabbas/97b44b854e1315384b7d1d5ccce20623 to your computer and use it in GitHub Desktop.
| // | |
| // 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() | |
| } | |
| } |
Hi @xtabbas, thanks for amazing solution. It really help to find way out!
I would like consider a small improvement in case bouncing at start and the end:
-
Add translation wrapper property
@GestureState var translation: CGFloat = 0 -
In .updating method of gesture you should update this translation property.Like here:
.updating($translation) { value, out, _ in out = value.translation.width self.UIState.screenDrag = Float(value.translation.width) } -
After that, using it for calculation offset
CGFloat(calcOffset) - (translation / 2)
This one will create a scroll limit in the beginning of carousel, and at the end!
Cheers!
Hi @xtabbas, thanks for amazing solution. It really help to find way out!
I would like consider a small improvement in case bouncing at start and the end:
- Add translation wrapper property
@GestureState var translation: CGFloat = 0- In .updating method of gesture you should update this translation property.Like here:
.updating($translation) { value, out, _ in out = value.translation.width self.UIState.screenDrag = Float(value.translation.width) }- After that, using it for calculation offset
CGFloat(calcOffset) - (translation / 2)
This one will create a scroll limit in the beginning of carousel, and at the end!Cheers!
Would you mind sharing your approach?
in NavigavionView set .navigationViewStyle(StackNavigationViewStyle()), There will be a bug back.
add clipped() to Canvas view to remove offset part.
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(.black)
.cornerRadius(8)
.shadow(color: .gray, radius: 4, x: 0, y: 4)
.transition(AnyTransition.slide)
.animation(.spring())
}
}
}
.clipped() <- here!
It looks slow when dragging. so I teak little bit
.animation(.spring())
to
.animation(UIState.screenDrag == 0 ? .easeOut : .linear(duration: 0), value: UIState.screenDrag)