parent
5c07386863
commit
6547a8de01
@ -0,0 +1,288 @@ |
||||
// |
||||
// FortuneWheelView.swift |
||||
// PadelClub |
||||
// |
||||
// Created by Razmig Sarkissian on 22/04/2024. |
||||
// |
||||
|
||||
import SwiftUI |
||||
|
||||
protocol SpinDrawable { |
||||
func segmentLabel() -> String |
||||
} |
||||
|
||||
extension String: SpinDrawable { |
||||
func segmentLabel() -> String { |
||||
self |
||||
} |
||||
} |
||||
|
||||
extension Match: SpinDrawable { |
||||
func segmentLabel() -> String { |
||||
self.matchTitle(.wide) |
||||
} |
||||
} |
||||
|
||||
extension TeamRegistration: SpinDrawable { |
||||
func segmentLabel() -> String { |
||||
self.teamLabel(.short) |
||||
} |
||||
} |
||||
|
||||
struct DrawResult: Identifiable { |
||||
let id: UUID = UUID() |
||||
let drawee: Int |
||||
let drawIndex: Int |
||||
} |
||||
|
||||
struct DrawOption: Identifiable, SpinDrawable { |
||||
let id: UUID = UUID() |
||||
let initialIndex: Int |
||||
let option: SpinDrawable |
||||
|
||||
func segmentLabel() -> String { |
||||
option.segmentLabel() |
||||
} |
||||
} |
||||
|
||||
struct SpinDrawView: View { |
||||
@Environment(\.dismiss) private var dismiss |
||||
|
||||
let time: Date = Date() |
||||
let drawees: [any SpinDrawable] |
||||
@State var segments: [any SpinDrawable] |
||||
let completion: ([DrawResult]) -> Void // Completion closure |
||||
|
||||
@State private var drawCount: Int = 0 |
||||
@State private var draws: [DrawResult] = [DrawResult]() |
||||
@State private var drawOptions: [DrawOption] = [DrawOption]() |
||||
|
||||
var autoMode: Bool { |
||||
drawees.count > 1 |
||||
} |
||||
|
||||
func validationLabel(drawee: Int, result: SpinDrawable) -> String { |
||||
let draw = drawees[drawee] |
||||
return draw.segmentLabel() + " -> " + result.segmentLabel() |
||||
} |
||||
|
||||
@State private var selectedIndex: Int? |
||||
var body: some View { |
||||
List { |
||||
Section { |
||||
Text(time.formatted(date: .complete, time: .complete)) |
||||
Text(time, style: .timer) |
||||
} |
||||
|
||||
if let selectedIndex { |
||||
Section { |
||||
Text(validationLabel(drawee: drawCount, result: segments[draws.last!.drawIndex])) |
||||
if autoMode == false || drawCount == drawees.count { |
||||
RowButtonView("ok") { |
||||
dismiss() |
||||
} |
||||
} else { |
||||
Text("Prochain tirage en préparation") |
||||
} |
||||
} |
||||
} else if drawCount < drawees.count { |
||||
Section { |
||||
Text(drawees[drawCount].segmentLabel()) |
||||
} |
||||
|
||||
Section { |
||||
FortuneWheelTestView(segments: drawOptions, autoMode: autoMode) { index in |
||||
self.selectedIndex = index |
||||
self.draws.append(DrawResult(drawee: drawCount, drawIndex: drawOptions[index].initialIndex)) |
||||
self.drawOptions.remove(at: index) |
||||
|
||||
if autoMode && drawCount < drawees.count { |
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { |
||||
self.drawCount += 1 |
||||
if drawOptions.count == 1 { |
||||
self.draws.append(DrawResult(drawee: self.drawCount, drawIndex: self.drawOptions[0].initialIndex)) |
||||
self.drawOptions.remove(at: 0) |
||||
self.drawCount += 1 |
||||
self.selectedIndex = nil |
||||
} else { |
||||
self.selectedIndex = nil |
||||
} |
||||
} |
||||
} |
||||
} |
||||
.listRowBackground(Color.clear) |
||||
.listRowSeparator(.hidden) |
||||
} |
||||
} else { |
||||
Section { |
||||
Text("Tous les tirages sont terminés") |
||||
ForEach(draws) { drawResult in |
||||
Text(validationLabel(drawee: drawResult.drawee, result: segments[drawResult.drawIndex])) |
||||
} |
||||
} |
||||
|
||||
RowButtonView("Valider les tirages") { |
||||
completion(draws) |
||||
dismiss() |
||||
} |
||||
} |
||||
|
||||
Section { |
||||
Text("XXX") |
||||
Text("XXX") |
||||
Text("XXX") |
||||
} header: { |
||||
Text("Comité du tournoi") |
||||
} |
||||
} |
||||
.listStyle(.insetGrouped) |
||||
.scrollDisabled(true) |
||||
.onAppear { |
||||
for (index, segment) in segments.enumerated() { |
||||
drawOptions.append(DrawOption(initialIndex: index, option: segment)) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
struct FortuneWheelTestView: View { |
||||
@State private var rotation: Double = 0 |
||||
let segments: [any SpinDrawable] |
||||
let autoMode: Bool |
||||
let completion: (Int) -> Void // Completion closure |
||||
|
||||
var body: some View { |
||||
FortuneWheelView(segments: segments) |
||||
.rotationEffect(.degrees(rotation)) |
||||
.aspectRatio(contentMode: .fill) |
||||
.padding(.top, 5) |
||||
.overlay(alignment: .top) { |
||||
Triangle() |
||||
.fill(Color.red) |
||||
.stroke(Color.black, lineWidth: 2) |
||||
.frame(width: 20, height: 20) |
||||
.rotationEffect(.degrees(180)) |
||||
|
||||
} |
||||
.onAppear { |
||||
if autoMode { |
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { |
||||
rollWheel() |
||||
} |
||||
} |
||||
} |
||||
.gesture( |
||||
DragGesture() |
||||
.onChanged { value in |
||||
// Calculate rotation based on the velocity of the drag |
||||
let initialVelocity = value.predictedEndTranslation.width / 10 // Adjust sensitivity |
||||
rotation += Double(initialVelocity) |
||||
} |
||||
.onEnded { value in |
||||
// Roll the wheel when drag ends |
||||
rollWheel() |
||||
} |
||||
) |
||||
} |
||||
|
||||
func rollWheel() { |
||||
|
||||
rotation = 0 |
||||
|
||||
// Generate a random angle for the wheel to rotate |
||||
let randomAngle = Double.random(in: 1440...2880) // Adjust range for more or less rotations |
||||
let duration = Double.random(in: 2...4) |
||||
|
||||
// Apply rotation animation with ease-out |
||||
withAnimation(.easeOut(duration: duration)) { |
||||
rotation += randomAngle |
||||
} |
||||
|
||||
|
||||
// Calculate the angle between the top center and the current rotation position |
||||
let segmentAngle = 360.0 / Double(segments.count) |
||||
let arrowAngle = 270.0 // Angle of the arrow at the top center of the wheel |
||||
let normalizedRotation = (rotation + 360).truncatingRemainder(dividingBy: 360) |
||||
let angleToTopCenter = (arrowAngle - normalizedRotation + 360).truncatingRemainder(dividingBy: 360) |
||||
|
||||
// Determine the selected segment based on the angle |
||||
let index = Int(angleToTopCenter / segmentAngle) |
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + duration + 1) { |
||||
let selectedSegment = index < 0 ? segments.count + index : index // Normalize index |
||||
completion(selectedSegment) |
||||
} |
||||
} |
||||
|
||||
|
||||
|
||||
} |
||||
|
||||
struct FortuneWheelView: View { |
||||
let segments: [any SpinDrawable] |
||||
let colors: [Color] = [.yellow, .cyan, .green, .blue, .orange, .purple, .mint, .brown] |
||||
|
||||
func getColor(forIndex index: Int) -> Color { |
||||
if index < colors.count { |
||||
return colors[index] |
||||
} |
||||
|
||||
let level = Double(index)/Double(colors.count) / 100 |
||||
let modulo = colors[index%colors.count].variation(withHueOffset: level) |
||||
|
||||
return modulo |
||||
} |
||||
|
||||
var body: some View { |
||||
GeometryReader { proxy in |
||||
let radius = proxy.size.width / 2 |
||||
ZStack { |
||||
ForEach(segments.indices, id: \.self) { index in |
||||
Path { path in |
||||
let segmentAngle = 360.0 / Double(segments.count) |
||||
let startAngle = Angle(degrees: Double(index) * segmentAngle) |
||||
let endAngle = Angle(degrees: Double(index + 1) * segmentAngle) |
||||
let center = CGPoint(x: radius, y: radius) |
||||
path.move(to: center) |
||||
path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: false) |
||||
path.closeSubpath() |
||||
} |
||||
.fill(getColor(forIndex:index)) |
||||
|
||||
Text(segments[index].segmentLabel()).multilineTextAlignment(.trailing) |
||||
.rotationEffect(.degrees(Double(index) * (360 / Double(segments.count)) + (360 / Double(segments.count) / 2))) |
||||
.foregroundColor(.white) |
||||
.position(arcPosition(index: index, radius: radius)) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Calculate the position for the text in the middle of the arc segment |
||||
private func arcPosition(index: Int, radius: Double) -> CGPoint { |
||||
let segmentAngle = 360.0 / Double(segments.count) |
||||
let startAngle = Double(index) * segmentAngle |
||||
let endAngle = Double(index + 1) * segmentAngle |
||||
let centerAngle = (startAngle + endAngle) / 2 |
||||
let adjustedRadius = radius - 20.0 |
||||
let x = radius + (adjustedRadius - 30) * cos(centerAngle * .pi / 180) // Adjusted radius for better fit |
||||
let y = radius + (adjustedRadius - 30) * sin(centerAngle * .pi / 180) // Adjusted radius for better fit |
||||
return CGPoint(x: x, y: y) |
||||
} |
||||
} |
||||
|
||||
struct Triangle: Shape { |
||||
func path(in rect: CGRect) -> Path { |
||||
var path = Path() |
||||
path.move(to: CGPoint(x: rect.midX, y: rect.minY)) |
||||
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY)) |
||||
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY)) |
||||
path.closeSubpath() |
||||
return path |
||||
} |
||||
} |
||||
|
||||
#Preview { |
||||
SpinDrawView(drawees: ["3", "4"], segments: ["1", "2"]) { draws in |
||||
} |
||||
} |
||||
Loading…
Reference in new issue