Create a Mesmerizing 3D Cube Animation with SwiftUI
Welcome to the exciting world of SwiftUI and 3D animations! Whether you’re just starting your iOS development journey or looking to spice up your app with eye-catching visuals, this tutorial is for you. We’ll dive into creating a stunning 3D cube animation using SwiftUI, all the while keeping our code clean and maintainable with the MVVM architectural pattern.
Before We Begin
Ensure you have Xcode installed on your Mac and a basic understanding of SwiftUI. This guide is crafted to be beginner-friendly, so don’t worry if you’re just dipping your toes into the world of iOS development.
Setting Up Your SwiftUI Project
Open Xcode and create a new SwiftUI project. Name it “Cube3DAnimation” or anything that sparks joy. We’ll start with a blank canvas to build our masterpiece.
Crafting the Data Model
Our animation consists of a 3D cube that can rotate, so let’s define our cubes. The AllCubes
enum represents each face of the cube with distinct cases (.one
, .two
, .three
, etc.), where each case corresponds to a different color of the cube face. This structure is pivotal for managing the cube's appearance during the animation, allowing us to switch views based on the cube's state:
import SwiftUI
//MARK: - AllCubes Enum
enum AllCubes: CaseIterable{
static var indexOffset : Int = 0
case one, two, three, four, five, clear
var view: AnyView{
switch self{
case .one, .two, .three, .four, .five:
return AnyView(Image(name()).resizable().frame(width: 80,height: 80))
default:
return AnyView(EmptyView())
}
}
private func name() -> String{
switch self {
case .one: return "green"
case .two: return "yellow"
case .three: return "red"
case .four: return "blue"
case .five: return "orange"
default: return ""
}
}
}
This approach ensures that our cube’s faces are easily identifiable and modifiable, allowing for a dynamic and interactive animation experience.
Implementing the ViewModel
The CubeViewModel
acts as the intermediary between our model and views, utilizing the @Published
properties to trigger UI updates when our cube's state changes. The rotation logic is encapsulated within the startRotation
and rotate
functions. These functions utilize SwiftUI's animation capabilities to smoothly transition between cube states:
import SwiftUI
class CubeViewModel: ObservableObject{
@Published var allCubes = AllCubes.allCases
@Published var allIndicies: [(CGFloat,CGFloat,Double,Bool)] = [
(-80, 40, 5, true),
(-40, 20, 3, false),
(0, 0, 1, false),
(40, 20, 2, true),
(0, 40, 4, false),
(-40, 60, 6, false)
]
@Published var currentIndex : Int = 4
func startRotation(){
withAnimation{
rotate()
}
}
private func rotate(){
let clearPosition = allIndicies[5]
allIndicies[5] = allIndicies[currentIndex]
allIndicies[currentIndex] = clearPosition
currentIndex = currentIndex - 1
if currentIndex == -1 {
currentIndex = 4
}
if allIndicies[currentIndex].3 {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1){
withAnimation{
self.rotate()
}
}
}else{
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3){
withAnimation{
self.rotate()
}
}
}
}
}
This class uses SwiftUI’s ObservableObject
to update our views whenever our cube's state changes. The rotation logic cleverly manipulates the cube’s state to create the illusion of a rotating 3D object, showcasing the power of SwiftUI’s animation system.
Designing the View
In the view layer, ContentView
acts as the entry point, which then leads to FinalView
where the cube and its animations are displayed. This separation ensures that our SwiftUI app is structured and manageable:
import SwiftUI
//MARK: - ContentView
struct ContentView: View {
var body: some View {
FinalView()
}
}
#Preview {
ContentView()
}
struct FinalView: View {
var body: some View {
ZStack{
Color.black
.ignoresSafeArea()
CubesView()
.offset(x:0, y: -95)
bottomPanel()
}
}
// Bottom panel with welcome text and arrow
@ViewBuilder
private func bottomPanel() -> some View{
ZStack{
RoundedRectangle(cornerRadius: 75)
.frame(width:450,height: 400)
.foregroundStyle(.gray).opacity(0.2)
.offset(x:0,y:350)
VStack{
Text("WELCOME")
.font(.title)
.fontWeight(.regular)
.foregroundStyle(Color.white)
arrowCircle()
}
.offset(x:0,y:300)
}
}
//Arrow inside a circle
@ViewBuilder
private func arrowCircle() -> some View{
ZStack{
Circle()
.frame(width: 75,height: 75)
.foregroundStyle(.clear)
.overlay(Circle().stroke(Color.white,lineWidth: 2))
Image(systemName: "arrow.right")
.resizable()
.frame(width: 35,height: 30)
.foregroundStyle(.white)
}
}
}
//MARK: - CubesView
struct CubesView:View {
var body: some View {
ZStack{
ForEach(0 ..< 10){index in
CubeSetView()
.offset(x:100)
.rotationEffect(.degrees(Double(index) * 60 ))
}
}
}
}
//MARK: - CubeSet View
struct CubeSetView : View {
@ObservedObject var viewModel = CubeViewModel()
var body: some View {
ZStack{
ForEach(0..<viewModel.allCubes.count, id : \.self){index in
cubeView(index:index)
}
}
.onAppear(perform: viewModel.startRotation)
}
private func cubeView(index: Int) -> some View{
let offset = viewModel.allIndicies[index]
return viewModel.allCubes[index].view
.offset(x: offset.0,y: offset.1)
.zIndex(offset.2)
}
}
The FinalView
composes the cube using CubesView
, a component that orchestrates the individual cube faces and controls their animation through the ViewModel.
Animation Logic
The real magic happens in how we define our animations within SwiftUI. By leveraging SwiftUI’s withAnimation
and animation
modifiers, we create smooth, continuous rotations that bring our cube to life. The key is in how we update the allIndicies
within CubeViewModel
to change each cube face's position and rotation angle, then use these updates to animate our views.
Wrapping Up
Congratulations! You’ve just created a captivating 3D cube animation with SwiftUI. Experiment with different animations, colors, and sizes. SwiftUI’s power is in your hands now, and the possibilities are endless.
Don’t forget to share your creations and any questions you might have. Happy coding!