commit
9e2882391b
@ -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