Home > database >  Animating Wheel of Fortune style view in SwiftUI
Animating Wheel of Fortune style view in SwiftUI

Time:01-13

I'm implementing a Wheel of Fortune-style SwiftUI view. It takes an array of Players and shows a wheel with slices representing each player.

I can show slices and labels correctly but struggle with the animation part of the view—specifically, animating labels to stick with their corresponding slice shape.

This is the current state of my implementation. As you can see even though I'm animating labels to the correct end position, they're not sticking with their slice and animate independently.

enter image description here

I am looking to update my below code to achieve a more realistic animation.

This is the main WheelView that creates slices and labels and handles animation

struct WheelView: View {
    let players: [Player]
    let colors = (0...10).map { _ in Color.random() }
    @State var rotation = 0.0
    
    private var anglePerSlice: Double {
        360.0 / Double(players.count)
    }
    
    func offsetDegree(index: Int) -> Double{
        (Double(index) * anglePerSlice   anglePerSlice / 2   rotation).degreesToRadians
    }
    var body: some View {
        VStack {
            ZStack {
                ForEach(Array(zip(players.indices, players)), id: \.0) { index, player in
                    SliceView(player: player,
                              index: index,
                              angle: anglePerSlice,
                              rotation: rotation)
                    .fill(colors[index])
                    .overlay(
                        ZStack {
                            SliceBorder(index: index,
                                        angle: anglePerSlice)
                            .stroke(Color.black, lineWidth: 3)
                            .rotationEffect(.degrees(rotation))
                        
                            Text(player.name)
                                .rotationEffect(.degrees(Double(index) * anglePerSlice   anglePerSlice / 2   rotation))
                                .offset(x: cos(offsetDegree(index: index)) * 150 ,
                                        y: sin(offsetDegree(index: index)) * 150)
                        }
                    )
                }
            }
            .onTapGesture {
                let randomAmount = Double(Int.random(in: 700..<1500))
                rotation  = randomAmount
            }
            .animation(.easeInOut(duration: 1.5), value: rotation)
        }.padding()
    }
} 

This is SliceView which is a simple Shape that draws an arc

struct SliceView: Shape {
    var player: Player
    var index: Int
    var angle: Double
    var rotation: Double
    
    func path(in rect: CGRect) -> Path {
        let center = CGPoint(x: rect.midX, y: rect.midY)
        let startAngle = Double(index) * angle
        let endAngle = angle   Double(index) * angle
        var path = Path()

        path.move(to: center)
        path.addArc(center: center,
                    radius: rect.width / 2,
                    startAngle: .degrees(startAngle),
                    endAngle: .degrees(endAngle),
                    clockwise: false)
        
        path.closeSubpath()
        return path.rotation(.degrees(rotation)).path(in: rect)
    }
    
    var animatableData: Double {
        get { return rotation }
        set { rotation = newValue }
    }
}

And couple of helpers

struct SliceBorder: Shape {
    var index: Int
    var angle: Double
    
    func path(in rect: CGRect) -> Path {
        let center = CGPoint(x: rect.midX, y: rect.midY)
        let startAngle = Double(index) * angle
        let endAngle = angle   Double(index) * angle
        var path = Path()
        path.move(to: center)
        path.addArc(center: center,
                    radius: rect.width / 2,
                    startAngle: .degrees(startAngle),
                    endAngle: .degrees(endAngle),
                    clockwise: false)
        
        path.closeSubpath()

        return path
    }
}

extension Double {
    var degreesToRadians: Double { return Double(self) * .pi / 180 }
}


SwiftUI somehow decides to animate the label's offset smoothly from rotation's start to end value. In reality, for offset, I want rotation's value to increase one by one so that offset can have a circular animation effect.

Any suggestions on how can I achieve this?

CodePudding user response:

You just need to apply rotation to the wheel as a whole, rather to the individual subviews. Here's a simpler implementation of a fortune wheel, that works as you need:

struct ContentView: View {
    
    @State var rotation: CGFloat = 0.0
    
    var body: some View {
        VStack {
            Wheel(rotation: $rotation)
                .frame(width: 200, height: 200)
                .rotationEffect(.radians(rotation))
                .animation(.easeInOut(duration: 1.5), value: rotation)
            Button("Spin") {
                let randomAmount = Double(Int.random(in: 7..<15))
                rotation  = randomAmount
            }
        }
        .padding()
    }
}


struct Wheel: View {
    
    @Binding var rotation: CGFloat
    
    let segments = ["Steve", "John", "Bill", "Dave", "Alan"]
    
    var body: some View {
        GeometryReader { proxy in
            ZStack {
                ForEach(segments.indices, id: \.self) { index in
                    ZStack {
                        Circle()
                            .inset(by: proxy.size.width / 4)
                            .trim(from: CGFloat(index) * segmentSize, to: CGFloat(index   1) * segmentSize)
                            .stroke(Color.all[index], style: StrokeStyle(lineWidth: proxy.size.width / 2))
                            .rotationEffect(.radians(.pi * segmentSize))
                            .opacity(0.3)
                        label(text: segments[index], index: CGFloat(index), offset: proxy.size.width / 4)
                    }
                }
            }
        }
    }
    
    var segmentSize: CGFloat {
        1 / CGFloat(segments.count)
    }
    
    func rotation(index: CGFloat) -> CGFloat {
        (.pi * (2 * segmentSize * (CGFloat(index   1))))
    }
    
    func label(text: String, index: CGFloat, offset: CGFloat) -> some View {
        Text(text)
            .rotationEffect(.radians(rotation(index: CGFloat(index))))
            .offset(x: cos(rotation(index: index)) * offset, y: sin(rotation(index: index)) * offset)
    }
}

extension Color {
    
    static var all: [Color] {
        [Color.yellow, .green, .pink, .cyan, .mint, .orange, .teal, .indigo]
    }
}

enter image description here

  • Related