Skip to content

Commit 4276703

Browse files
authored
feat(example): Universal App (#86)
1 parent 4d3c1db commit 4276703

File tree

43 files changed

+487
-1730
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+487
-1730
lines changed

Example/macOS/SwiftAudio/SwiftAudio.xcodeproj/project.pbxproj renamed to Example/SwiftAudio.xcodeproj/project.pbxproj

Lines changed: 111 additions & 105 deletions
Large diffs are not rendered by default.

Example/SwiftAudio/PlayerView.swift

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
//
2+
// PlayerView.swift
3+
// SwiftAudio
4+
//
5+
// Created by Brandon Sneed on 3/30/24.
6+
//
7+
8+
import SwiftUI
9+
import SwiftAudioEx
10+
11+
struct PlayerView: View {
12+
@ObservedObject var viewModel: ViewModel
13+
@State private var showingQueue = false
14+
15+
let controller = AudioController.shared
16+
17+
init(viewModel: PlayerView.ViewModel = ViewModel()) {
18+
self.viewModel = viewModel
19+
}
20+
21+
var body: some View {
22+
VStack(spacing: 0) {
23+
HStack(alignment: .center) {
24+
Spacer()
25+
Button(action: { showingQueue.toggle() }, label: {
26+
Text("Queue")
27+
.fontWeight(.bold)
28+
})
29+
}
30+
31+
if let image = viewModel.artwork {
32+
#if os(macOS)
33+
Image(nsImage: image)
34+
.resizable()
35+
.scaledToFit()
36+
.frame(width: 240, height: 240)
37+
.padding(.top, 30)
38+
#elseif os(iOS)
39+
Image(uiImage: image)
40+
.resizable()
41+
.scaledToFit()
42+
.frame(width: 240, height: 240)
43+
.padding(.top, 30)
44+
#endif
45+
} else {
46+
AsyncImage(url: nil)
47+
.frame(width: 240, height: 240)
48+
.padding(.top, 30)
49+
}
50+
51+
VStack(spacing: 4) {
52+
Text(viewModel.title)
53+
.fontWeight(.semibold)
54+
.font(.system(size: 18))
55+
Text(viewModel.artist)
56+
.fontWeight(.thin)
57+
}
58+
.padding(.top, 30)
59+
60+
if viewModel.maxTime > 0 {
61+
VStack {
62+
Slider(value: $viewModel.position, in: 0...viewModel.maxTime) { editing in
63+
viewModel.isScrubbing = editing
64+
print("scrubbing = \(viewModel.isScrubbing)")
65+
if viewModel.isScrubbing == false {
66+
controller.player.seek(to: viewModel.position)
67+
}
68+
}
69+
HStack {
70+
Text(viewModel.elapsedTime)
71+
.font(.system(size: 14))
72+
Spacer()
73+
Text(viewModel.remainingTime)
74+
.font(.system(size: 14))
75+
}
76+
}
77+
.padding(.top, 25)
78+
} else {
79+
Text("Live Stream")
80+
.padding(.top, 35)
81+
}
82+
83+
HStack {
84+
Button(action: controller.player.previous, label: {
85+
Text("Prev")
86+
.font(.system(size: 14))
87+
})
88+
.frame(maxWidth: .infinity)
89+
90+
Button(action: {
91+
if viewModel.playing {
92+
controller.player.pause()
93+
} else {
94+
controller.player.play()
95+
}
96+
}, label: {
97+
Text(!viewModel.playWhenReady || viewModel.playbackState == .failed ? "Play" : "Pause")
98+
.font(.system(size: 18))
99+
.fontWeight(.semibold)
100+
})
101+
102+
.frame(maxWidth: .infinity)
103+
Button(action: controller.player.next, label: {
104+
Text("Next")
105+
.font(.system(size: 14))
106+
})
107+
.frame(maxWidth: .infinity)
108+
}
109+
.padding(.top, 80)
110+
111+
VStack {
112+
if viewModel.playbackState == .failed {
113+
Text("Playback failed.")
114+
.font(.system(size: 14))
115+
.foregroundStyle(.red)
116+
.padding(.top, 20)
117+
} else if (viewModel.playbackState == .loading || viewModel.playbackState == .buffering) && viewModel.playWhenReady {
118+
ProgressView()
119+
.progressViewStyle(.circular)
120+
.controlSize(.small)
121+
.padding(.top, 20)
122+
}
123+
}
124+
125+
Spacer()
126+
}
127+
.sheet(isPresented: $showingQueue) {
128+
QueueView()
129+
#if os(macOS)
130+
.frame(width: 300, height: 400)
131+
#endif
132+
}
133+
.padding(.horizontal, 16)
134+
.padding(.top)
135+
}
136+
}
137+
138+
#Preview("Standard") {
139+
let viewModel = PlayerView.ViewModel()
140+
viewModel.title = "Longing"
141+
viewModel.artist = "David Chavez"
142+
143+
return PlayerView(viewModel: viewModel)
144+
}
145+
146+
#Preview("Error") {
147+
let viewModel = PlayerView.ViewModel()
148+
viewModel.title = "Longing"
149+
viewModel.artist = "David Chavez"
150+
viewModel.playbackState = .failed
151+
152+
return PlayerView(viewModel: viewModel)
153+
}
154+
155+
#Preview("Buffering") {
156+
let viewModel = PlayerView.ViewModel()
157+
viewModel.title = "Longing"
158+
viewModel.artist = "David Chavez"
159+
viewModel.playbackState = .buffering
160+
viewModel.playWhenReady = true
161+
162+
return PlayerView(viewModel: viewModel)
163+
}
164+
165+
#Preview("Live Stream") {
166+
let viewModel = PlayerView.ViewModel()
167+
viewModel.title = "Longing"
168+
viewModel.artist = "David Chavez"
169+
viewModel.maxTime = 0
170+
171+
return PlayerView(viewModel: viewModel)
172+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
//
2+
// PlayerViewModel.swift
3+
// SwiftAudio
4+
//
5+
// Created by David Chavez on 4/12/24.
6+
//
7+
8+
import SwiftAudioEx
9+
10+
#if os(macOS)
11+
import AppKit
12+
public typealias NativeImage = NSImage
13+
#elseif os(iOS)
14+
import UIKit
15+
public typealias NativeImage = UIImage
16+
#endif
17+
18+
extension PlayerView {
19+
final class ViewModel: ObservableObject {
20+
// MARK: - Observables
21+
22+
@Published var playing: Bool = false
23+
@Published var position: Double = 0
24+
@Published var artwork: NativeImage? = nil
25+
@Published var title: String = ""
26+
@Published var artist: String = ""
27+
@Published var maxTime: TimeInterval = 100
28+
@Published var isScrubbing: Bool = false
29+
@Published var elapsedTime: String = "00:00"
30+
@Published var remainingTime: String = "00:00"
31+
32+
@Published var playWhenReady: Bool = false
33+
@Published var playbackState: AudioPlayerState = .idle
34+
35+
// MARK: - Properties
36+
37+
let controller = AudioController.shared
38+
39+
// MARK: - Initializer
40+
41+
init() {
42+
controller.player.event.playWhenReadyChange.addListener(self, handlePlayWhenReadyChange)
43+
controller.player.event.stateChange.addListener(self, handleAudioPlayerStateChange)
44+
controller.player.event.playbackEnd.addListener(self, handleAudioPlayerPlaybackEnd(data:))
45+
controller.player.event.secondElapse.addListener(self, handleAudioPlayerSecondElapsed)
46+
controller.player.event.seek.addListener(self, handleAudioPlayerDidSeek)
47+
controller.player.event.updateDuration.addListener(self, handleAudioPlayerUpdateDuration)
48+
controller.player.event.didRecreateAVPlayer.addListener(self, handleAVPlayerRecreated)
49+
}
50+
51+
// MARK: - Updates
52+
53+
private func render() {
54+
DispatchQueue.main.async { [weak self] in
55+
guard let self else { return }
56+
playing = (controller.player.playerState == .playing)
57+
playbackState = controller.player.playerState
58+
playWhenReady = controller.player.playWhenReady
59+
position = controller.player.currentTime
60+
maxTime = controller.player.duration
61+
artist = controller.player.currentItem?.getArtist() ?? ""
62+
title = controller.player.currentItem?.getTitle() ?? ""
63+
elapsedTime = controller.player.currentTime.secondsToString()
64+
remainingTime = (controller.player.duration - controller.player.currentTime).secondsToString()
65+
if let item = controller.player.currentItem as? DefaultAudioItem {
66+
artwork = item.artwork
67+
} else {
68+
artwork = nil
69+
}
70+
}
71+
}
72+
73+
private func renderTimes() {
74+
DispatchQueue.main.async { [weak self] in
75+
guard let self else { return }
76+
position = controller.player.currentTime
77+
maxTime = controller.player.duration
78+
elapsedTime = controller.player.currentTime.secondsToString()
79+
remainingTime = (controller.player.duration - controller.player.currentTime).secondsToString()
80+
print(elapsedTime)
81+
}
82+
}
83+
84+
// MARK: - AudioPlayer Event Handlers
85+
86+
func handleAudioPlayerStateChange(data: AudioPlayer.StateChangeEventData) {
87+
print("state=\(data)")
88+
render()
89+
}
90+
91+
func handlePlayWhenReadyChange(data: AudioPlayer.PlayWhenReadyChangeData) {
92+
print("playWhenReady=\(data)")
93+
render()
94+
}
95+
96+
func handleAudioPlayerPlaybackEnd(data: AudioPlayer.PlaybackEndEventData) {
97+
print("playEndReason=\(data)")
98+
}
99+
100+
func handleAudioPlayerSecondElapsed(data: AudioPlayer.SecondElapseEventData) {
101+
if !isScrubbing {
102+
renderTimes()
103+
}
104+
}
105+
106+
func handleAudioPlayerDidSeek(data: AudioPlayer.SeekEventData) {
107+
// .. don't need this
108+
}
109+
110+
func handleAudioPlayerUpdateDuration(data: AudioPlayer.UpdateDurationEventData) {
111+
if !isScrubbing {
112+
renderTimes()
113+
}
114+
}
115+
116+
func handleAVPlayerRecreated() {
117+
// .. don't need this
118+
}
119+
}
120+
}

