I have the following scrollview that displays a certain number of cards. I want the scrollview to display only 2 cards at a time with slight peeking on the right to indicate more cards. The scrollview should also be scrollable by 2 cards at a time. For example, show card 1 & 2 with card 3 peeking, then on scroll show card 3 & 4 with card 5 peeking and then on scroll show card 5. If the number of cards are odd, the last card should display by itself but with leading alignment in the scrollview. I'd also like each card to be sized such that the 2 card display takes up most of the space such that each card is sized equally and with a spacing of 15 points between the cards and the edges (horizontally & vertically). How can I achieve this?
Here's my code:
struct CardGallery: View {
@State var cards: [Int] = [1,2,3,4,5]
var body: some View {
GeometryReader { geometry in
VStack {
let width = geometry.size.width
let height = geometry.size.height / 1.5
let cardWidth = width / 2.2
let cardHeight = height / 1.2
ZStack {
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 20) {
ForEach(cards, id: \.self) { level in
CardView(level: level)
.frame(width: cardWidth,height: cardHeight)
}
}
.padding(.horizontal)
}
.scrollTargetBehavior(.paging)
.background {
Color.yellow
}
.clipShape(RoundedRectangle(cornerRadius: 15))
.overlay {
RoundedRectangle(cornerRadius: 15)
.strokeBorder(Color.black, lineWidth: 5)
}
}
.frame(width: width, height: height)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.background {
Color.green.ignoresSafeArea(.all)
}
}
}
struct CardView: View {
let level: Int
var body: some View {
VStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 15)
.fill(Color.blue)
}
.padding()
}
}
I would suggest showing the cards in pairs, using an HStack
to combine each pair. Then use ViewAlignedScrollTargetBehavior
to align the pairs to the left side of the scroll view.
Ways of iterating over an array in pairs are shown in the post Iterate over collection two at a time in Swift.
Other changes:
Add .scrollTargetLayout
to the LazyHStack
.
Add leading and trailing padding to the LazyHStack
to give a space before the first card and a space after the last card, also adding the space needed to show the peeking card.
If the last pair only contains one card (in other words, if the number of cards is odd), use a placeholder to fill the space.
Remove the padding from CardView
and manage the spacing in the main view instead.
Here is the updated example to show it working:
struct CardGallery: View {
@State var cards: [Int] = [1,2,3,4,5]
let gapSize: CGFloat = 15
let peekSize: CGFloat = 40
private var cardsInPairs: [(firstCard: Int, secondCard: Int?)] {
stride(from: 0, to: cards.endIndex, by: 2).map {
(cards[$0], $0 < cards.index(before: cards.endIndex) ? cards[$0.advanced(by: 1)] : nil)
}
}
var body: some View {
GeometryReader { geometry in
let width = geometry.size.width
let height = geometry.size.height / 1.5
let cardWidth = (width - (3 * gapSize) - peekSize) / 2
let cardHeight = height - (2 * gapSize)
ZStack {
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: gapSize) {
ForEach(Array(cardsInPairs.enumerated()), id: \.offset) { offset, cardPair in
HStack(spacing: gapSize) {
CardView(level: cardPair.firstCard)
.frame(width: cardWidth, height: cardHeight)
if let secondCard = cardPair.secondCard {
CardView(level: secondCard)
.frame(width: cardWidth, height: cardHeight)
} else {
Color.clear
.frame(width: cardWidth, height: cardHeight)
}
}
}
}
.scrollTargetLayout()
.padding(.leading, gapSize)
.padding(.trailing, gapSize + peekSize)
}
.scrollTargetBehavior(.viewAligned)
.background {
Color.yellow
}
.clipShape(RoundedRectangle(cornerRadius: 15))
.overlay {
RoundedRectangle(cornerRadius: 15)
.strokeBorder(.black, lineWidth: 5)
}
}
.frame(width: width, height: height)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.background(.green)
}
}
struct CardView: View {
let level: Int
var body: some View {
RoundedRectangle(cornerRadius: 15)
.fill(.blue)
.overlay {
Text("\(level)")
.font(.title)
.foregroundStyle(.white)
}
}
}