-
-
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)