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.
 
 
PadelClub/PadelClub/Views/Components/FortuneWheelView.swift

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