오디션 - 오디오 재생기능 추가

This commit is contained in:
Yu Sung 2025-01-07 23:58:50 +09:00
parent 7e13689763
commit dd6c2fe469
3 changed files with 174 additions and 1 deletions

View File

@ -13,6 +13,8 @@ struct AuditionApplicantItemView: View {
let item: GetAuditionRoleApplicantItem
let onClickVote: (Int) -> Void
@StateObject var soundManager = AuditionSoundManager.shared
var body: some View {
VStack(spacing: 5.3) {
HStack(spacing: 13.3) {
@ -26,8 +28,12 @@ struct AuditionApplicantItemView: View {
.frame(width: 40, height: 40)
.clipShape(Circle())
Image("ic_audition_play")
Image(soundManager.applicantId == item.applicantId && soundManager.isPlaying ? "ic_audition_pause" : "ic_audition_play")
.onTapGesture {
soundManager.playOrPause(
applicantId: item.applicantId,
url: item.voiceUrl
)
}
}
@ -38,6 +44,17 @@ struct AuditionApplicantItemView: View {
.foregroundColor(Color.white)
Spacer()
if soundManager.applicantId == item.applicantId {
Text("\(secondsToMinutesSeconds(Int(soundManager.currentTime)))/\(secondsToMinutesSeconds(Int(soundManager.duration)))")
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color.gray77)
}
}
if soundManager.applicantId == item.applicantId {
ProgressView(value: soundManager.currentTime, total: soundManager.duration)
.progressViewStyle(LinearProgressViewStyle(tint: Color.button))
}
}
@ -60,6 +77,13 @@ struct AuditionApplicantItemView: View {
}
.padding(.horizontal, 13.3)
}
private func secondsToMinutesSeconds(_ seconds: Int) -> String {
let minute = String(format: "%02d", seconds / 60)
let second = String(format: "%02d", seconds % 60)
return "\(minute):\(second)"
}
}
#Preview {

View File

@ -0,0 +1,126 @@
//
// AuditionSoundManager.swift
// SodaLive
//
// Created by klaus on 1/7/25.
//
import Foundation
import AVKit
import Combine
class AuditionSoundManager: NSObject, ObservableObject {
static let shared = AuditionSoundManager()
@Published private (set) var isPlaying = false
@Published var isLoading = false
@Published var errorMessage = ""
@Published var isShowPopup = false
@Published var currentTime: Double = 0.0
@Published private (set) var duration: Double = 0.0
private var player: AVAudioPlayer!
private var timer: Timer?
@Published private (set) var applicantId = -1
func resetPlayer() {
stop()
}
func playOrPause(applicantId: Int, url: String) {
if applicantId <= 0 {
stop()
}
if self.applicantId != applicantId {
stop()
setupPlayer(with: url)
self.applicantId = applicantId
} else {
if !isPlaying {
player?.play()
isPlaying = player?.isPlaying ?? false
startTimer()
} else {
player?.pause()
isPlaying = player?.isPlaying ?? false
stopTimer()
}
}
}
private func stop() {
stopTimer()
isPlaying = false
isLoading = false
player?.stop()
player?.currentTime = 0
currentTime = 0.0
duration = 0.0
applicantId = -1
}
private func setupPlayer(with url: String) {
guard let url = URL(string: url) else {
self.errorMessage = "오류가 발생했습니다. 다시 시도해 주세요."
self.isShowPopup = true
return
}
isLoading = true
URLSession.shared.dataTask(with: url) { [unowned self] data, response, error in
guard let audioData = data else {
self.isLoading = false
return
}
do {
self.player = try AVAudioPlayer(data: audioData)
DispatchQueue.main.async {
self.player?.volume = 1
self.player?.delegate = self
self.player?.prepareToPlay()
self.player?.play()
self.duration = self.player?.duration ?? 0.0
self.isPlaying = self.player.isPlaying
self.isLoading = false
self.startTimer()
}
} catch {
DispatchQueue.main.async {
self.errorMessage = "오류가 발생했습니다. 다시 시도해 주세요."
self.isShowPopup = true
self.isLoading = false
}
}
}.resume()
}
private func startTimer() {
stopTimer() //
timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] _ in
self?.updateCurrentTime()
}
}
//
private func stopTimer() {
timer?.invalidate()
timer = nil
}
private func updateCurrentTime() {
guard let audioPlayer = player else { return }
currentTime = audioPlayer.currentTime
}
}
extension AuditionSoundManager: AVAudioPlayerDelegate {
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
stop()
}
}

View File

@ -14,6 +14,7 @@ struct AuditionRoleDetailView: View {
@StateObject var viewModel = AuditionRoleDetailViewModel()
@StateObject var keyboardHandler = KeyboardHandler()
@StateObject var soundManager = AuditionSoundManager.shared
@State private var isShowApplyMethodView = false
@State private var isShowSelectAudioView = false
@ -123,10 +124,28 @@ struct AuditionRoleDetailView: View {
Spacer()
}
}
.popup(isPresented: $soundManager.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
HStack {
Spacer()
Text(soundManager.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12))
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
.padding(.bottom, 66.7)
Spacer()
}
}
.onAppear {
viewModel.onFailure = { AppState.shared.back() }
viewModel.auditionRoleId = roleId
}
.onDisappear {
soundManager.resetPlayer()
}
if let roleDetail = viewModel.auditionRoleDetail {
Text(roleDetail.isAlreadyApplicant ? "오디션 재지원" : "오디션 지원")
@ -208,6 +227,10 @@ struct AuditionRoleDetailView: View {
viewModel.isShowNotifyVote = false
}
}
if soundManager.isLoading {
LoadingView()
}
}
}