룰렛 뷰 추가
This commit is contained in:
		
							
								
								
									
										40
									
								
								SodaLive/Sources/FortuneWheel/FortuneWheel.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								SodaLive/Sources/FortuneWheel/FortuneWheel.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| // | ||||
| //  FortuneWheel.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/12/07. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| struct FortuneWheel: View { | ||||
|  | ||||
|     private let model: FortuneWheelModel | ||||
|     @StateObject private var viewModel: FortuneWheelViewModel | ||||
|      | ||||
|     public init(model: FortuneWheelModel) { | ||||
|         self.model = model | ||||
|         _viewModel = StateObject(wrappedValue: FortuneWheelViewModel(model: model)) | ||||
|     } | ||||
|      | ||||
|     public var body: some View { | ||||
|         ZStack(alignment: .top) { | ||||
|             ZStack(alignment: .center) { | ||||
|                 SpinWheelView(data: (0..<model.titles.count).map { _ in Double(100 / model.titles.count) }, | ||||
|                               labels: model.titles, colors: model.colors) | ||||
|                     .frame(width: model.size, height: model.size) | ||||
|                     .overlay( | ||||
|                         RoundedRectangle(cornerRadius: model.size / 2) | ||||
|                             .stroke(lineWidth: model.strokeWidth) | ||||
|                             .foregroundColor(model.strokeColor) | ||||
|                     ) | ||||
|                     .rotationEffect(.degrees(viewModel.degree)) | ||||
|                     .onAppear { | ||||
|                         viewModel.spinWheel() | ||||
|                     } | ||||
|                 SpinWheelBolt() | ||||
|             } | ||||
|             SpinWheelPointer(pointerColor: model.pointerColor).offset(x: 0, y: -25) | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										51
									
								
								SodaLive/Sources/FortuneWheel/FortuneWheelModel.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								SodaLive/Sources/FortuneWheel/FortuneWheelModel.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| // | ||||
| //  FortuneWheelModel.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/12/07. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| struct FortuneWheelModel { | ||||
|      | ||||
|     let titles: [String] | ||||
|     let size: CGFloat | ||||
|     let onSpinEnd: ((Int) -> ())? | ||||
|     let colors: [Color] | ||||
|     let pointerColor: Color | ||||
|     let strokeWidth: CGFloat | ||||
|     let strokeColor: Color | ||||
|     let animDuration: Double | ||||
|     let animation: Animation | ||||
|     let getWheelItemIndex: (() -> (Int))? | ||||
|      | ||||
|     public init( | ||||
|         titles: [String], size: CGFloat, onSpinEnd: ((Int) -> ())?, | ||||
|         colors: [Color]? = nil, | ||||
|         pointerColor: Color = .red, | ||||
|         strokeWidth: CGFloat = 5, | ||||
|         strokeColor: Color = .white, | ||||
|         animDuration: Double = Double(2), | ||||
|         animation: Animation? = nil, | ||||
|         getWheelItemIndex: (() -> (Int))? = nil | ||||
|     ) { | ||||
|         self.titles = titles | ||||
|         self.size = size | ||||
|         self.onSpinEnd = onSpinEnd | ||||
|         self.colors = colors ?? [ | ||||
|             Color(hex: "#F5D55A"), | ||||
|             Color(hex: "#E4813B"), | ||||
|             Color(hex: "#E6AAC1"), | ||||
|             Color(hex: "#8FCEEA"), | ||||
|             Color(hex: "#CD5880"), | ||||
|             Color(hex: "#C2C85E") | ||||
|         ] | ||||
|         self.pointerColor = pointerColor | ||||
|         self.strokeWidth = strokeWidth | ||||
|         self.strokeColor = strokeColor | ||||
|         self.animDuration = animDuration | ||||
|         self.animation = animation ?? Animation.timingCurve(0.51, 0.97, 0.56, 0.99, duration: animDuration) | ||||
|         self.getWheelItemIndex = getWheelItemIndex | ||||
|     } | ||||
| } | ||||
							
								
								
									
										59
									
								
								SodaLive/Sources/FortuneWheel/FortuneWheelViewModel.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								SodaLive/Sources/FortuneWheel/FortuneWheelViewModel.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| // | ||||
| //  FortuneWheelViewModel.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/12/07. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| class FortuneWheelViewModel: ObservableObject { | ||||
|  | ||||
|     private var pendingRequestWorkItem: DispatchWorkItem? | ||||
|     @Published var degree = 0.0 | ||||
|  | ||||
|     private let model: FortuneWheelModel | ||||
|  | ||||
|     init(model: FortuneWheelModel) { | ||||
|         self.model = model | ||||
|     } | ||||
|  | ||||
|     private func getWheelStopDegree() -> Double { | ||||
|         var index = -1; | ||||
|         if let method = model.getWheelItemIndex { index = method() } | ||||
|         if index < 0 || index >= model.titles.count { index = Int.random(in: 0..<model.titles.count) } | ||||
|         index = model.titles.count - index - 1; | ||||
|         /* | ||||
|          itemRange - Each items degree range (For 4, each will have 360 / 4 = 90 degrees) | ||||
|          indexDegree - No. of 90 degrees to reach i item | ||||
|          freeRange - Flexible degree in the item, so the pointer doesn't always point start of the item | ||||
|          freeSpins - No. of spins before it goes to selected item index | ||||
|          finalDegree - Final exact degree to spin and stop in the index | ||||
|          */ | ||||
|         let itemRange = 360 / model.titles.count; | ||||
|         let indexDegree = itemRange * index; | ||||
|         let freeRange = Int.random(in: 0...itemRange); | ||||
|         let freeSpins = (2...20).map({ return $0 * 360 }).randomElement()! | ||||
|         let finalDegree = freeSpins + indexDegree + freeRange; | ||||
|         return Double(finalDegree); | ||||
|     } | ||||
|      | ||||
|     func spinWheel() { | ||||
|         withAnimation(model.animation) { | ||||
|             self.degree = Double(360 * Int(self.degree / 360)) + getWheelStopDegree(); | ||||
|         } | ||||
|         // Cancel the currently pending item | ||||
|         pendingRequestWorkItem?.cancel() | ||||
|         // Wrap our request in a work item | ||||
|         let requestWorkItem = DispatchWorkItem { [weak self] in | ||||
|             guard let self = self else { return } | ||||
|             let count = self.model.titles.count | ||||
|             let distance = self.degree.truncatingRemainder(dividingBy: 360) | ||||
|             let pointer = floor(distance / (360 / Double(count))) | ||||
|             if let onSpinEnd = self.model.onSpinEnd { onSpinEnd(count - Int(pointer) - 1) } | ||||
|         } | ||||
|         // Save the new work item and execute it after duration | ||||
|         pendingRequestWorkItem = requestWorkItem | ||||
|         DispatchQueue.main.asyncAfter(deadline: .now() + model.animDuration + 1, execute: requestWorkItem) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										38
									
								
								SodaLive/Sources/FortuneWheel/SpinWheelCell.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								SodaLive/Sources/FortuneWheel/SpinWheelCell.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| // | ||||
| //  SpinWheelCell.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/12/07. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| struct SpinWheelCell: Shape { | ||||
|      | ||||
|     let startAngle: Double, endAngle: Double | ||||
|      | ||||
|     func path(in rect: CGRect) -> Path { | ||||
|         var path = Path() | ||||
|         let radius = min(rect.width, rect.height) / 2 | ||||
|         let alpha = CGFloat(startAngle) | ||||
|         let center = CGPoint( | ||||
|             x: rect.midX, | ||||
|             y: rect.midY | ||||
|         ) | ||||
|         path.move(to: center) | ||||
|         path.addLine( | ||||
|             to: CGPoint( | ||||
|                 x: center.x + cos(alpha) * radius, | ||||
|                 y: center.y + sin(alpha) * radius | ||||
|             ) | ||||
|         ) | ||||
|         path.addArc( | ||||
|             center: center, radius: radius, | ||||
|             startAngle: Angle(radians: startAngle), | ||||
|             endAngle: Angle(radians: endAngle), | ||||
|             clockwise: false | ||||
|         ) | ||||
|         path.closeSubpath() | ||||
|         return path | ||||
|     } | ||||
| } | ||||
							
								
								
									
										96
									
								
								SodaLive/Sources/FortuneWheel/SpinWheelView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								SodaLive/Sources/FortuneWheel/SpinWheelView.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,96 @@ | ||||
| // | ||||
| //  SpinWheelView.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/12/07. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| struct Triangle: Shape { | ||||
|     public 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.addLine(to: CGPoint(x: rect.midX, y: rect.minY)) | ||||
|         path.addCurve(to: CGPoint(x: rect.midX, y: rect.minY), control1: CGPoint(x: rect.maxX, y: rect.minY), control2: CGPoint(x: rect.midX, y: rect.minY)) | ||||
|         return path | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct SpinWheelPointer: View { | ||||
|     var pointerColor: Color | ||||
|     var body: some View { | ||||
|         Triangle().frame(width: 50, height: 50) | ||||
|             .foregroundColor(pointerColor).cornerRadius(24) | ||||
|             .rotationEffect(.init(degrees: 180)) | ||||
|             .shadow(color: Color(hex: "212121").opacity(0.5), radius: 5, x: 0.0, y: 1.0) | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct SpinWheelBolt: View { | ||||
|     var body: some View { | ||||
|         ZStack { | ||||
|             Circle().frame(width: 28, height: 28) | ||||
|                 .foregroundColor(Color(hex: "F4C25B")) | ||||
|             Circle().frame(width: 18, height: 18) | ||||
|                 .foregroundColor(Color(hex: "FFD25A")) | ||||
|                 .shadow(color: Color(hex: "404040").opacity(0.35), radius: 3, x: 0.0, y: 1.0) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct SpinWheelView: View { | ||||
|      | ||||
|     var data: [Double], labels: [String] | ||||
|      | ||||
|     private let colors: [Color] | ||||
|     private let sliceOffset: Double = -.pi / 2 | ||||
|     @available(macOS 10.15, *) | ||||
|      | ||||
|     init(data: [Double], labels: [String], colors: [Color]) { | ||||
|         self.data = data | ||||
|         self.labels = labels | ||||
|         self.colors = colors | ||||
|     } | ||||
|     @available(macOS 10.15.0, *) | ||||
|      | ||||
|     var body: some View { | ||||
|         GeometryReader { geo in | ||||
|             ZStack(alignment: .center) { | ||||
|                 ForEach(0..<data.count, id: \.self) { index in | ||||
|                     SpinWheelCell(startAngle: startAngle(for: index), endAngle: endAngle(for: index)) | ||||
|                         .fill(colors[index % colors.count]) | ||||
|                     Text(labels[index]).font(.custom(Font.medium.rawValue, size: 13)).foregroundColor(Color.black) | ||||
|                         .offset(viewOffset(for: index, in: geo.size)).zIndex(1) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private func startAngle(for index: Int) -> Double { | ||||
|         switch index { | ||||
|         case 0: return sliceOffset | ||||
|         default: | ||||
|             let ratio: Double = data[..<index].reduce(0.0, +) / data.reduce(0.0, +) | ||||
|             return sliceOffset + 2 * .pi * ratio | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private func endAngle(for index: Int) -> Double { | ||||
|         switch index { | ||||
|         case data.count - 1: return sliceOffset + 2 * .pi | ||||
|         default: | ||||
|             let ratio: Double = data[..<(index + 1)].reduce(0.0, +) / data.reduce(0.0, +) | ||||
|             return sliceOffset + 2 * .pi * ratio | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private func viewOffset(for index: Int, in size: CGSize) -> CGSize { | ||||
|         let radius = min(size.width, size.height) / 3 | ||||
|         let dataRatio = (2 * data[..<index].reduce(0, +) + data[index]) / (2 * data.reduce(0, +)) | ||||
|         let angle = CGFloat(sliceOffset + 2 * .pi * dataRatio) | ||||
|         return CGSize(width: radius * cos(angle), height: radius * sin(angle)) | ||||
|     } | ||||
| } | ||||
| @@ -390,7 +390,6 @@ struct LiveRoomView: View { | ||||
|                             } | ||||
|                         } | ||||
|                         .frame(width: screenSize().width) | ||||
|                         .animation(nil) | ||||
|                     } | ||||
|                      | ||||
|                     HStack(spacing: 0) { | ||||
| @@ -704,7 +703,9 @@ struct LiveRoomView: View { | ||||
|             } | ||||
|              | ||||
|             if viewModel.isShowRoulette { | ||||
|                  | ||||
|                 RouletteViewDialog(isShowing: $viewModel.isShowRoulette, options: viewModel.rouletteItems, selectedOption: viewModel.rouletteSelectedItem) { | ||||
|                     viewModel.sendRouletteDonation() | ||||
|                 } | ||||
|             } | ||||
|              | ||||
|             if viewModel.isLoading && viewModel.liveRoomInfo == nil { | ||||
| @@ -713,9 +714,6 @@ struct LiveRoomView: View { | ||||
|         } | ||||
|         .ignoresSafeArea(.keyboard) | ||||
|         .edgesIgnoringSafeArea(keyboardHandler.keyboardHeight > 0 ? .bottom : .init()) | ||||
|         .transaction { transaction in | ||||
|             transaction.animation = nil | ||||
|         } | ||||
|         .sheet( | ||||
|             isPresented: $viewModel.isShowShareView, | ||||
|             onDismiss: { viewModel.shareMessage = "" }, | ||||
|   | ||||
| @@ -141,7 +141,7 @@ final class LiveRoomViewModel: NSObject, ObservableObject { | ||||
|     @Published var roulettePreview: RoulettePreview? = nil | ||||
|      | ||||
|     @Published var isShowRoulette = false | ||||
|     @Published var rouletteItems = [RouletteItem]() | ||||
|     @Published var rouletteItems = [String]() | ||||
|     @Published var rouletteSelectedItem = "" | ||||
|     var rouletteCan = 0 | ||||
|      | ||||
| @@ -1432,10 +1432,9 @@ final class LiveRoomViewModel: NSObject, ObservableObject { | ||||
|     } | ||||
|      | ||||
|     func sendRouletteDonation() { | ||||
|         let rawMessage = rouletteSelectedItem | ||||
|         let rouletteRawMessage = LiveRoomChatRawMessage( | ||||
|             type: .ROULETTE_DONATION, | ||||
|             message: rawMessage, | ||||
|             message: rouletteSelectedItem, | ||||
|             can: rouletteCan, | ||||
|             donationMessage: "" | ||||
|         ) | ||||
| @@ -1449,7 +1448,7 @@ final class LiveRoomViewModel: NSObject, ObservableObject { | ||||
|                         LiveRoomRouletteDonationChat( | ||||
|                             profileUrl: profileUrl, | ||||
|                             nickname: nickname, | ||||
|                             rouletteResult: rawMessage | ||||
|                             rouletteResult: rouletteSelectedItem | ||||
|                         ) | ||||
|                     ) | ||||
|                      | ||||
| @@ -1495,10 +1494,10 @@ final class LiveRoomViewModel: NSObject, ObservableObject { | ||||
|          | ||||
|         isLoading = false | ||||
|         self.rouletteItems.removeAll() | ||||
|         self.rouletteItems.append(contentsOf: items) | ||||
|         self.rouletteItems.append(contentsOf: items.map { $0.title }) | ||||
|         self.rouletteSelectedItem = rouletteItems[Int(arc4random_uniform(UInt32(rouletteItems.count)))] | ||||
|         self.rouletteCan = can | ||||
|         sendRouletteDonation() | ||||
|         self.isShowRoulette = true | ||||
|     } | ||||
|      | ||||
|     private func refundRouletteDonation() { | ||||
|   | ||||
| @@ -0,0 +1,37 @@ | ||||
| // | ||||
| //  RouletteViewDialog.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/12/07. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| struct RouletteViewDialog: View { | ||||
|     @Binding var isShowing: Bool | ||||
|      | ||||
|     let options: [String] | ||||
|     let selectedOption: String | ||||
|     let complete: () -> Void | ||||
|      | ||||
|     var body: some View { | ||||
|         let model = FortuneWheelModel( | ||||
|             titles: options, | ||||
|             size: 320, | ||||
|             onSpinEnd: onSpinEnd, | ||||
|             getWheelItemIndex: getWheelItemIndex | ||||
|         ) | ||||
|         ZStack { | ||||
|             FortuneWheel(model: model) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private func onSpinEnd(index: Int) { | ||||
|         complete() | ||||
|         isShowing = false | ||||
|     } | ||||
|      | ||||
|     private func getWheelItemIndex() -> Int { | ||||
|         return options.firstIndex(of: selectedOption) ?? 0 | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Yu Sung
					Yu Sung