Skip to content

Commit

Permalink
feat(example): Universal App (#86)
Browse files Browse the repository at this point in the history
  • Loading branch information
dcvz authored Apr 12, 2024
1 parent 4d3c1db commit 4276703
Show file tree
Hide file tree
Showing 43 changed files with 487 additions and 1,730 deletions.

Large diffs are not rendered by default.

File renamed without changes.
File renamed without changes.
172 changes: 172 additions & 0 deletions Example/SwiftAudio/PlayerView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
//
// PlayerView.swift
// SwiftAudio
//
// Created by Brandon Sneed on 3/30/24.
//

import SwiftUI
import SwiftAudioEx

struct PlayerView: View {
@ObservedObject var viewModel: ViewModel
@State private var showingQueue = false

let controller = AudioController.shared

init(viewModel: PlayerView.ViewModel = ViewModel()) {
self.viewModel = viewModel
}

var body: some View {
VStack(spacing: 0) {
HStack(alignment: .center) {
Spacer()
Button(action: { showingQueue.toggle() }, label: {
Text("Queue")
.fontWeight(.bold)
})
}

if let image = viewModel.artwork {
#if os(macOS)
Image(nsImage: image)
.resizable()
.scaledToFit()
.frame(width: 240, height: 240)
.padding(.top, 30)
#elseif os(iOS)
Image(uiImage: image)
.resizable()
.scaledToFit()
.frame(width: 240, height: 240)
.padding(.top, 30)
#endif
} else {
AsyncImage(url: nil)
.frame(width: 240, height: 240)
.padding(.top, 30)
}

VStack(spacing: 4) {
Text(viewModel.title)
.fontWeight(.semibold)
.font(.system(size: 18))
Text(viewModel.artist)
.fontWeight(.thin)
}
.padding(.top, 30)

if viewModel.maxTime > 0 {
VStack {
Slider(value: $viewModel.position, in: 0...viewModel.maxTime) { editing in
viewModel.isScrubbing = editing
print("scrubbing = \(viewModel.isScrubbing)")
if viewModel.isScrubbing == false {
controller.player.seek(to: viewModel.position)
}
}
HStack {
Text(viewModel.elapsedTime)
.font(.system(size: 14))
Spacer()
Text(viewModel.remainingTime)
.font(.system(size: 14))
}
}
.padding(.top, 25)
} else {
Text("Live Stream")
.padding(.top, 35)
}

HStack {
Button(action: controller.player.previous, label: {
Text("Prev")
.font(.system(size: 14))
})
.frame(maxWidth: .infinity)

Button(action: {
if viewModel.playing {
controller.player.pause()
} else {
controller.player.play()
}
}, label: {
Text(!viewModel.playWhenReady || viewModel.playbackState == .failed ? "Play" : "Pause")
.font(.system(size: 18))
.fontWeight(.semibold)
})

.frame(maxWidth: .infinity)
Button(action: controller.player.next, label: {
Text("Next")
.font(.system(size: 14))
})
.frame(maxWidth: .infinity)
}
.padding(.top, 80)

VStack {
if viewModel.playbackState == .failed {
Text("Playback failed.")
.font(.system(size: 14))
.foregroundStyle(.red)
.padding(.top, 20)
} else if (viewModel.playbackState == .loading || viewModel.playbackState == .buffering) && viewModel.playWhenReady {
ProgressView()
.progressViewStyle(.circular)
.controlSize(.small)
.padding(.top, 20)
}
}

Spacer()
}
.sheet(isPresented: $showingQueue) {
QueueView()
#if os(macOS)
.frame(width: 300, height: 400)
#endif
}
.padding(.horizontal, 16)
.padding(.top)
}
}

#Preview("Standard") {
let viewModel = PlayerView.ViewModel()
viewModel.title = "Longing"
viewModel.artist = "David Chavez"

return PlayerView(viewModel: viewModel)
}

#Preview("Error") {
let viewModel = PlayerView.ViewModel()
viewModel.title = "Longing"
viewModel.artist = "David Chavez"
viewModel.playbackState = .failed

return PlayerView(viewModel: viewModel)
}

#Preview("Buffering") {
let viewModel = PlayerView.ViewModel()
viewModel.title = "Longing"
viewModel.artist = "David Chavez"
viewModel.playbackState = .buffering
viewModel.playWhenReady = true

return PlayerView(viewModel: viewModel)
}

#Preview("Live Stream") {
let viewModel = PlayerView.ViewModel()
viewModel.title = "Longing"
viewModel.artist = "David Chavez"
viewModel.maxTime = 0

return PlayerView(viewModel: viewModel)
}
120 changes: 120 additions & 0 deletions Example/SwiftAudio/PlayerViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
//
// PlayerViewModel.swift
// SwiftAudio
//
// Created by David Chavez on 4/12/24.
//

import SwiftAudioEx