Example/SwiftAudio/QueueView.swift

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//
2+
// QueueView.swift
3+
// SwiftAudio
4+
//
5+
// Created by David Chavez on 4/12/24.
6+
//
7+
8+
import SwiftUI
9+
import SwiftAudioEx
10+
11+
struct QueueView: View {
12+
let controller = AudioController.shared
13+
@Environment(\.dismiss) var dismiss
14+
15+
var body: some View {
16+
NavigationStack {
17+
VStack {
18+
List {
19+
if controller.player.currentItem != nil {
20+
Section(header: Text("Playing Now")) {
21+
QueueItemView(
22+
title: controller.player.currentItem?.getTitle() ?? "",
23+
artist: controller.player.currentItem?.getArtist() ?? ""
24+
)
25+
}
26+
}
27+
Section(header: Text("Up Next")) {
28+
ForEach(controller.player.nextItems as! [DefaultAudioItem]) { item in
29+
QueueItemView(
30+
title: item.getTitle() ?? "",
31+
artist: item.getArtist() ?? ""
32+
)
33+
}
34+
}
35+
}
36+
}
37+
.navigationTitle("Queue")
38+
.toolbar {
39+
Button("Close") {
40+
dismiss()
41+
}
42+
}
43+
}
44+
}
45+
}
46+
47+
struct QueueItemView: View {
48+
let title: String
49+
let artist: String
50+
51+
var body: some View {
52+
VStack(alignment: .leading) {
53+
Text(title)
54+
.fontWeight(.semibold)
55+
Text(artist)
56+
.fontWeight(.light)
57+
}
58+
}
59+
}
60+
61+
62+
#Preview {
63+
QueueView()
64+
}
65+
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//
2+
// SwiftAudioApp.swift
3+
// SwiftAudio
4+
//
5+
// Created by Brandon Sneed on 3/30/24.
6+
//
7+
8+
import SwiftUI
9+
10+
@main
11+
struct SwiftAudioApp: App {
12+
var body: some Scene {
13+
WindowGroup {
14+
PlayerView()
15+
}
16+
}
17+
}

0 commit comments

Comments
 (0)