룰렛 뷰 추가

This commit is contained in:
Yu Sung 2023-12-07 08:43:18 +09:00
parent 0af16ac000
commit d52f0d1176
8 changed files with 329 additions and 11 deletions

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

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

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

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

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

View File

@ -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 = "" },

View File

@ -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() {

View File

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