From 1b7558a4f1f8a742ca1ff293aa533f2ddf3f799e Mon Sep 17 00:00:00 2001 From: dimitris-c Date: Thu, 25 Apr 2024 23:54:18 +0300 Subject: [PATCH] refactor --- .../AudioPlayer.xcodeproj/project.pbxproj | 26 +- .../AudioPlayer/AudioPlayerModel.swift | 150 ----------- .../Content/AudioPlayer/AudioPlayerView.swift | 235 ++++++++++++++++-- .../Content/AudioPlayer/EqualizerView.swift | 1 - .../Content/AudioQueue/AudioQueueModel.swift | 13 + .../Content/AudioQueue/AudioQueueView.swift | 15 +- .../AudioPlayerService.swift | 8 + .../{ => Dependencies}/EqualizerService.swift | 0 .../{ => Helpers}/DisplayLink.swift | 0 .../AudioPlayer/Helpers/Notifier.swift | 29 +++ .../AudioPlayer/Navigation/DetailView.swift | 2 +- 11 files changed, 303 insertions(+), 176 deletions(-) rename AudioPlayer/AudioPlayer/{ => Dependencies}/AudioPlayerService.swift (91%) rename AudioPlayer/AudioPlayer/{ => Dependencies}/EqualizerService.swift (100%) rename AudioPlayer/AudioPlayer/{ => Helpers}/DisplayLink.swift (100%) create mode 100644 AudioPlayer/AudioPlayer/Helpers/Notifier.swift diff --git a/AudioPlayer/AudioPlayer.xcodeproj/project.pbxproj b/AudioPlayer/AudioPlayer.xcodeproj/project.pbxproj index 106cb2d..e05cd36 100644 --- a/AudioPlayer/AudioPlayer.xcodeproj/project.pbxproj +++ b/AudioPlayer/AudioPlayer.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 9816A8B22BC8330C00AD1299 /* bensound-jazzyfrenchy.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 9816A8AE2BC832DB00AD1299 /* bensound-jazzyfrenchy.m4a */; }; 9816A8B32BC8330C00AD1299 /* hipjazz.wav in Resources */ = {isa = PBXBuildFile; fileRef = 9816A8AF2BC832DC00AD1299 /* hipjazz.wav */; }; 9816A8BB2BC87BC200AD1299 /* AudioPlayerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9816A8BA2BC87BC200AD1299 /* AudioPlayerService.swift */; }; + 984DE9552BDAE59C004B427A /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 984DE9542BDAE59C004B427A /* Notifier.swift */; }; 98BFB41A2BC97AF800E812C0 /* DisplayLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98BFB4192BC97AF800E812C0 /* DisplayLink.swift */; }; 98BFB41D2BCD7BB800E812C0 /* EqualizerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98BFB41C2BCD7BB800E812C0 /* EqualizerView.swift */; }; 98BFB41F2BCD814000E812C0 /* EqualizerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98BFB41E2BCD814000E812C0 /* EqualizerService.swift */; }; @@ -61,6 +62,7 @@ 9816A8AE2BC832DB00AD1299 /* bensound-jazzyfrenchy.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = "bensound-jazzyfrenchy.m4a"; sourceTree = ""; }; 9816A8AF2BC832DC00AD1299 /* hipjazz.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = hipjazz.wav; sourceTree = ""; }; 9816A8BA2BC87BC200AD1299 /* AudioPlayerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerService.swift; sourceTree = ""; }; + 984DE9542BDAE59C004B427A /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = ""; }; 98BFB4192BC97AF800E812C0 /* DisplayLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayLink.swift; sourceTree = ""; }; 98BFB41B2BCAAD8A00E812C0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 98BFB41C2BCD7BB800E812C0 /* EqualizerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EqualizerView.swift; sourceTree = ""; }; @@ -103,10 +105,9 @@ 9806E8162BC5D12500757370 /* AudioPlayer */ = { isa = PBXGroup; children = ( + 984DE9532BDAE57F004B427A /* Dependencies */, 98BFB41B2BCAAD8A00E812C0 /* Info.plist */, - 98BFB41E2BCD814000E812C0 /* EqualizerService.swift */, - 9816A8BA2BC87BC200AD1299 /* AudioPlayerService.swift */, - 98BFB4192BC97AF800E812C0 /* DisplayLink.swift */, + 984DE9522BDAE571004B427A /* Helpers */, 9816A8A82BC7F4DE00AD1299 /* Common */, 9806E8282BC68F7300757370 /* Content */, 9806E8272BC68F6600757370 /* Navigation */, @@ -173,6 +174,24 @@ path = Resources; sourceTree = ""; }; + 984DE9522BDAE571004B427A /* Helpers */ = { + isa = PBXGroup; + children = ( + 98BFB4192BC97AF800E812C0 /* DisplayLink.swift */, + 984DE9542BDAE59C004B427A /* Notifier.swift */, + ); + path = Helpers; + sourceTree = ""; + }; + 984DE9532BDAE57F004B427A /* Dependencies */ = { + isa = PBXGroup; + children = ( + 98BFB41E2BCD814000E812C0 /* EqualizerService.swift */, + 9816A8BA2BC87BC200AD1299 /* AudioPlayerService.swift */, + ); + path = Dependencies; + sourceTree = ""; + }; 98E3921B2BD845D200B586E9 /* AudioQueue */ = { isa = PBXGroup; children = ( @@ -275,6 +294,7 @@ 98E392202BD8461D00B586E9 /* AudioQueueModel.swift in Sources */, 9816A8AC2BC820DF00AD1299 /* AudioContent.swift in Sources */, 9806E8262BC5D2A900757370 /* Sidebar.swift in Sources */, + 984DE9552BDAE59C004B427A /* Notifier.swift in Sources */, 9806E82A2BC68F8700757370 /* AudioPlayerView.swift in Sources */, 9806E8312BC6927D00757370 /* AudioPlayerModel.swift in Sources */, 98BFB41F2BCD814000E812C0 /* EqualizerService.swift in Sources */, diff --git a/AudioPlayer/AudioPlayer/Content/AudioPlayer/AudioPlayerModel.swift b/AudioPlayer/AudioPlayer/Content/AudioPlayer/AudioPlayerModel.swift index 5c47d90..a50b327 100644 --- a/AudioPlayer/AudioPlayer/Content/AudioPlayer/AudioPlayerModel.swift +++ b/AudioPlayer/AudioPlayer/Content/AudioPlayer/AudioPlayerModel.swift @@ -23,43 +23,18 @@ enum ScrubState: Equatable { public class AudioPlayerModel { @ObservationIgnored private(set) var audioPlayerService: AudioPlayerService - @ObservationIgnored - private var displayLink: DisplayLink? var audioTracks: [AudioPlaylist] = [] - var isLiveAudioStreaming: Bool { - totalTime == 0 - } - - var liveAudioMetadata: String? - - var isPlaying: Bool = false - var isMuted: Bool = false - - var playbackRate: Double = 0.0 - - var currentTime: Double = 0 - var totalTime: Double? - - var scrubState: ScrubState = .idle - - var formattedCurrentTime: String? - var formattedTotalTime: String? - var currentTrack: AudioTrack? init(audioTracksProvider: () -> [AudioPlaylist] = audioTracksProvider, audioPlayerService: AudioPlayerService) { self.audioPlayerService = audioPlayerService self.audioTracks = audioTracksProvider() - - self.audioPlayerService.delegate = self } deinit { audioPlayerService.stop() - displayLink?.deactivate() - displayLink = nil } func addNewAudioTrack(url: URL) { @@ -76,136 +51,11 @@ public class AudioPlayerModel { } } - func mute() { - isMuted.toggle() - audioPlayerService.toggleMute() - } - - func playPause() { - if audioPlayerService.state == .playing { - audioPlayerService.pause() - } else { - audioPlayerService.resume() - } - } - - func update(rate: Float) { - let rate = round(rate / 0.2) * 0.2 - audioPlayerService.update(rate: rate) - } - - - func stop() { - isPlaying = false - audioPlayerService.stop() - currentTrack?.status = .idle - currentTrack = nil - } - func play(_ track: AudioTrack) { if track != currentTrack { - currentTrack?.status = .idle - audioPlayerService.play(url: track.url) currentTrack = track } } - - func onTick() { - let duration = audioPlayerService.duration - let progress = audioPlayerService.progress - if duration > 0 { - let elapsed = Int(progress) - let remaining = Int(duration - progress) - totalTime = duration - switch scrubState { - case .idle: - currentTime = progress - case .started: - break - case .ended(let seekTime): - currentTime = seekTime - if audioPlayerService.duration > 0 { - audioPlayerService.seek(at: seekTime) - } - scrubState = .idle - } - formattedCurrentTime = timeFrom(seconds: Int(elapsed)) - formattedTotalTime = timeFrom(seconds: remaining) - } else { - let elapsed = Int(progress) - formattedCurrentTime = timeFrom(seconds: Int(elapsed)) - if formattedTotalTime != nil { - formattedTotalTime = nil - } - } - } - - func resetLabels() { - currentTime = 0 - totalTime = 0 - formattedCurrentTime = nil - formattedTotalTime = nil - } - - private func timeFrom(seconds: Int) -> String { - let correctSeconds = seconds % 60 - let minutes = (seconds / 60) % 60 - let hours = seconds / 3600 - - if hours > 0 { - return String(format: "%02d:%02d:%02d", hours, minutes, correctSeconds) - } - return String(format: "%02d:%02d", minutes, correctSeconds) - } -} - -extension AudioPlayerModel: AudioPlayerServiceDelegate { - func didStartPlaying(id: AudioEntryId) { - self.displayLink = DisplayLink(onTick: { [weak self] _ in - self?.onTick() - }) - displayLink?.activate() - } - - func didStopPlaying(id: AudioEntryId, reason: AudioPlayerStopReason) { - resetLabels() - liveAudioMetadata = nil - playbackRate = 1.0 - displayLink?.deactivate() - } - - func statusChanged(status: AudioStreaming.AudioPlayerState) { - isPlaying = status == .playing - displayLink?.isPaused = !isPlaying - switch status { - case .bufferring: - currentTrack?.status = .buffering - case .error: - currentTrack?.status = .error - currentTrack = nil - case .playing: - currentTrack?.status = .playing - case .paused: - currentTrack?.status = .paused - case .stopped: - currentTrack?.status = .idle - default: - currentTrack?.status = .idle - } - } - - func errorOccurred(error: AudioStreaming.AudioPlayerError) { - - } - - func metadataReceived(metadata: [String : String]) { - guard !metadata.isEmpty else { return } - if let title = metadata["StreamTitle"] { - liveAudioMetadata = title.isEmpty ? "-" : title - } else { - liveAudioMetadata = nil - } - } } func audioTracksProvider() -> [AudioPlaylist] { diff --git a/AudioPlayer/AudioPlayer/Content/AudioPlayer/AudioPlayerView.swift b/AudioPlayer/AudioPlayer/Content/AudioPlayer/AudioPlayerView.swift index 2b692dc..33ccab2 100644 --- a/AudioPlayer/AudioPlayer/Content/AudioPlayer/AudioPlayerView.swift +++ b/AudioPlayer/AudioPlayer/Content/AudioPlayer/AudioPlayerView.swift @@ -42,7 +42,7 @@ struct AudioPlayerView: View { } } .safeAreaInset(edge: .bottom) { - AudioPlayerControls(viewModel: model) + AudioPlayerControls(appModel: appModel, currentTrack: $model.currentTrack) .background( .ultraThinMaterial.shadow( ShadowStyle.drop(color: .black.opacity(0.1), radius: 8, x: 0, y: -10) @@ -82,26 +82,35 @@ struct AudioPlayerView: View { } struct AudioPlayerControls: View { - @Bindable var viewModel: AudioPlayerModel + @State var model: Model + @Binding var currentTrack: AudioTrack? + + init(appModel: AppModel, currentTrack: Binding) { + self._model = State(wrappedValue: Model(audioPlayerService: appModel.audioPlayerService)) + self._currentTrack = currentTrack + } var body: some View { VStack(alignment: .leading) { HStack { - Button(action: { viewModel.playPause() }) { - Image(systemName: viewModel.isPlaying ? "pause.fill" : "play.fill") + Button(action: { model.playPause() }) { + Image(systemName: model.isPlaying ? "pause.fill" : "play.fill") .font(.title) .imageScale(.small) } .contentTransition(.symbolEffect(.replace)) - Button(action: { viewModel.stop() }) { + Button(action: { + model.stop() + currentTrack = nil + }) { Image(systemName: "stop.fill") .font(.title) .imageScale(.small) } .padding(.leading, 8) Spacer() - Button(action: { viewModel.mute() }) { - Image(systemName: viewModel.isMuted ? "speaker.slash.fill" : "speaker.fill") + Button(action: { model.mute() }) { + Image(systemName: model.isMuted ? "speaker.slash.fill" : "speaker.fill") .font(.title) .imageScale(.small) } @@ -110,7 +119,7 @@ struct AudioPlayerControls: View { } .tint(.mint) .padding(16) - if let audioMetadata = viewModel.liveAudioMetadata, viewModel.isLiveAudioStreaming { + if let audioMetadata = model.liveAudioMetadata, model.isLiveAudioStreaming { Text("Now Playing: \(audioMetadata)") .font(.caption) .foregroundStyle(.black) @@ -119,21 +128,21 @@ struct AudioPlayerControls: View { Divider() VStack { Slider( - value: $viewModel.currentTime, - in: 0...(viewModel.totalTime ?? 1.0), + value: $model.currentTime, + in: 0...(model.totalTime ?? 1.0), onEditingChanged: { scrubStarted in if scrubStarted { - viewModel.scrubState = .started + model.scrubState = .started } else { - viewModel.scrubState = .ended(viewModel.currentTime) + model.scrubState = .ended(model.currentTime) } } ) - .disabled(viewModel.totalTime == nil) + .disabled(model.totalTime == nil) HStack { - Text(viewModel.formattedCurrentTime ?? "--:--") + Text(model.formattedCurrentTime ?? "--:--") Spacer() - Text(viewModel.formattedTotalTime ?? "") + Text(model.formattedTotalTime ?? "") } .foregroundStyle(.black) .font(.caption) @@ -143,21 +152,209 @@ struct AudioPlayerControls: View { .padding(.horizontal, 16) Divider() VStack(alignment: .leading) { - Text("Playback Rate: \(String(format: "%0.1f", viewModel.playbackRate))") + Text("Playback Rate: \(String(format: "%0.1f", model.playbackRate))") .font(.subheadline) .fontWeight(.medium) .foregroundStyle(.black) - Slider(value: $viewModel.playbackRate, in: 1.0...4.0, step: 0.2) - .onChange(of: viewModel.playbackRate) { _, new in - viewModel.update(rate: Float(new)) + Slider(value: $model.playbackRate, in: 1.0...4.0, step: 0.2) + .onChange(of: model.playbackRate) { _, new in + model.update(rate: Float(new)) } } .padding(.bottom, 8) .padding(.horizontal, 16) } + .onChange(of: currentTrack) { oldValue, newValue in + if let track = newValue { + model.play(track) + } + } } } +extension AudioPlayerControls { + @Observable + final class Model { + @ObservationIgnored + private(set) var audioPlayerService: AudioPlayerService + @ObservationIgnored + private var displayLink: DisplayLink? + + var isLiveAudioStreaming: Bool { + totalTime == 0 + } + + var liveAudioMetadata: String? + + var isPlaying: Bool = false + var isMuted: Bool = false + + var playbackRate: Double = 0.0 + + var currentTime: Double = 0 + var totalTime: Double? + + var scrubState: ScrubState = .idle + + var formattedCurrentTime: String? + var formattedTotalTime: String? + + var currentTrack: AudioTrack? + + init(audioPlayerService: AudioPlayerService) { + self.audioPlayerService = audioPlayerService + + registerObservations() + } + + deinit { + displayLink?.deactivate() + displayLink = nil + } + + func registerObservations() { + Task { @MainActor in + for await status in await audioPlayerService.statusChangedNotifier.values() { + isPlaying = status == .playing + displayLink?.isPaused = !isPlaying + switch status { + case .bufferring: + currentTrack?.status = .buffering + case .error: + currentTrack?.status = .error + currentTrack = nil + case .playing: + currentTrack?.status = .playing + case .paused: + currentTrack?.status = .paused + case .stopped: + currentTrack?.status = .idle + default: + currentTrack?.status = .idle + } + } + } + + Task { @MainActor in + for await metadata in await audioPlayerService.metadataReceivedNotifier.values() { + guard !metadata.isEmpty else { break } + if let title = metadata["StreamTitle"] { + liveAudioMetadata = title.isEmpty ? "-" : title + } else { + liveAudioMetadata = nil + } + } + } + + Task { @MainActor in + for await startStopped in await audioPlayerService.playingStartedStopped.values() { + if startStopped.started { + self.didStartPlaying() + } else { + self.didStopPlaying() + } + } + } + } + + func mute() { + isMuted.toggle() + audioPlayerService.toggleMute() + } + + func playPause() { + if audioPlayerService.state == .playing { + audioPlayerService.pause() + } else { + audioPlayerService.resume() + } + } + + func update(rate: Float) { + let rate = round(rate / 0.2) * 0.2 + audioPlayerService.update(rate: rate) + } + + func stop() { + isPlaying = false + audioPlayerService.stop() + currentTrack?.status = .idle + currentTrack = nil + } + + func play(_ track: AudioTrack) { + if track != currentTrack { + currentTrack?.status = .idle + audioPlayerService.play(url: track.url) + currentTrack = track + } + } + + func onTick() { + let duration = audioPlayerService.duration + let progress = audioPlayerService.progress + if duration > 0 { + let elapsed = Int(progress) + let remaining = Int(duration - progress) + totalTime = duration + switch scrubState { + case .idle: + currentTime = progress + case .started: + break + case .ended(let seekTime): + currentTime = seekTime + if audioPlayerService.duration > 0 { + audioPlayerService.seek(at: seekTime) + } + scrubState = .idle + } + formattedCurrentTime = timeFrom(seconds: Int(elapsed)) + formattedTotalTime = timeFrom(seconds: remaining) + } else { + let elapsed = Int(progress) + formattedCurrentTime = timeFrom(seconds: Int(elapsed)) + if formattedTotalTime != nil { + formattedTotalTime = nil + } + } + } + + func resetLabels() { + currentTime = 0 + totalTime = 0 + formattedCurrentTime = nil + formattedTotalTime = nil + } + + private func timeFrom(seconds: Int) -> String { + let correctSeconds = seconds % 60 + let minutes = (seconds / 60) % 60 + let hours = seconds / 3600 + + if hours > 0 { + return String(format: "%02d:%02d:%02d", hours, minutes, correctSeconds) + } + return String(format: "%02d:%02d", minutes, correctSeconds) + } + + private func didStartPlaying() { + self.displayLink = DisplayLink(onTick: { [weak self] _ in + self?.onTick() + }) + displayLink?.activate() + } + + private func didStopPlaying() { + resetLabels() + liveAudioMetadata = nil + playbackRate = 1.0 + displayLink?.deactivate() + } + } + +} + #Preview { AudioPlayerView(appModel: AppModel()) } diff --git a/AudioPlayer/AudioPlayer/Content/AudioPlayer/EqualizerView.swift b/AudioPlayer/AudioPlayer/Content/AudioPlayer/EqualizerView.swift index abd1364..1e0bf36 100644 --- a/AudioPlayer/AudioPlayer/Content/AudioPlayer/EqualizerView.swift +++ b/AudioPlayer/AudioPlayer/Content/AudioPlayer/EqualizerView.swift @@ -127,7 +127,6 @@ struct EQSliderView: View { } GeometryReader { innerGeo in ZStack { - LineShape(values: eqModel.shouldReset ? resetPoints : dragPointYLocations.map { Double($0) }) .stroke(Color.mint, lineWidth: 2) .animation(.easeInOut(duration: 0.2), value: eqModel.shouldReset) diff --git a/AudioPlayer/AudioPlayer/Content/AudioQueue/AudioQueueModel.swift b/AudioPlayer/AudioPlayer/Content/AudioQueue/AudioQueueModel.swift index b596065..72cf1c7 100644 --- a/AudioPlayer/AudioPlayer/Content/AudioQueue/AudioQueueModel.swift +++ b/AudioPlayer/AudioPlayer/Content/AudioQueue/AudioQueueModel.swift @@ -4,3 +4,16 @@ // import Foundation + +@Observable +class AudioQueueModel { + @ObservationIgnored + private(set) var audioPlayerService: AudioPlayerService + @ObservationIgnored + private var displayLink: DisplayLink? + + + init(audioPlayerService: AudioPlayerService) { + self.audioPlayerService = audioPlayerService + } +} diff --git a/AudioPlayer/AudioPlayer/Content/AudioQueue/AudioQueueView.swift b/AudioPlayer/AudioPlayer/Content/AudioQueue/AudioQueueView.swift index 3b574d9..2ce0fe1 100644 --- a/AudioPlayer/AudioPlayer/Content/AudioQueue/AudioQueueView.swift +++ b/AudioPlayer/AudioPlayer/Content/AudioQueue/AudioQueueView.swift @@ -6,11 +6,22 @@ import SwiftUI struct AudioQueueView: View { + @Environment(AppModel.self) var appModel + + @State var model: AudioPlayerModel + + @State var eqSheetIsShown: Bool = false + @State var addNewAudioIsShown: Bool = false + + init(appModel: AppModel) { + self._model = State(wrappedValue: AudioPlayerModel(audioPlayerService: appModel.audioPlayerService)) + } + var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + return Text("") } } #Preview { - AudioQueueView() + AudioQueueView(appModel: AppModel()) } diff --git a/AudioPlayer/AudioPlayer/AudioPlayerService.swift b/AudioPlayer/AudioPlayer/Dependencies/AudioPlayerService.swift similarity index 91% rename from AudioPlayer/AudioPlayer/AudioPlayerService.swift rename to AudioPlayer/AudioPlayer/Dependencies/AudioPlayerService.swift index bc92495..0db2e44 100644 --- a/AudioPlayer/AudioPlayer/AudioPlayerService.swift +++ b/AudioPlayer/AudioPlayer/Dependencies/AudioPlayerService.swift @@ -40,6 +40,10 @@ final class AudioPlayerService { player.state } + var statusChangedNotifier = Notifier() + var metadataReceivedNotifier = Notifier<[String: String]>() + var playingStartedStopped = Notifier<(started: Bool, AudioEntryId, AudioPlayerStopReason?)>() + init(audioPlayer: AudioPlayer) { player = audioPlayer player.delegate = self @@ -150,12 +154,14 @@ extension AudioPlayerService: AudioPlayerDelegate { func audioPlayerDidStartPlaying(player _: AudioPlayer, with id: AudioEntryId) { print("audioPlayerDidStartPlaying entryId: \(id)") delegate?.didStartPlaying(id: id) + Task { await playingStartedStopped.send((true, id, nil)) } } func audioPlayerDidFinishBuffering(player _: AudioPlayer, with _: AudioEntryId) {} func audioPlayerStateChanged(player _: AudioPlayer, with newState: AudioPlayerState, previous _: AudioPlayerState) { print("audioPlayerDidStartPlaying newState: \(newState)") + Task { await statusChangedNotifier.send(newState) } delegate?.statusChanged(status: newState) } @@ -166,6 +172,7 @@ extension AudioPlayerService: AudioPlayerDelegate { duration _: Double) { print("audioPlayerDidFinishPlaying entryId: \(id), reason: \(reason)") + Task { await playingStartedStopped.send((false, id, reason)) } delegate?.didStopPlaying(id: id, reason: reason) } @@ -176,6 +183,7 @@ extension AudioPlayerService: AudioPlayerDelegate { func audioPlayerDidCancel(player _: AudioPlayer, queuedItems _: [AudioEntryId]) {} func audioPlayerDidReadMetadata(player _: AudioPlayer, metadata: [String: String]) { + Task { await metadataReceivedNotifier.send(metadata) } delegate?.metadataReceived(metadata: metadata) } } diff --git a/AudioPlayer/AudioPlayer/EqualizerService.swift b/AudioPlayer/AudioPlayer/Dependencies/EqualizerService.swift similarity index 100% rename from AudioPlayer/AudioPlayer/EqualizerService.swift rename to AudioPlayer/AudioPlayer/Dependencies/EqualizerService.swift diff --git a/AudioPlayer/AudioPlayer/DisplayLink.swift b/AudioPlayer/AudioPlayer/Helpers/DisplayLink.swift similarity index 100% rename from AudioPlayer/AudioPlayer/DisplayLink.swift rename to AudioPlayer/AudioPlayer/Helpers/DisplayLink.swift diff --git a/AudioPlayer/AudioPlayer/Helpers/Notifier.swift b/AudioPlayer/AudioPlayer/Helpers/Notifier.swift new file mode 100644 index 0000000..ec03c40 --- /dev/null +++ b/AudioPlayer/AudioPlayer/Helpers/Notifier.swift @@ -0,0 +1,29 @@ +// +// Created by Dimitris Chatzieleftheriou on 25/04/2024. +// + +import Foundation + +actor Notifier { + private var continuations: [UUID: AsyncStream.Continuation] = [:] + + func values(bufferingPolicy limit: AsyncStream.Continuation.BufferingPolicy = .bufferingNewest(1)) -> AsyncStream { + AsyncStream(bufferingPolicy: limit) { continuation in + let id = UUID() + continuations[id] = continuation + continuation.onTermination = { _ in + Task { await self.cancel(id) } + } + } + } + + func send(_ value: Output) { + for continuation in continuations.values { + continuation.yield(value) + } + } + + private func cancel(_ id: UUID) { + continuations[id] = nil + } +} diff --git a/AudioPlayer/AudioPlayer/Navigation/DetailView.swift b/AudioPlayer/AudioPlayer/Navigation/DetailView.swift index 3ed8475..50b52cc 100644 --- a/AudioPlayer/AudioPlayer/Navigation/DetailView.swift +++ b/AudioPlayer/AudioPlayer/Navigation/DetailView.swift @@ -16,7 +16,7 @@ struct DetailView: View { case .audioPlayer: AudioPlayerView(appModel: appModel) case .audioQueue: - Text("Audio Queue") + AudioQueueView(appModel: appModel) } } }