라이브 방

- 하트 애니메이션 추가
This commit is contained in:
Yu Sung 2024-10-24 17:05:03 +09:00
parent c7314cc1d4
commit 9fa1bf9f64
6 changed files with 207 additions and 9 deletions

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

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

View File

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

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

View File

@ -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: "라이브 나가기",