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.
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]
}
}