Create an auto-scroll 3D carousel in SwiftUI

Deepak Carpenter
Appgrid
Published in
2 min readJun 2, 2023

In this example, we have a ContentView that displays a stack of CarouselCardView instances using a ZStack. The CarouselCardView represents an individual card in the carousel and contains an image and a title.

import SwiftUI

struct ContentView: View {
@State private var currentIndex: Int = 0
private let timer = Timer.publish(every: 5, on: .main, in: .common).autoconnect()

let carouselItems = [
CarouselItem(title: "Gorilla", image: "cover-gorilla"),
CarouselItem(title: "Cheetah", image: "cover-cheetah"),
CarouselItem(title: "Buffalo", image: "cover-buffalo"),
CarouselItem(title: "Elephant", image: "cover-elephant"),
CarouselItem(title: "Giraffe", image: "cover-giraffe"),
// Add more items as needed
]

var body: some View {
VStack {
ZStack {
ForEach(carouselItems.indices) { index in
CarouselCardView(item: carouselItems[index], currentIndex: $currentIndex, cardIndex: index)
.rotation3DEffect(.degrees(getAngle(index: index)), axis: (x: 0, y: 1, z: 0))
.offset(x: getOffsetX(index: index))
.animation(.default)
}
}
.gesture(DragGesture()
.onChanged({ value in
timer.upstream.connect().cancel() // Cancel auto-scrolling while dragging
let offset = value.translation.width
let cardWidth = UIScreen.main.bounds.width * 0.8
let normalizedOffset = Double(offset / cardWidth)
let newIndex = currentIndex - Int(round(normalizedOffset))

currentIndex = min(max(newIndex, 0), carouselItems.count - 1)
})
)
}
.onReceive(timer) { _ in
currentIndex = (currentIndex + 1) % carouselItems.count // Auto-scroll to the next card
}
}

func getAngle(index: Int) -> Double {
let angleOffset = -Double(currentIndex) * 30
let cardIndex = Double(index)
return cardIndex * 30 + angleOffset
}

func getOffsetX(index: Int) -> CGFloat {
let cardIndex = Double(index)
let offset = cardIndex - Double(currentIndex)
let cardWidth = UIScreen.main.bounds.width * 0.8
return CGFloat(offset * cardWidth)
}
}

struct CarouselCardView: View {
let item: CarouselItem
@Binding var currentIndex: Int
let cardIndex: Int

var body: some View {
VStack {
Image(item.image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: UIScreen.main.bounds.width * 0.8, height: UIScreen.main.bounds.height * 0.3)
.cornerRadius(10)

Text(item.title)
.font(.title)
.fontWeight(.bold)
.padding(.top, 10)
}
.frame(width: UIScreen.main.bounds.width * 0.7, height: UIScreen.main.bounds.height * 0.4)
.background(Color.white)
.cornerRadius(10)
.shadow(radius: 5)
.onTapGesture {
currentIndex = cardIndex
}
}
}

struct CarouselItem {
let title: String
let image: String
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

The carousel supports swiping left or right to navigate between cards. It uses a DragGesture to track the drag offset and updates the currentIndex accordingly. The cards are rotated and offset in 3D space based on the currentIndex, giving the illusion of a 3D carousel.

You can customize the carouselItems array to add more items to the carousel. Make sure to replace the image names in the array with the actual image.

Cheers!
Happy Coding!

--

--