Loopy Carousel: Submission to the SwiftUI Series Workarounds challenge
Loopy Carousel
Submission to the SwiftUI Series Workarounds challenge.
Workaround details
In order to create the illusion of looping through the cards/images infinitely, we create two additional copies of the original deck and we tape them toghether making a 3x-length strip.
When the user taps on a card we reposition the strip using a calculated offset so that we are always set around the middle of the tape. We do this explicitely without animation. The goal is for the user not to notice any visual changes at all during this phase.
Finally we transition in the direction of the target card by moving the offset one more time, this time using withAnimation()
. The animation will create the carousel effect by sliding while scaling the card at the center.
Relevant snippets
All the relevant code is in the ContentView source.
The layout consists of an outer, fixed HStack
which wraps an inner, shifting HStack containing all the cards.
HStack(spacing: 0) {
HStack(spacing: Self.spacing) {
ForEach(0 ..< loopCount, id: \.self) { i in
Card(content: cardLabels[i % cardLabels.count], isSelected: .constant(current == i))
.onTapGesture { onSelect(i) }
}
}
.offset(x: offset)
}
.clipped()
Some calculated properties help us render the deck and determine the current offset.
private var loopCount: Int {
cardLabels.count * 3
}
private var cardWidth: CGFloat {
Card.normalWidth + Self.spacing
}
private var evenShift: CGFloat {
loopCount.isMultiple(of: 2)
? -cardWidth / 2
: 0
}
private var offset: CGFloat {
cardWidth * CGFloat(loopCount / 2 - current) + evenShift
}
Our handler for the tap action does the work of preparing the view before triggering the animated transition.
private func onSelect(_ index: Int) -> () {
let jump = abs(current - index)
if (index >= cardLabels.count * 2) {
current = cardLabels.count + (index % cardLabels.count) - jump
withAnimation { current += jump }
} else if (index < cardLabels.count) {
current = cardLabels.count + (index % cardLabels.count) + jump
withAnimation { current -= jump }
} else {
withAnimation { current = index }
}
}