라이브 방
- 하트 애니메이션 추가
This commit is contained in:
		
							
								
								
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/ic_heart_pink.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/ic_heart_pink.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -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 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/ic_heart_pink.imageset/ic_heart_pink.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/ic_heart_pink.imageset/ic_heart_pink.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 10 KiB | 
| @@ -37,6 +37,7 @@ final class LiveRoomViewModel: NSObject, ObservableObject { | ||||
|      | ||||
|     @Published var coverImageUrl: String? | ||||
|      | ||||
|     @Published var isLoadingLikeHeart = false | ||||
|     @Published var isLoading = false | ||||
|     @Published var errorMessage = "" | ||||
|     @Published var reportMessage = "" | ||||
| @@ -140,6 +141,13 @@ final class LiveRoomViewModel: NSObject, ObservableObject { | ||||
|     @Published var isShowUesrReportView = false | ||||
|     @Published var isShowProfileReportConfirm = false | ||||
|     @Published var isShowNoChattingConfirm = false | ||||
|     @Published var isShowNoticeLikeHeart = false { | ||||
|         didSet { | ||||
|             if !isShowNoticeLikeHeart { | ||||
|                 isAvailableLikeHeart = true | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     @Published var reportUserId = 0 | ||||
|     @Published var reportUserNickname = "" | ||||
| @@ -190,11 +198,16 @@ final class LiveRoomViewModel: NSObject, ObservableObject { | ||||
|     } | ||||
|     @Published var selectedMenu: SelectedMenu? = nil | ||||
|      | ||||
|     @Published var hearts: [Heart] = [] | ||||
|      | ||||
|     var signatureImageUrls = [String]() | ||||
|     var signatureList = [LiveRoomDonationResponse]() | ||||
|     var isShowSignatureImage = false | ||||
|      | ||||
|     var timer: DispatchSourceTimer? | ||||
|     var heartTimer: DispatchSourceTimer? | ||||
|      | ||||
|     var isAvailableLikeHeart = false | ||||
|      | ||||
|     private var blockedMemberIdList = Set<Int>() | ||||
|      | ||||
| @@ -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..<hearts.count).reversed() { | ||||
|             hearts[i].offsetY -= hearts[i].speed * 2 // Y축으로 이동 | ||||
|             hearts[i].opacity -= hearts[i].speed * 0.004444444444 // 투명도 감소 | ||||
|             hearts[i].scale += 0.0067 | ||||
|              | ||||
|             if hearts[i].direction == "left" { | ||||
|                 hearts[i].offsetX -= 0.8 | ||||
|                  | ||||
|                 if hearts[i].offsetX <= -22 { | ||||
|                     hearts[i].direction = "right" | ||||
|                 } | ||||
|             } else { | ||||
|                 hearts[i].offsetX += 0.8 | ||||
|                  | ||||
|                 if hearts[i].offsetX >= 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 { | ||||
|   | ||||
| @@ -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" | ||||
|         ) | ||||
|     ) | ||||
| } | ||||
							
								
								
									
										18
									
								
								SodaLive/Sources/Live/Room/V2/Heart.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								SodaLive/Sources/Live/Room/V2/Heart.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| } | ||||
| @@ -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: "라이브 나가기", | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Yu Sung
					Yu Sung