From 9fa1bf9f6497793213bdfbfe7b383d2755c454fe Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Thu, 24 Oct 2024 17:05:03 +0900 Subject: [PATCH] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EB=B0=A9=20-=20?= =?UTF-8?q?=ED=95=98=ED=8A=B8=20=EC=95=A0=EB=8B=88=EB=A9=94=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ic_heart_pink.imageset/Contents.json | 21 ++++ .../ic_heart_pink.imageset/ic_heart_pink.png | Bin 0 -> 10727 bytes .../Sources/Live/Room/LiveRoomViewModel.swift | 100 ++++++++++++++++++ .../V2/Component/View/LiveRoomHeartView.swift | 33 ++++++ SodaLive/Sources/Live/Room/V2/Heart.swift | 18 ++++ .../Sources/Live/Room/V2/LiveRoomViewV2.swift | 44 ++++++-- 6 files changed, 207 insertions(+), 9 deletions(-) create mode 100644 SodaLive/Resources/Assets.xcassets/ic_heart_pink.imageset/Contents.json create mode 100644 SodaLive/Resources/Assets.xcassets/ic_heart_pink.imageset/ic_heart_pink.png create mode 100644 SodaLive/Sources/Live/Room/V2/Component/View/LiveRoomHeartView.swift create mode 100644 SodaLive/Sources/Live/Room/V2/Heart.swift diff --git a/SodaLive/Resources/Assets.xcassets/ic_heart_pink.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_heart_pink.imageset/Contents.json new file mode 100644 index 0000000..b296832 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_heart_pink.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_heart_pink.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_heart_pink.imageset/ic_heart_pink.png b/SodaLive/Resources/Assets.xcassets/ic_heart_pink.imageset/ic_heart_pink.png new file mode 100644 index 0000000000000000000000000000000000000000..5c165adf3dda922647a99b81d9d03b52bd4145f4 GIT binary patch literal 10727 zcmb_?2{hF2_y5?JgzRg^mL<#Bv&+86*wpu71XS?@(#G9Gw9i--<1^@sD4fM6agl|{E zV?#+!_*|lSX#)Td2_dw!%)GSpw9rU2#=-{*gCBQAyTHMEno^32#{dA~6c_{o))y1* zgfc)NoxMjzs4)THxVS_x~u+LONSQH9@^Z;(AJv5@$m^%z~5D?+)aR#`s5^+odFL1* za`d0MZ)Bbax&yzEobOF1b(0C7duW?tqBO6#;4|9ECnnz#at~cj6M<{2Bb6u zA2rmd!G;}&o4W<;#z*jj3c$=N%c-AmOo=p!#%ydFuV~FT73sNh5?$QsT`14cXYsc* zzr1%bJba2yv!@_E92!nbLb7cch0o+Ai9Dl5(soy|t564S24l-eWM`vXB(mi)#A=sP zR1-pmhua#snV0}X2~ng393UbGkPuo#0Aj)i?an7am++(#F?^E~5#$^IvRw=SFha!n zqy3Ud_$RK^E|-dcn%W3Wq?!k6^IAerRPZ5Qaqf?2!E7FdP~K z#KLhq3dO!@-TlTu1AGvHa3laA4U(2p0x2qiWPu=%k_<>mP8RUJR|1Ci21)`o{je^6 zSOEAN2WX6Ql>h)J&J!MjPEwGxjMRD3@17Awr4a4OiHJ!6SH5GUK%gtc-#r7kULpC> zwkh}?X@atyzh5mP7$vI+P%Ky#Cv;Ky=-Xid015t`hbXX6-5&s;&qr9=;%rTfl?c{K zKzEk8M4-3Nt}2y4B|_61j)MXNy^$!aQlKi|E<%aW-Vsak0e2xdFI7HU6EmO|8UqK) zOMoOmd}`D{AW#M4>ZSzN*8N5&{8Hufz~Ow9Bqaj^0we-tB+wXlNy45|lmtmjN=u6q z5aQS%6b>3Fj>7WqDfzBP8;*rx5I#5r8U@_Z3w1&J;Z*tfb_V+Kw`Zp-?8i7hei-EL zD6TL`I1=tnm~5=1l!TPzpLhgse-KgFZ_yycMKTcTLm+|vNWvl9{&DLU)=tlV(%^xJ z|Df&k{3p%F1C2vtJ<$JQ=#TC_1-rAb7aSE=m=X*FhZ5HGPf|kP?j%{jeSejH??58d z{Df_!9Etl?`j1UJ3rYzIMY*f;1&X`E z-JpI*9G}|%u-T#BKFa^S;s0OG{c6>&bANVY$3YCj-2;dH)r;MD|ElHN4DChq&%XU~ zeEg60{mY6S({KnJ68`_gGJh$MjFh;Gi#XN`j)QspkGB18#*S%NKNnBJ;rj>UG@
    ZdyzXk$b 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 oQzS`4XT2thU=VR() @@ -1803,6 +1816,93 @@ final class LiveRoomViewModel: NSObject, ObservableObject { } } } + + func likeHeart() { + if isAvailableLikeHeart { + if !isLoadingLikeHeart { + isLoadingLikeHeart = true + addHeart() + + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [unowned self] in + self.isLoadingLikeHeart = false + } + } + } else { + isShowNoticeLikeHeart = true + } + } + + private func addHeart() { + let heart = Heart( + id: UUID(), + offsetX: 0, + offsetY: 0, + opacity: 1, + speed: CGFloat.random(in: 1...3), + scale: 0.5, + direction: Bool.random() ? "left" : "right" + ) + hearts.append(heart) + + if hearts.count == 1 { + startHeartTimer() + } + } + + private func updateHearts() { + for i in (0..= 22 { + hearts[i].direction = "left" + } + } + + // 화면을 벗어나거나 완전히 사라진 하트는 삭제 + if hearts[i].scale >= 1 || hearts[i].opacity <= 0 || hearts[i].offsetY < -450 { + hearts.remove(at: i) + + if hearts.isEmpty { + stopHeartTimer() + } + } + } + + // 최대 하트 개수 제한 + if hearts.count > 100 { + hearts.removeFirst() + } + } + + func startHeartTimer() { + if heartTimer == nil { + let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.main) + timer.schedule(deadline: .now(), repeating: 0.033) // 30 FPS + timer.setEventHandler { [unowned self] in + DispatchQueue.main.async { + self.updateHearts() + } + } + timer.resume() + self.heartTimer = timer + } + } + + func stopHeartTimer() { + heartTimer?.cancel() + heartTimer = nil + } } extension LiveRoomViewModel: AgoraRtcEngineDelegate { diff --git a/SodaLive/Sources/Live/Room/V2/Component/View/LiveRoomHeartView.swift b/SodaLive/Sources/Live/Room/V2/Component/View/LiveRoomHeartView.swift new file mode 100644 index 0000000..854b018 --- /dev/null +++ b/SodaLive/Sources/Live/Room/V2/Component/View/LiveRoomHeartView.swift @@ -0,0 +1,33 @@ +// +// LiveRoomHeartView.swift +// SodaLive +// +// Created by klaus on 10/24/24. +// + +import SwiftUI + +struct LiveRoomHeartView: View { + let heart: Heart + + var body: some View { + Image("ic_heart_pink") + .resizable() + .frame(width: 24 * heart.scale, height: 24 * heart.scale) // 크기 조절 + .shadow(radius: 10) + } +} + +#Preview { + LiveRoomHeartView( + heart: Heart( + id: UUID(), + offsetX: 0, + offsetY: 0, + opacity: 1.0, + speed: 1.0, + scale: 0.5, + direction: "left" + ) + ) +} diff --git a/SodaLive/Sources/Live/Room/V2/Heart.swift b/SodaLive/Sources/Live/Room/V2/Heart.swift new file mode 100644 index 0000000..f2e6c32 --- /dev/null +++ b/SodaLive/Sources/Live/Room/V2/Heart.swift @@ -0,0 +1,18 @@ +// +// Heart.swift +// SodaLive +// +// Created by klaus on 10/24/24. +// + +import Foundation + +struct Heart: Identifiable { + let id: UUID + var offsetX: CGFloat // X축 위치 + var offsetY: CGFloat // Y축 위치 + var opacity: Double // 투명도 + var speed: CGFloat // 이동 속도 + var scale: CGFloat // 크기 + var direction: String +} diff --git a/SodaLive/Sources/Live/Room/V2/LiveRoomViewV2.swift b/SodaLive/Sources/Live/Room/V2/LiveRoomViewV2.swift index a809a1c..d4d49ee 100644 --- a/SodaLive/Sources/Live/Room/V2/LiveRoomViewV2.swift +++ b/SodaLive/Sources/Live/Room/V2/LiveRoomViewV2.swift @@ -178,12 +178,20 @@ struct LiveRoomViewV2: View { VStack(alignment: .trailing, spacing: 0) { Spacer() - LiveRoomRightBottomButton( - imageName: viewModel.isSpeakerMute ? "ic_speaker_off" : "ic_speaker_on", - onClick: { viewModel.toggleSpeakerMute() } - ) - .padding(.bottom, 40) - .padding(.trailing, 13.3) + ZStack(alignment: .bottom) { + LiveRoomRightBottomButton( + imageName: viewModel.isSpeakerMute ? "ic_speaker_off" : "ic_speaker_on", + onClick: { viewModel.toggleSpeakerMute() } + ) + .padding(.bottom, 40) + .padding(.trailing, 13.3) + + ForEach(viewModel.hearts) { heart in + LiveRoomHeartView(heart: heart) + .offset(x: heart.offsetX, y: heart.offsetY) + .opacity(heart.opacity) + } + } HStack(alignment: .bottom, spacing: 0) { LiveRoomInputChatView { @@ -201,11 +209,18 @@ struct LiveRoomViewV2: View { imageName: "ic_roulette_settings", onClick: { viewModel.isShowRouletteSettings = true } ) - } else if liveRoomInfo.creatorId != UserDefaults.int(forKey: .userId) && viewModel.isActiveRoulette { + } else { LiveRoomRightBottomButton( - imageName: "ic_roulette", - onClick: { viewModel.showRoulette() } + imageName: "ic_heart_pink", + onClick: { viewModel.likeHeart() } ) + + if viewModel.isActiveRoulette { + LiveRoomRightBottomButton( + imageName: "ic_roulette", + onClick: { viewModel.showRoulette() } + ) + } } LiveRoomRightBottomButton( @@ -414,6 +429,17 @@ struct LiveRoomViewV2: View { ) } + if viewModel.isShowNoticeLikeHeart { + SodaDialog( + title: "안내", + desc: "'좋아해요'는 유료 후원입니다.\n" + + "클릭시 1캔이 소진됩니다.", + confirmButtonTitle: "확인" + ) { + viewModel.isShowNoticeLikeHeart = false + } + } + if viewModel.isShowQuitPopup { SodaDialog( title: "라이브 나가기",