You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
351 lines
13 KiB
351 lines
13 KiB
//
|
|
// FortuneWheelView.swift
|
|
// PadelClub
|
|
//
|
|
// Created by Razmig Sarkissian on 22/04/2024.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
protocol SpinDrawable {
|
|
func segmentLabel(_ displayStyle: DisplayStyle) -> [String]
|
|
}
|
|
|
|
extension String: SpinDrawable {
|
|
func segmentLabel(_ displayStyle: DisplayStyle) -> [String] {
|
|
[self]
|
|
}
|
|
}
|
|
|
|
extension Match: SpinDrawable {
|
|
func segmentLabel(_ displayStyle: DisplayStyle) -> [String] {
|
|
let teams = teams()
|
|
if teams.count == 1 {
|
|
return teams.first!.segmentLabel(displayStyle)
|
|
} else {
|
|
return [roundTitle(), matchTitle(displayStyle)].compactMap { $0 }
|
|
}
|
|
}
|
|
}
|
|
|
|
extension TeamRegistration: SpinDrawable {
|
|
func segmentLabel(_ displayStyle: DisplayStyle) -> [String] {
|
|
var strings: [String] = []
|
|
let indexLabel = tournamentObject()?.labelIndexOf(team: self)
|
|
if let indexLabel {
|
|
strings.append(indexLabel)
|
|
}
|
|
strings.append(contentsOf: self.players().map { $0.playerLabel(displayStyle) })
|
|
return strings
|
|
}
|
|
}
|
|
|
|
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(_ displayStyle: DisplayStyle) -> [String] {
|
|
option.segmentLabel(displayStyle)
|
|
}
|
|
}
|
|
|
|
struct SpinDrawView: View {
|
|
@Environment(\.dismiss) private var dismiss
|
|
|
|
let drawees: [any SpinDrawable]
|
|
@State var segments: [any SpinDrawable]
|
|
var autoMode: Bool = false
|
|
let completion: ([DrawResult]) async -> Void // Completion closure
|
|
|
|
@State private var drawCount: Int = 0
|
|
@State private var draws: [DrawResult] = [DrawResult]()
|
|
@State private var drawOptions: [DrawOption] = [DrawOption]()
|
|
@State private var selectedIndex: Int?
|
|
@State private var disabled: Bool = false
|
|
|
|
var body: some View {
|
|
List {
|
|
if selectedIndex != nil {
|
|
Section {
|
|
_validationLabelView(drawee: drawCount, result: segments[draws.last!.drawIndex])
|
|
if autoMode == false || drawCount == drawees.count {
|
|
RowButtonView("Valider le tirage") {
|
|
await completion(draws)
|
|
dismiss()
|
|
}
|
|
} else {
|
|
Text("Prochain tirage en préparation")
|
|
}
|
|
}
|
|
} else if drawCount < drawees.count {
|
|
Section {
|
|
_segmentLabelView(segment: drawees[drawCount].segmentLabel(.wide), horizontalAlignment: .center)
|
|
}
|
|
|
|
Section {
|
|
ZStack {
|
|
FortuneWheelContainerView(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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.simultaneousGesture(
|
|
DragGesture().onChanged({ (value) in
|
|
disabled = true
|
|
}).onEnded({ (value) in
|
|
})
|
|
)
|
|
Rectangle()
|
|
.fill(.white.opacity(0.01))
|
|
.clipShape(Circle())
|
|
.allowsHitTesting(disabled)
|
|
}
|
|
.listRowBackground(Color.clear)
|
|
.listRowSeparator(.hidden)
|
|
} footer: {
|
|
HStack {
|
|
Spacer()
|
|
if autoMode {
|
|
Text("Mode automatique")
|
|
} else {
|
|
Text("Lancer la roue en glissant avec le doigt").multilineTextAlignment(.center)
|
|
}
|
|
Spacer()
|
|
}
|
|
}
|
|
} else {
|
|
Section {
|
|
Text("Tous les tirages sont terminés")
|
|
ForEach(draws) { drawResult in
|
|
_validationLabelView(drawee: drawResult.drawee, result: segments[drawResult.drawIndex])
|
|
}
|
|
}
|
|
|
|
RowButtonView("Valider les tirages") {
|
|
await completion(draws)
|
|
dismiss()
|
|
}
|
|
}
|
|
|
|
//todo
|
|
// Section {
|
|
// Text("XXX")
|
|
// Text("XXX")
|
|
// Text("XXX")
|
|
// } header: {
|
|
// Text("Comité du tournoi")
|
|
// }
|
|
}
|
|
.toolbar {
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
Button("Annuler", role: .cancel) {
|
|
dismiss()
|
|
}
|
|
.disabled(disabled || autoMode)
|
|
}
|
|
}
|
|
.navigationBarBackButtonHidden()
|
|
.navigationTitle("Tirage au sort")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbarBackground(.visible, for: .navigationBar)
|
|
.toolbar(.hidden, for: .tabBar)
|
|
.listStyle(.insetGrouped)
|
|
.scrollDisabled(true)
|
|
.interactiveDismissDisabled()
|
|
.onAppear {
|
|
for (index, segment) in segments.enumerated() {
|
|
drawOptions.append(DrawOption(initialIndex: index, option: segment))
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
private func _segmentLabelView(segment: [String], horizontalAlignment: HorizontalAlignment = .leading) -> some View {
|
|
VStack(alignment: horizontalAlignment, spacing: 0.0) {
|
|
ForEach(segment, id: \.self) { string in
|
|
Text(string).font(.title3)
|
|
.frame(maxWidth: .infinity)
|
|
.lineLimit(1)
|
|
}
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func _validationLabelView(drawee: Int, result: SpinDrawable) -> some View {
|
|
VStack(spacing: 0.0) {
|
|
let draw = drawees[drawee]
|
|
_segmentLabelView(segment: draw.segmentLabel(.wide), horizontalAlignment: .center)
|
|
if result as? TeamRegistration != nil {
|
|
Image(systemName: "flag.2.crossed.fill").font(.largeTitle).foregroundColor(.logoRed)
|
|
} else {
|
|
Image(systemName: "arrowshape.down.fill").font(.largeTitle).foregroundColor(.logoRed)
|
|
}
|
|
_segmentLabelView(segment: result.segmentLabel(.wide), horizontalAlignment: .center)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct FortuneWheelContainerView: 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) {
|
|
rotation = 0
|
|
rollWheel()
|
|
}
|
|
}
|
|
}
|
|
.gesture(
|
|
DragGesture().onChanged({ (value) in
|
|
if value.translation.width < 0 {
|
|
rotation = Double(-value.translation.width)
|
|
}
|
|
}).onEnded({ (value) in
|
|
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))
|
|
|
|
VStack(alignment: .trailing, spacing: 0.0) {
|
|
let strings = segments[index].segmentLabel(.short)
|
|
ForEach(strings, id: \.self) { string in
|
|
Text(string).bold()
|
|
}
|
|
}
|
|
.padding(.trailing, 40)
|
|
.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
|
|
}
|
|
}
|
|
|