#if os(macOS)
import AppKit
public typealias NativeImage = NSImage
#elseif os(iOS)
import UIKit
public typealias NativeImage = UIImage
#endif

extension PlayerView {
final class ViewModel: ObservableObject {
// MARK: - Observables

@Published var playing: Bool = false
@Published var position: Double = 0
@Published var artwork: NativeImage? = nil
@Published var title: String = ""
@Published var artist: String = ""
@Published var maxTime: TimeInterval = 100
@Published var isScrubbing: Bool = false
@Published var elapsedTime: String = "00:00"
@Published var remainingTime: String = "00:00"

@Published var playWhenReady: Bool = false
@Published var playbackState: AudioPlayerState = .idle

// MARK: - Properties

let controller = AudioController.shared

// MARK: - Initializer

init() {
controller.player.event.playWhenReadyChange.addListener(self, handlePlayWhenReadyChange)
controller.player.event.stateChange.addListener(self, handleAudioPlayerStateChange)
controller.player.event.playbackEnd.addListener(self, handleAudioPlayerPlaybackEnd(data:))
controller.player.event.secondElapse.addListener(self, handleAudioPlayerSecondElapsed)
controller.player.event.seek.addListener(self, handleAudioPlayerDidSeek)
controller.player.event.updateDuration.addListener(self, handleAudioPlayerUpdateDuration)
controller.player.event.didRecreateAVPlayer.addListener(self, handleAVPlayerRecreated)
}

// MARK: - Updates

private func render() {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
playing = (controller.player.playerState == .playing)
playbackState = controller.player.playerState
playWhenReady = controller.player.playWhenReady
position = controller.player.currentTime
maxTime = controller.player.duration
artist = controller.player.currentItem?.getArtist() ?? ""
title = controller.player.currentItem?.getTitle() ?? ""
elapsedTime = controller.player.currentTime.secondsToString()
remainingTime = (controller.player.duration - controller.player.currentTime).secondsToString()
if let item = controller.player.currentItem as? DefaultAudioItem {
artwork = item.artwork
} else {
artwork = nil
}
}
}

private func renderTimes() {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
position = controller.player.currentTime
maxTime = controller.player.duration
elapsedTime = controller.player.currentTime.secondsToString()
remainingTime = (controller.player.duration - controller.player.currentTime).secondsToString()
print(elapsedTime)
}
}

// MARK: - AudioPlayer Event Handlers

func handleAudioPlayerStateChange(data: AudioPlayer.StateChangeEventData) {
print("state=\(data)")
render()
}

func handlePlayWhenReadyChange(data: AudioPlayer.PlayWhenReadyChangeData) {
print("playWhenReady=\(data)")
render()
}

func handleAudioPlayerPlaybackEnd(data: AudioPlayer.PlaybackEndEventData) {
print("playEndReason=\(data)")
}

func handleAudioPlayerSecondElapsed(data: AudioPlayer.SecondElapseEventData) {
if !isScrubbing {
renderTimes()
}
}

func handleAudioPlayerDidSeek(data: AudioPlayer.SeekEventData) {
// .. don't need this
}

func handleAudioPlayerUpdateDuration(data: AudioPlayer.UpdateDurationEventData) {
if !isScrubbing {
renderTimes()
}
}

func handleAVPlayerRecreated() {
// .. don't need this
}
}
}
65 changes: 65 additions & 0 deletions Example/SwiftAudio/QueueView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//
// QueueView.swift
// SwiftAudio
//
// Created by David Chavez on 4/12/24.
//

import SwiftUI
import SwiftAudioEx

struct QueueView: View {
let controller = AudioController.shared
@Environment(\.dismiss) var dismiss

var body: some View {
NavigationStack {
VStack {
List {
if controller.player.currentItem != nil {
Section(header: Text("Playing Now")) {
QueueItemView(
title: controller.player.currentItem?.getTitle() ?? "",
artist: controller.player.currentItem?.getArtist() ?? ""
)
}
}
Section(header: Text("Up Next")) {
ForEach(controller.player.nextItems as! [DefaultAudioItem]) { item in
QueueItemView(
title: item.getTitle() ?? "",
artist: item.getArtist() ?? ""
)
}
}
}
}
.navigationTitle("Queue")
.toolbar {
Button("Close") {
dismiss()
}
}
}
}
}

struct QueueItemView: View {
let title: String
let artist: String

var body: some View {
VStack(alignment: .leading) {
Text(title)
.fontWeight(.semibold)
Text(artist)
.fontWeight(.light)
}
}
}


#Preview {
QueueView()
}

File renamed without changes.
17 changes: 17 additions & 0 deletions Example/SwiftAudio/SwiftAudioApp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// SwiftAudioApp.swift
// SwiftAudio
//
// Created by Brandon Sneed on 3/30/24.
//

import SwiftUI

@main
struct SwiftAudioApp: App {
var body: some Scene {
WindowGroup {
PlayerView()
}
}
}
Loading

0 comments on commit 4276703

Please sign in to comment.