Compare commits

..

17 Commits

Author SHA1 Message Date
jorgenhenrichsen b2972063dd Merge pull request #7 from jorgenhenrichsen/dev
Dev
2018-03-25 18:46:23 +02:00
jorgenhenrichsen fd232f48ca Update podspec
Bump version
2018-03-25 18:36:51 +02:00
jorgenhenrichsen 6ed80f4ff4 Merge pull request #6 from jorgenhenrichsen/feature/queue
Queue
2018-03-25 18:35:33 +02:00
Jørgen Henrichsen f3b55b9a3b Update README
Added bit about the queue
2018-03-25 18:20:12 +02:00
Jørgen Henrichsen 460908e180 Updated example app
Added title, artist, image, timelabels and a queue.
2018-03-25 17:58:56 +02:00
Jørgen Henrichsen 8587b161b2 Added getters for next and previous items. 2018-03-25 17:58:45 +02:00
Jørgen Henrichsen f149ea09ff Moved APError to seperate file 2018-03-25 16:04:49 +02:00
Jørgen Henrichsen efc04395f9 More functions for the queuemanager and QueuedAudioPlayer 2018-03-25 16:03:24 +02:00
Jørgen Henrichsen 65bba1eb75 Next, and previous methods. 2018-03-24 17:44:57 +01:00
Jørgen Henrichsen 876ed22967 Do not remove current item on item complete. 2018-03-24 17:42:48 +01:00
Jørgen Henrichsen b4953f0a73 QueueManager
Two subclasses of AudioPlayer. Simple and Queued.
2018-03-24 16:53:11 +01:00
jorgenhenrichsen 31c3c728f6 Update podscpec
Version
2018-03-22 12:14:43 +01:00
jorgenhenrichsen d6375d996d Merge pull request #4 from jorgenhenrichsen/feature/audio-items-supply-commands
Audio items supply commands
2018-03-22 12:14:13 +01:00
jorgenhenrichsen 730629a07a Merge branch 'master' into feature/audio-items-supply-commands 2018-03-22 12:01:38 +01:00
Jørgen Henrichsen 0bb59cde4a Update README
RemoteCommandable
2018-03-22 11:48:40 +01:00
Jørgen Henrichsen 661a7755df AudioItems can select their own remote commands.
ALso fixed a problem where remote commands would not be disabled properly.
2018-03-22 11:40:27 +01:00
jorgenhenrichsen 01702b41f1 Update README
Correct method signature for enabling remote commands.
2018-03-22 10:11:34 +01:00
21 changed files with 1104 additions and 115 deletions
+16
View File
@@ -7,11 +7,15 @@
objects = {
/* Begin PBXBuildFile section */
070713052067E3EA00F789B3 /* APError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 070713042067E3EA00F789B3 /* APError.swift */; };
0773265A205ED6C400C4D1CD /* AVPlayerObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07732657205ED6C400C4D1CD /* AVPlayerObserver.swift */; };
0773265B205ED6C400C4D1CD /* AVPlayerItemNotificationObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07732658205ED6C400C4D1CD /* AVPlayerItemNotificationObserver.swift */; };
0773265C205ED6C400C4D1CD /* AVPlayerTimeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07732659205ED6C400C4D1CD /* AVPlayerTimeObserver.swift */; };
07732660205ED7BF00C4D1CD /* AudioItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0773265F205ED7BF00C4D1CD /* AudioItem.swift */; };
0775574B2061C1820002C6A1 /* RemoteCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0775574A2061C1820002C6A1 /* RemoteCommand.swift */; };
077557572066867F0002C6A1 /* QueueManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077557562066867F0002C6A1 /* QueueManager.swift */; };
0775575D2066A7DB0002C6A1 /* SimpleAudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0775575C2066A7DB0002C6A1 /* SimpleAudioPlayer.swift */; };
077557612066ABAD0002C6A1 /* QueuedAudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077557602066ABAD0002C6A1 /* QueuedAudioPlayer.swift */; };
07F41B1A205FC0B100E25749 /* AudioSessionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F41B19205FC0B100E25749 /* AudioSessionController.swift */; };
07F41B2220614BDC00E25749 /* RemoteCommandController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F41B2120614BDC00E25749 /* RemoteCommandController.swift */; };
0A2CA8B0DD7E300ABFCF1956E6B58248 /* Nimble.h in Headers */ = {isa = PBXBuildFile; fileRef = 2727DBDF3F41A52F002F6B1992165881 /* Nimble.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -168,11 +172,15 @@
0136A629F86E79FA6B68D9675E08D0AF /* Pods-SwiftAudio_Example-resources.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-SwiftAudio_Example-resources.sh"; sourceTree = "<group>"; };
014CC71E572C5CC14ADFA82A8B7B97DC /* Expression.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Expression.swift; path = Sources/Nimble/Expression.swift; sourceTree = "<group>"; };
057B1682EF92E71137867F0C259AF2CC /* NSString+C99ExtendedIdentifier.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "NSString+C99ExtendedIdentifier.swift"; path = "Sources/Quick/NSString+C99ExtendedIdentifier.swift"; sourceTree = "<group>"; };
070713042067E3EA00F789B3 /* APError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = APError.swift; path = SwiftAudio/Classes/APError.swift; sourceTree = "<group>"; };
07732657205ED6C400C4D1CD /* AVPlayerObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AVPlayerObserver.swift; sourceTree = "<group>"; };
07732658205ED6C400C4D1CD /* AVPlayerItemNotificationObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AVPlayerItemNotificationObserver.swift; sourceTree = "<group>"; };
07732659205ED6C400C4D1CD /* AVPlayerTimeObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AVPlayerTimeObserver.swift; sourceTree = "<group>"; };
0773265F205ED7BF00C4D1CD /* AudioItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AudioItem.swift; path = SwiftAudio/Classes/AudioItem.swift; sourceTree = "<group>"; };
0775574A2061C1820002C6A1 /* RemoteCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = RemoteCommand.swift; path = SwiftAudio/Classes/RemoteCommand.swift; sourceTree = "<group>"; };
077557562066867F0002C6A1 /* QueueManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = QueueManager.swift; path = SwiftAudio/Classes/QueueManager.swift; sourceTree = "<group>"; };
0775575C2066A7DB0002C6A1 /* SimpleAudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SimpleAudioPlayer.swift; path = SwiftAudio/Classes/SimpleAudioPlayer.swift; sourceTree = "<group>"; };
077557602066ABAD0002C6A1 /* QueuedAudioPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = QueuedAudioPlayer.swift; path = SwiftAudio/Classes/QueuedAudioPlayer.swift; sourceTree = "<group>"; };
07F41B19205FC0B100E25749 /* AudioSessionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AudioSessionController.swift; path = SwiftAudio/Classes/AudioSessionController.swift; sourceTree = "<group>"; };
07F41B2120614BDC00E25749 /* RemoteCommandController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = RemoteCommandController.swift; path = SwiftAudio/Classes/RemoteCommandController.swift; sourceTree = "<group>"; };
0A9D7EA20C39A55A1EF0C23094895A75 /* Quick-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Quick-dummy.m"; sourceTree = "<group>"; };
@@ -388,6 +396,8 @@
isa = PBXGroup;
children = (
E469FE2F4CBA2182A64C31D9B41A936E /* AudioPlayer.swift */,
0775575C2066A7DB0002C6A1 /* SimpleAudioPlayer.swift */,
077557602066ABAD0002C6A1 /* QueuedAudioPlayer.swift */,
0773265F205ED7BF00C4D1CD /* AudioItem.swift */,
C455E233BD4071A35296FEDA8D99CEDE /* MediaItemProperty.swift */,
32A658905B9E84D827AA64605F28E3F9 /* NowPlayingInfoController.swift */,
@@ -396,6 +406,8 @@
07F41B19205FC0B100E25749 /* AudioSessionController.swift */,
07F41B2120614BDC00E25749 /* RemoteCommandController.swift */,
0775574A2061C1820002C6A1 /* RemoteCommand.swift */,
077557562066867F0002C6A1 /* QueueManager.swift */,
070713042067E3EA00F789B3 /* APError.swift */,
07732656205ED6C400C4D1CD /* Observer */,
25961FA3FF989AFD1E08A55F4E464276 /* AVPlayerWrapper */,
853A8C0AF6F0B4902930084E06DEA640 /* Support Files */,
@@ -981,6 +993,8 @@
0773265C205ED6C400C4D1CD /* AVPlayerTimeObserver.swift in Sources */,
07F41B1A205FC0B100E25749 /* AudioSessionController.swift in Sources */,
A1A245A1D2A54FF00AACE521F153D281 /* AudioPlayer.swift in Sources */,
0775575D2066A7DB0002C6A1 /* SimpleAudioPlayer.swift in Sources */,
077557572066867F0002C6A1 /* QueueManager.swift in Sources */,
C525C159B05D6FEEE0FC3D16910C934B /* AVPlayerWrapper.swift in Sources */,
C226CFBDCE851BDA9CD1DA26894BF272 /* AVPlayerWrapperState.swift in Sources */,
5E48B05B2B78E0AD05E2DBA8B1862AE5 /* MediaItemProperty.swift in Sources */,
@@ -988,10 +1002,12 @@
FCE55A287889D68B475EBFBE8AB26E4B /* NowPlayingInfoController.swift in Sources */,
0775574B2061C1820002C6A1 /* RemoteCommand.swift in Sources */,
07732660205ED7BF00C4D1CD /* AudioItem.swift in Sources */,
070713052067E3EA00F789B3 /* APError.swift in Sources */,
07F41B2220614BDC00E25749 /* RemoteCommandController.swift in Sources */,
6E0C5034AB99CC7E6B0BF002D488D85D /* NowPlayingInfoProperty.swift in Sources */,
A45D4CA6BC297DB896B20F391DF60D61 /* SwiftAudio-dummy.m in Sources */,
0773265B205ED6C400C4D1CD /* AVPlayerItemNotificationObserver.swift in Sources */,
077557612066ABAD0002C6A1 /* QueuedAudioPlayer.swift in Sources */,
8577EFB55077C469A41CFFBB5C749995 /* TimeEventFrequency.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -8,6 +8,11 @@
/* Begin PBXBuildFile section */
02EAB7FA622CF9CCD4F328C7 /* Pods_SwiftAudio_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C9CEE0D1B64E6BEF775F214D /* Pods_SwiftAudio_Tests.framework */; };
070713072067EB4F00F789B3 /* Double + Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 070713062067EB4F00F789B3 /* Double + Extensions.swift */; };
070713092067EFFB00F789B3 /* AudioController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 070713082067EFFB00F789B3 /* AudioController.swift */; };
0707130B2067F2E000F789B3 /* QueueViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0707130A2067F2E000F789B3 /* QueueViewController.swift */; };
0707130F2067F40A00F789B3 /* QueueTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0707130D2067F40A00F789B3 /* QueueTableViewCell.swift */; };
070713102067F40A00F789B3 /* QueueTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0707130E2067F40A00F789B3 /* QueueTableViewCell.xib */; };
074A6483205C155E0083D868 /* AVPlayerTimeObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074A6482205C155E0083D868 /* AVPlayerTimeObserverTests.swift */; };
074A6485205C29920083D868 /* AVPlayerItemNotificationObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074A6484205C29920083D868 /* AVPlayerItemNotificationObserverTests.swift */; };
074A6487205E59B60083D868 /* AVPlayerWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074A6486205E59B60083D868 /* AVPlayerWrapperTests.swift */; };
@@ -15,6 +20,7 @@
07732653205EB1B500C4D1CD /* nasa_throttle_up.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 07732652205EB1B500C4D1CD /* nasa_throttle_up.mp3 */; };
07732654205ECA8B00C4D1CD /* WAV-MP3.wav in Resources */ = {isa = PBXBuildFile; fileRef = 07732650205EACA300C4D1CD /* WAV-MP3.wav */; };
07732655205ECE1C00C4D1CD /* nasa_throttle_up.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 07732652205EB1B500C4D1CD /* nasa_throttle_up.mp3 */; };
0775575920668B020002C6A1 /* QueueManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0775575820668B020002C6A1 /* QueueManagerTests.swift */; };
607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; };
607FACD81AFB9204008FA782 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD71AFB9204008FA782 /* ViewController.swift */; };
607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; };
@@ -35,11 +41,17 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
070713062067EB4F00F789B3 /* Double + Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double + Extensions.swift"; sourceTree = "<group>"; };
070713082067EFFB00F789B3 /* AudioController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioController.swift; sourceTree = "<group>"; };
0707130A2067F2E000F789B3 /* QueueViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueViewController.swift; sourceTree = "<group>"; };
0707130D2067F40A00F789B3 /* QueueTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueTableViewCell.swift; sourceTree = "<group>"; };
0707130E2067F40A00F789B3 /* QueueTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = QueueTableViewCell.xib; sourceTree = "<group>"; };
074A6482205C155E0083D868 /* AVPlayerTimeObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerTimeObserverTests.swift; sourceTree = "<group>"; };
074A6484205C29920083D868 /* AVPlayerItemNotificationObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerItemNotificationObserverTests.swift; sourceTree = "<group>"; };
074A6486205E59B60083D868 /* AVPlayerWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerWrapperTests.swift; sourceTree = "<group>"; };
07732650205EACA300C4D1CD /* WAV-MP3.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = "WAV-MP3.wav"; sourceTree = "<group>"; };
07732652205EB1B500C4D1CD /* nasa_throttle_up.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = nasa_throttle_up.mp3; sourceTree = "<group>"; };
0775575820668B020002C6A1 /* QueueManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueManagerTests.swift; sourceTree = "<group>"; };
521F3AEC1228A2FA2637355F /* Pods-SwiftAudio_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftAudio_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftAudio_Tests/Pods-SwiftAudio_Tests.debug.xcconfig"; sourceTree = "<group>"; };
607FACD01AFB9204008FA782 /* SwiftAudio_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftAudio_Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
607FACD41AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -106,7 +118,12 @@
isa = PBXGroup;
children = (
607FACD51AFB9204008FA782 /* AppDelegate.swift */,
070713082067EFFB00F789B3 /* AudioController.swift */,
607FACD71AFB9204008FA782 /* ViewController.swift */,
0707130A2067F2E000F789B3 /* QueueViewController.swift */,
070713062067EB4F00F789B3 /* Double + Extensions.swift */,
0707130D2067F40A00F789B3 /* QueueTableViewCell.swift */,
0707130E2067F40A00F789B3 /* QueueTableViewCell.xib */,
607FACD91AFB9204008FA782 /* Main.storyboard */,
607FACDC1AFB9204008FA782 /* Images.xcassets */,
607FACDE1AFB9204008FA782 /* LaunchScreen.xib */,
@@ -131,6 +148,7 @@
074A6482205C155E0083D868 /* AVPlayerTimeObserverTests.swift */,
074A6484205C29920083D868 /* AVPlayerItemNotificationObserverTests.swift */,
074A6486205E59B60083D868 /* AVPlayerWrapperTests.swift */,
0775575820668B020002C6A1 /* QueueManagerTests.swift */,
07732650205EACA300C4D1CD /* WAV-MP3.wav */,
07732652205EB1B500C4D1CD /* nasa_throttle_up.mp3 */,
607FACE91AFB9204008FA782 /* Supporting Files */,
@@ -275,6 +293,7 @@
607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */,
607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */,
07732655205ECE1C00C4D1CD /* nasa_throttle_up.mp3 in Resources */,
070713102067F40A00F789B3 /* QueueTableViewCell.xib in Resources */,
07732654205ECA8B00C4D1CD /* WAV-MP3.wav in Resources */,
607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */,
);
@@ -403,7 +422,11 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0707130B2067F2E000F789B3 /* QueueViewController.swift in Sources */,
070713072067EB4F00F789B3 /* Double + Extensions.swift in Sources */,
607FACD81AFB9204008FA782 /* ViewController.swift in Sources */,
0707130F2067F40A00F789B3 /* QueueTableViewCell.swift in Sources */,
070713092067EFFB00F789B3 /* AudioController.swift in Sources */,
607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -412,6 +435,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0775575920668B020002C6A1 /* QueueManagerTests.swift in Sources */,
074A6483205C155E0083D868 /* AVPlayerTimeObserverTests.swift in Sources */,
074A6485205C29920083D868 /* AVPlayerItemNotificationObserverTests.swift in Sources */,
607FACEC1AFB9204008FA782 /* AVPlayerObserverTests.swift in Sources */,
+39
View File
@@ -0,0 +1,39 @@
//
// AudioController.swift
// SwiftAudio_Example
//
// Created by Jørgen Henrichsen on 25/03/2018.
// Copyright © 2018 CocoaPods. All rights reserved.
//
import Foundation
import SwiftAudio
class AudioController {
static let shared = AudioController()
let player = QueuedAudioPlayer()
let audioSessionController = AudioSessionController.shared
let sources: [AudioItem] = [
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/67b51d90ffddd6bb3f095059997021b589845f81?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "33 \"GOD\"", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/081447adc23dad4f79ba4f1082615d1c56edf5e1?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "8 (circle)", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/6f9999d909b017eabef97234dd7a206355720d9d?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "715 - CRΣΣKS", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/bf9bdd403c67fdbe06a582e7b292487c8cfd1f7e?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "____45_____", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI"))
]
init() {
player.remoteCommands = [
.stop,
.togglePlayPause,
.skipForward(preferredIntervals: [30]),
.skipBackward(preferredIntervals: [30]),
.changePlaybackPosition
]
try? audioSessionController.set(category: .playback)
try? audioSessionController.activateSession()
try? player.add(items: sources, playWhenReady: false)
}
}
+144 -35
View File
@@ -22,64 +22,173 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ExO-ir-bnt">
<rect key="frame" x="168" y="20" width="38" height="30"/>
<state key="normal" title="PlayA"/>
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" translatesAutoresizingMaskIntoConstraints="NO" id="RX3-VR-CL6">
<rect key="frame" x="32" y="533" width="311" height="34"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="9Q1-U9-TUC">
<rect key="frame" x="0.0" y="0.0" width="103.5" height="34"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<state key="normal" title="Prev"/>
<connections>
<action selector="previous:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="fFb-iW-sFr"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" horizontalCompressionResistancePriority="751" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="EOo-zV-6l2">
<rect key="frame" x="103.5" y="0.0" width="104" height="34"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="18"/>
<state key="normal" title="Play"/>
<connections>
<action selector="togglePlay:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="oYu-xi-n6T"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Nhf-qB-91A">
<rect key="frame" x="207.5" y="0.0" width="103.5" height="34"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<state key="normal" title="Next"/>
<connections>
<action selector="next:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="Tha-3J-gVM"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstAttribute="height" constant="34" id="T4q-HG-vqM"/>
</constraints>
</stackView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="l9B-hM-Ajc">
<rect key="frame" x="302" y="20" width="57" height="34"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="18"/>
<state key="normal" title="Queue"/>
<connections>
<action selector="playA:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="KZw-pL-C6H"/>
<segue destination="vDz-qW-uY8" kind="presentation" identifier="QueueSegue" id="eke-1c-Fsm"/>
</connections>
</button>
<slider opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="0.5" minValue="0.0" maxValue="1" translatesAutoresizingMaskIntoConstraints="NO" id="RWN-If-dGG">
<rect key="frame" x="14" y="318.5" width="347" height="31"/>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="FCd-3e-22D">
<rect key="frame" x="67" y="84" width="240" height="240"/>
<constraints>
<constraint firstAttribute="width" constant="343" id="CD7-DZ-gUR"/>
<constraint firstAttribute="width" constant="240" id="5Sj-BZ-sg4"/>
<constraint firstAttribute="height" constant="240" id="Hij-Yw-6Lg"/>
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="00:00" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="3CL-8o-zYW">
<rect key="frame" x="16" y="462" width="39" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" red="0.99999600649999998" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="00:00" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="RVb-HZ-QCX">
<rect key="frame" x="320" y="462" width="39" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" red="0.99999600649999998" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<slider opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" minValue="0.0" maxValue="1" translatesAutoresizingMaskIntoConstraints="NO" id="RWN-If-dGG">
<rect key="frame" x="14" y="424" width="347" height="31"/>
<color key="tintColor" red="1" green="0.83234566450000003" blue="0.47320586440000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="maximumTrackTintColor" red="0.99999600649999998" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="thumbTintColor" red="1" green="0.83234566450000003" blue="0.47320586440000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<connections>
<action selector="scrubbing:" destination="vXZ-lx-hvc" eventType="touchUpOutside" id="HeH-aB-VXZ"/>
<action selector="scrubbing:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="NfP-3T-dnw"/>
<action selector="scrubbingValueChanged:" destination="vXZ-lx-hvc" eventType="valueChanged" id="MLD-nW-rXm"/>
<action selector="startScrubbing:" destination="vXZ-lx-hvc" eventType="touchDown" id="lD9-dR-QTO"/>
</connections>
</slider>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="EOo-zV-6l2">
<rect key="frame" x="164" y="494" width="46" height="30"/>
<constraints>
<constraint firstAttribute="height" constant="30" id="9sD-FT-FL0"/>
</constraints>
<state key="normal" title="Play"/>
<connections>
<action selector="togglePlay:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="oYu-xi-n6T"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="TPr-Wm-k0L">
<rect key="frame" x="168" y="58" width="39" height="30"/>
<state key="normal" title="PlayB"/>
<connections>
<action selector="playA:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="LEW-J0-qbI"/>
<action selector="playB:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="OhP-ri-ZWx"/>
</connections>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Title" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="dfk-yr-rwm">
<rect key="frame" x="16" y="354" width="343" height="21.5"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="18"/>
<color key="textColor" red="0.99999600649999998" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Artist" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="T7Y-1Q-7UU">
<rect key="frame" x="16" y="379.5" width="343" height="19.5"/>
<fontDescription key="fontDescription" type="system" weight="thin" pointSize="16"/>
<color key="textColor" red="0.99999600649999998" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="backgroundColor" red="0.12984204290000001" green="0.12984612579999999" blue="0.12984395030000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="tintColor" red="1" green="0.83234566450000003" blue="0.47320586440000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="trailingMargin" secondItem="EOo-zV-6l2" secondAttribute="trailing" constant="149" id="71L-Hv-1br"/>
<constraint firstItem="RWN-If-dGG" firstAttribute="centerY" secondItem="kh9-bI-dsS" secondAttribute="centerY" id="Ee0-Yx-h5a"/>
<constraint firstItem="RWN-If-dGG" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="EzW-Sk-mlN"/>
<constraint firstItem="EOo-zV-6l2" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leadingMargin" constant="148" id="OKq-yH-xWk"/>
<constraint firstItem="TPr-Wm-k0L" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="OaA-Nd-ZGX"/>
<constraint firstItem="ExO-ir-bnt" firstAttribute="top" secondItem="jyV-Pf-zRb" secondAttribute="bottom" id="VtX-IN-h4a"/>
<constraint firstItem="2fi-mo-0CV" firstAttribute="top" secondItem="EOo-zV-6l2" secondAttribute="bottom" constant="143" id="m8i-yM-pER"/>
<constraint firstItem="ExO-ir-bnt" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="mc2-i7-c1f"/>
<constraint firstItem="TPr-Wm-k0L" firstAttribute="top" secondItem="ExO-ir-bnt" secondAttribute="bottom" constant="8" id="voY-Ue-QrT"/>
<constraint firstItem="T7Y-1Q-7UU" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leadingMargin" id="0eh-sL-186"/>
<constraint firstItem="l9B-hM-Ajc" firstAttribute="trailing" secondItem="kh9-bI-dsS" secondAttribute="trailingMargin" id="54L-0h-0ba"/>
<constraint firstItem="l9B-hM-Ajc" firstAttribute="top" secondItem="jyV-Pf-zRb" secondAttribute="bottom" id="9Uh-K9-988"/>
<constraint firstItem="RVb-HZ-QCX" firstAttribute="trailing" secondItem="kh9-bI-dsS" secondAttribute="trailingMargin" id="BhV-UD-qhh"/>
<constraint firstItem="FCd-3e-22D" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="GhI-f1-DkR"/>
<constraint firstItem="T7Y-1Q-7UU" firstAttribute="trailing" secondItem="kh9-bI-dsS" secondAttribute="trailingMargin" id="HoH-i0-yof"/>
<constraint firstItem="RWN-If-dGG" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leadingMargin" id="Nw7-WM-LFd"/>
<constraint firstItem="RX3-VR-CL6" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="O0h-NL-iXW"/>
<constraint firstItem="dfk-yr-rwm" firstAttribute="top" secondItem="FCd-3e-22D" secondAttribute="bottom" constant="30" id="W4w-6K-AW8"/>
<constraint firstItem="RWN-If-dGG" firstAttribute="top" secondItem="T7Y-1Q-7UU" secondAttribute="bottom" constant="25" id="XgV-XL-QCL"/>
<constraint firstItem="dfk-yr-rwm" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leadingMargin" id="YUE-uf-Rp1"/>
<constraint firstItem="RVb-HZ-QCX" firstAttribute="top" secondItem="RWN-If-dGG" secondAttribute="bottom" constant="8" id="ZkD-u2-Zbr"/>
<constraint firstItem="T7Y-1Q-7UU" firstAttribute="top" secondItem="dfk-yr-rwm" secondAttribute="bottom" constant="4" id="baR-zV-tgo"/>
<constraint firstItem="RWN-If-dGG" firstAttribute="trailing" secondItem="kh9-bI-dsS" secondAttribute="trailingMargin" id="eNt-u9-qot"/>
<constraint firstItem="RX3-VR-CL6" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leadingMargin" constant="16" id="hEd-b2-Ggo"/>
<constraint firstItem="FCd-3e-22D" firstAttribute="top" secondItem="l9B-hM-Ajc" secondAttribute="bottom" constant="30" id="ikz-ZP-jNM"/>
<constraint firstAttribute="trailingMargin" secondItem="RX3-VR-CL6" secondAttribute="trailing" constant="16" id="kSP-Mq-R5P"/>
<constraint firstItem="dfk-yr-rwm" firstAttribute="trailing" secondItem="kh9-bI-dsS" secondAttribute="trailingMargin" id="m6u-7a-ffF"/>
<constraint firstItem="3CL-8o-zYW" firstAttribute="top" secondItem="RWN-If-dGG" secondAttribute="bottom" constant="8" id="sGK-bn-zxD"/>
<constraint firstItem="2fi-mo-0CV" firstAttribute="top" secondItem="RX3-VR-CL6" secondAttribute="bottom" constant="100" id="vd2-dd-hVu"/>
<constraint firstItem="3CL-8o-zYW" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leadingMargin" id="wOy-Rx-rvK"/>
</constraints>
</view>
<connections>
<outlet property="artistLabel" destination="T7Y-1Q-7UU" id="b5S-lt-PqG"/>
<outlet property="elapsedTimeLabel" destination="3CL-8o-zYW" id="7Wg-7X-Vrd"/>
<outlet property="imageView" destination="FCd-3e-22D" id="gKL-za-haV"/>
<outlet property="playButton" destination="EOo-zV-6l2" id="2d1-ad-s1k"/>
<outlet property="remainingTimeLabel" destination="RVb-HZ-QCX" id="8hp-CK-XjF"/>
<outlet property="slider" destination="RWN-If-dGG" id="Yxw-Gf-bR3"/>
<outlet property="titleLabel" destination="dfk-yr-rwm" id="Hk3-m5-IOi"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="x5A-6p-PRh" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="117.59999999999999" y="118.29085457271366"/>
</scene>
<!--Queue View Controller-->
<scene sceneID="5Fm-oE-9Zc">
<objects>
<viewController id="vDz-qW-uY8" customClass="QueueViewController" customModule="SwiftAudio_Example" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="kv3-s6-lb0"/>
<viewControllerLayoutGuide type="bottom" id="Fhe-7w-8BG"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="y7Y-Gm-oyZ">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="dzA-9p-ejh">
<rect key="frame" x="310" y="20" width="49" height="34"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="18"/>
<state key="normal" title="Close"/>
<connections>
<action selector="closeButton:" destination="vDz-qW-uY8" eventType="touchUpInside" id="0TB-bG-he7"/>
</connections>
</button>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="none" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="HPi-Pd-J9K">
<rect key="frame" x="0.0" y="74" width="375" height="593"/>
<color key="backgroundColor" red="0.12984204290000001" green="0.12984612579999999" blue="0.12984395030000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</tableView>
</subviews>
<color key="backgroundColor" red="0.12984204290000001" green="0.12984612579999999" blue="0.12984395030000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="tintColor" red="1" green="0.1857388616" blue="0.57339501380000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="HPi-Pd-J9K" secondAttribute="trailing" id="CdI-lT-19N"/>
<constraint firstItem="Fhe-7w-8BG" firstAttribute="top" secondItem="HPi-Pd-J9K" secondAttribute="bottom" id="Gb9-C1-ajx"/>
<constraint firstItem="HPi-Pd-J9K" firstAttribute="leading" secondItem="y7Y-Gm-oyZ" secondAttribute="leading" id="aN2-LD-yxR"/>
<constraint firstItem="HPi-Pd-J9K" firstAttribute="top" secondItem="dzA-9p-ejh" secondAttribute="bottom" constant="20" id="aSx-t1-T3e"/>
<constraint firstItem="dzA-9p-ejh" firstAttribute="top" secondItem="kv3-s6-lb0" secondAttribute="bottom" id="nAL-i2-VQS"/>
<constraint firstAttribute="trailing" secondItem="dzA-9p-ejh" secondAttribute="trailing" constant="16" id="qrg-S3-JJ2"/>
</constraints>
</view>
<connections>
<outlet property="tableView" destination="HPi-Pd-J9K" id="P8P-at-xLc"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="zk4-9r-5Oh" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="917.60000000000002" y="117.39130434782609"/>
</scene>
</scenes>
</document>
@@ -0,0 +1,25 @@
//
// Double + Extensions.swift
// SwiftAudio_Example
//
// Created by Jørgen Henrichsen on 25/03/2018.
// Copyright © 2018 CocoaPods. All rights reserved.
//
import Foundation
extension Double {
private var formatter: DateComponentsFormatter {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.minute, .second]
formatter.unitsStyle = .positional
formatter.zeroFormattingBehavior = .pad
return formatter
}
func secondsToString() -> String {
return formatter.string(from: self) ?? ""
}
}
+4
View File
@@ -35,10 +35,14 @@
<array>
<string>armv7</string>
</array>
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleLightContent</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>
@@ -0,0 +1,24 @@
//
// QueueTableViewCell.swift
// SwiftAudio_Example
//
// Created by Jørgen Henrichsen on 25/03/2018.
// Copyright © 2018 CocoaPods. All rights reserved.
//
import UIKit
class QueueTableViewCell: UITableViewCell {
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var artistLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
}
+55
View File
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="QueueTableViewCell" customModule="SwiftAudio_Example" customModuleProvider="target"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" id="KGk-i7-Jjw" customClass="QueueTableViewCell" customModule="SwiftAudio_Example" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="80"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
<rect key="frame" x="0.0" y="0.0" width="375" height="79.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="R0I-g7-ETn">
<rect key="frame" x="16" y="16" width="343" height="19.5"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="16"/>
<color key="textColor" red="0.99999600649999998" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Artist" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="jRU-3B-2pA">
<rect key="frame" x="16" y="43.5" width="343" height="19.5"/>
<fontDescription key="fontDescription" type="system" weight="light" pointSize="16"/>
<color key="textColor" red="0.99999600649999998" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" red="0.12984204290000001" green="0.12984612579999999" blue="0.12984395030000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="R0I-g7-ETn" firstAttribute="trailing" secondItem="H2p-sc-9uM" secondAttribute="trailingMargin" id="8gl-XI-iAW"/>
<constraint firstItem="jRU-3B-2pA" firstAttribute="trailing" secondItem="H2p-sc-9uM" secondAttribute="trailingMargin" id="A7F-XO-H0i"/>
<constraint firstItem="jRU-3B-2pA" firstAttribute="top" secondItem="R0I-g7-ETn" secondAttribute="bottom" constant="8" id="Jdu-e3-Oeq"/>
<constraint firstItem="R0I-g7-ETn" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leadingMargin" id="VNU-d7-G4N"/>
<constraint firstAttribute="bottomMargin" secondItem="jRU-3B-2pA" secondAttribute="bottom" constant="6" id="nBr-J4-PUM"/>
<constraint firstItem="R0I-g7-ETn" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="topMargin" constant="5" id="tE6-pp-JML"/>
<constraint firstItem="jRU-3B-2pA" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leadingMargin" id="z3F-hI-GcC"/>
</constraints>
</tableViewCellContentView>
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
<connections>
<outlet property="artistLabel" destination="jRU-3B-2pA" id="IVV-n5-wmt"/>
<outlet property="titleLabel" destination="R0I-g7-ETn" id="ICg-6a-6vz"/>
</connections>
<point key="canvasLocation" x="34.5" y="54"/>
</tableViewCell>
</objects>
</document>
@@ -0,0 +1,84 @@
//
// QueueViewController.swift
// SwiftAudio_Example
//
// Created by Jørgen Henrichsen on 25/03/2018.
// Copyright © 2018 CocoaPods. All rights reserved.
//
import UIKit
import SwiftAudio
class QueueViewController: UIViewController {
let controller = AudioController.shared
@IBOutlet weak var tableView: UITableView!
let cellReuseId: String = "QueueCell"
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UINib.init(nibName: "QueueTableViewCell", bundle: Bundle.main), forCellReuseIdentifier: cellReuseId)
tableView.delegate = self
tableView.dataSource = self
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
@IBAction func closeButton(_ sender: UIButton) {
self.dismiss(animated: true, completion: nil)
}
}
extension QueueViewController: UITableViewDataSource, UITableViewDelegate {
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section {
case 0:
return 1
case 1:
return controller.player.nextItems?.count ?? 0
default:
return 0
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseId, for: indexPath) as! QueueTableViewCell
let item: AudioItem?
switch indexPath.section {
case 0:
item = controller.player.currentItem
case 1:
item = controller.player.nextItems?[indexPath.row]
default:
item = nil
}
if let item = item {
cell.titleLabel.text = item.getTitle()
cell.artistLabel.text = item.getArtist()
}
return cell
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch section {
case 0: return "Playing Now"
case 1: return "Up Next"
default: return nil
}
}
}
+42 -41
View File
@@ -16,43 +16,30 @@ class ViewController: UIViewController {
@IBOutlet weak var playButton: UIButton!
@IBOutlet weak var slider: UISlider!
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var remainingTimeLabel: UILabel!
@IBOutlet weak var elapsedTimeLabel: UILabel!
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var artistLabel: UILabel!
var isScrubbing: Bool = false
var audioPlayer: AudioPlayer = AudioPlayer()
let audioSessionController: AudioSessionController = AudioSessionController.shared
let localSource = DefaultAudioItem(audioUrl: Bundle.main.path(forResource: "WAV-MP3", ofType: "wav")!, artist: "Artist", title: "Title", albumTitle: "Album", sourceType: .file, artwork: #imageLiteral(resourceName: "cover"))
let streamSource = DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/081447adc23dad4f79ba4f1082615d1c56edf5e1?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "8 (circle)", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI"))
var artwork: MPMediaItemArtwork!
let controller = AudioController.shared
override func viewDidLoad() {
super.viewDidLoad()
audioPlayer.delegate = self
audioPlayer.enableRemoteCommands([
.stop,
.togglePlayPause,
.skipForward(preferredIntervals: [30]),
.skipBackward(preferredIntervals: [30]),
.changePlaybackPosition
])
try? audioSessionController.set(category: .playback)
try? audioSessionController.activateSession()
let image = #imageLiteral(resourceName: "cover")
artwork = MPMediaItemArtwork(boundsSize: image.size, requestHandler: { (size) -> UIImage in
return image
})
}
@IBAction func playA(_ sender: Any) {
try? audioPlayer.load(item: localSource)
}
@IBAction func playB(_ sender: Any) {
try? audioPlayer.load(item: streamSource)
controller.player.delegate = self
}
@IBAction func togglePlay(_ sender: Any) {
try? audioPlayer.togglePlaying()
try? controller.player.togglePlaying()
}
@IBAction func previous(_ sender: Any) {
try? controller.player.previous()
}
@IBAction func next(_ sender: Any) {
try? controller.player.next()
}
@IBAction func startScrubbing(_ sender: UISlider) {
@@ -60,27 +47,39 @@ class ViewController: UIViewController {
}
@IBAction func scrubbing(_ sender: UISlider) {
try? audioPlayer.seek(to: Double(slider.value))
try? controller.player.seek(to: Double(slider.value))
}
func update() {
slider.maximumValue = Float(audioPlayer.duration)
slider.setValue(Float(audioPlayer.currentTime), animated: true)
@IBAction func scrubbingValueChanged(_ sender: UISlider) {
let value = Double(slider.value)
elapsedTimeLabel.text = value.secondsToString()
remainingTimeLabel.text = (controller.player.duration - value).secondsToString()
}
}
extension ViewController: AudioPlayerDelegate {
func audioPlayer(playerDidChangeState state: AVPlayerWrapperState) {
print("AudioPlayer state: ", state.rawValue)
self.update()
if state == .playing {
playButton.setTitle("Pause", for: .normal)
}
else {
playButton.setTitle("Play", for: .normal)
playButton.setTitle(state == .playing ? "Pause" : "Play", for: .normal)
switch state {
case .ready:
if let item = controller.player.currentItem {
titleLabel.text = item.getTitle()
artistLabel.text = item.getArtist()
item.getArtwork({ (image) in
self.imageView.image = image
})
}
slider.maximumValue = Float(controller.player.duration)
slider.setValue(Float(controller.player.currentTime), animated: true)
case .loading, .playing, .paused, .idle:
slider.maximumValue = Float(controller.player.duration)
slider.setValue(Float(controller.player.currentTime), animated: true)
}
}
@@ -92,6 +91,8 @@ extension ViewController: AudioPlayerDelegate {
func audioPlayer(secondsElapsed seconds: Double) {
if !isScrubbing {
slider.setValue(Float(seconds), animated: false)
elapsedTimeLabel.text = controller.player.currentTime.secondsToString()
remainingTimeLabel.text = (controller.player.duration - controller.player.currentTime).secondsToString()
}
}
+254
View File
@@ -0,0 +1,254 @@
import Quick
import Nimble
@testable import SwiftAudio
class QueueManagerTests: QuickSpec {
let dummyItem = 0
let dummyItems: [Int] = [0, 1, 2, 3, 4, 5, 6]
override func spec() {
describe("A QueueManager") {
var manager: QueueManager<Int>!
beforeEach {
manager = QueueManager()
}
context("when adding one item", {
beforeEach {
manager.addItem(self.dummyItem)
}
it("should have an item in the queue", closure: {
expect(manager.items).notTo(beEmpty())
})
it("should set it as the current item", closure: {
expect(manager.current).toNot(beNil())
expect(manager.current).to(equal(self.dummyItem))
})
context("then calling next", {
var nextItem: Int?
beforeEach {
nextItem = try? manager.next()
}
it("should not return", closure: {
expect(nextItem).to(beNil())
})
})
context("then calling previous", {
var previousItem: Int?
beforeEach {
previousItem = try? manager.previous()
}
it("should not return", closure: {
expect(previousItem).to(beNil())
})
})
})
context("when adding multiple items", {
beforeEach {
manager.addItems(self.dummyItems)
}
it("should have items in the queue", closure: {
expect(manager.items.count).to(equal(self.dummyItems.count))
})
it("should have the first item as a current item", closure: {
expect(manager.current).toNot(beNil())
expect(manager.current).to(equal(self.dummyItems.first))
})
context("then calling next", {
var nextItem: Int?
beforeEach {
nextItem = try? manager.next()
}
it("should return the next item", closure: {
expect(nextItem).toNot(beNil())
expect(nextItem).to(equal(self.dummyItems[1]))
})
it("should have next current item", closure: {
expect(manager.current).to(equal(self.dummyItems[1]))
})
context("then calling previous", {
var previousItem: Int?
beforeEach {
previousItem = try? manager.previous()
}
it("should return the first item", closure: {
expect(previousItem).toNot(beNil())
expect(previousItem).to(equal(self.dummyItems.first))
})
it("should have the previous current item", closure: {
expect(manager.current).to(equal(self.dummyItems.first))
})
})
})
// MARK: - Removal
context("then removing the second item", {
var removed: Int?
beforeEach {
removed = try? manager.remove(atIndex: 1)
}
it("should have one less item", closure: {
expect(removed).toNot(beNil())
expect(manager.items.count).to(equal(self.dummyItems.count - 1))
})
})
context("then removing the last item", {
var removed: Int?
beforeEach {
removed = try? manager.remove(atIndex: self.dummyItems.count - 1)
}
it("should have one less item", closure: {
expect(removed).toNot(beNil())
expect(manager.items.count).to(equal(self.dummyItems.count - 1))
})
})
context("then removing the current item", {
var removed: Int?
beforeEach {
removed = try? manager.remove(atIndex: manager.currentIndex)
}
it("should not remove any items", closure: {
expect(removed).to(beNil())
expect(manager.items.count).to(equal(self.dummyItems.count))
})
})
context("then removing with too large index", {
var removed: Int?
beforeEach {
removed = try? manager.remove(atIndex: self.dummyItems.count)
}
it("should not remove any items", closure: {
expect(removed).to(beNil())
expect(manager.items.count).to(equal(self.dummyItems.count))
})
})
context("then removing with too small index", {
var removed: Int?
beforeEach {
removed = try? manager.remove(atIndex: -1)
}
it("should not remove any items", closure: {
expect(removed).to(beNil())
expect(manager.items.count).to(equal(self.dummyItems.count))
})
})
// MARK: - Jumping
context("then jumping to the second item", {
var jumped: Int?
beforeEach {
try? jumped = manager.jump(to: 1)
}
it("should return the current item", closure: {
expect(jumped).toNot(beNil())
expect(jumped).to(equal(manager.current))
})
it("should move the current index", closure: {
expect(manager.currentIndex).to(equal(1))
})
})
context("then jumping to last item", closure: {
var jumped: Int?
beforeEach {
try? jumped = manager.jump(to: manager.items.count - 1)
}
it("should return the current item", closure: {
expect(jumped).toNot(beNil())
expect(jumped).to(equal(manager.current))
})
it("should move the current index", closure: {
expect(manager.currentIndex).to(equal(manager.items.count - 1))
})
})
context("then jumping to a negative index", closure: {
var jumped: Int?
beforeEach {
jumped = try? manager.jump(to: -1)
}
it("should not return", closure: {
expect(jumped).to(beNil())
})
it("should not move the current index", closure: {
expect(manager.currentIndex).to(equal(0))
})
})
context("then jumping with too large index", closure: {
var jumped: Int?
beforeEach {
jumped = try? manager.jump(to: manager.items.count)
}
it("should not return", closure: {
expect(jumped).to(beNil())
})
it("should not move the current index", closure: {
expect(manager.currentIndex).to(equal(0))
})
})
// MARK: - Moving
context("then moving 2nd to 4th", closure: {
let afterMoving: [Int] = [0, 2, 3, 1, 4, 5, 6]
beforeEach {
try? manager.moveItem(fromIndex: 1, toIndex: 3)
}
it("should move the item", closure: {
expect(manager.items).to(equal(afterMoving))
})
})
})
}
}
}
+28 -8
View File
@@ -9,6 +9,7 @@ SwiftAudio is an audio player written in Swift, making it simpler to work with a
## Example
To see the audio player in action clone the repo and run the example project!
To run the example project, clone the repo, and run `pod install` from the Example directory first.
## Requirements
@@ -27,12 +28,21 @@ pod 'SwiftAudio'
### AudioPlayer
```swift
let player = AudioPlayer()
let player = QueuedAudioPlayer()
let audioItem = DefaultAudioItem(audioUrl: "someUrl", sourceType: .stream)
player.add(item: audioItem)
```
The player will load the track and start playing when ready. To disable this behaviour use `add(item:playWhenReady:)` and pass in `false`. This is `true` by default. To get notified of events during playback and loading, implement `AudioPlayerDelegate` and the player will notify you with changes.
If you want a simpler audio player without queue functionality, use:
```swift
let player = SimpleAudioPlayer()
let audioItem = DefaultAudioItem(audioUrl: "someUrl", sourceType: .stream)
player.load(item: audioItem)
```
The player will load the track and start playing when ready. To disable this behaviour use `load(item:playWhenReady:)` and pass in `false`. This is `true` by default. To get notified of events during playback and loading, implement `AudioPlayerDelegate` and the player will notify you with changes.
**NOTE**: Do not use `AudioPlayer` directly. Use one of the above types.
#### States
The `AudioPlayer` has a `state` property, to make it easier to determine appropriate actions. The different states:
@@ -42,6 +52,17 @@ The `AudioPlayer` has a `state` property, to make it easier to determine appropr
+ **playing**: The player is playing.
+ **paused**: The player is paused.
#### Queue
The `QueuedAudioPlayer` maintains a queue of audio tracks.
The arrangement of the tracks are: [Previous]-[Current]-[Next].
When a track is done playing, the player will load the next track and update the queue, as long as `automaticallyPlayNextSong` is `true` (This is by default).
Items can be added to the queue by calling `player.add(item:)` or `player.add(items:)`.
Use `removeItem(atIndex:)` and `moveItem(fromIndex:toIndex:)` to manipulate the queue.
The queue can be navigated by using `next()`, `previous()` and `jumpToItem(atIndex:)`
### Audio Session
Remember to activate an audio session with an appropriate category for your app. This can be done with `AudioSessionCategory`:
```swift
@@ -56,20 +77,22 @@ If you want audio to continue playing when the app is inactive, remember to acti
App Settings -> Capabilities -> Background Modes -> Check 'Audio, AirPlay, and Picture in Picture'.
### Now Playing Info
The `AudioPlayer` will automatically update the `MPNowPlayingInfoCenter` with artist, title, album, artwork, time etc. if the passed in `AudioItem` supports this.
The `AudioPlayer` will automatically update the `MPNowPlayingInfoCenter` with artist, title, album, artwork, time if the passed in `AudioItem` supports this.
If you need to set additional properties for some items use `AudioPlayer.add(property:)`. Available properties can be found in `NowPlayingInfoProperty`.
### Remote Commands
The player will handle remote commands received from `MPRemoteCommandCenter`'s shared instance, enabled by:
```swift
audioPlayer.enable(commands: [
audioPlayer.remoteCommands = [
.play,
.pause,
.skipForward(intervals: [30]),
.skipBackward(intervals: [30]),
])
]
```
These commands will be activated for each `AudioItem`. If you need some audio items to have different commands, implement `RemoteCommandable`. These commands will override the commands found in `AudioPlayer.remoteCommands` so make sure to supply all commands you need for that particular `AudioItem`.
**Remember** to go to App Settings -> Capabilites -> Background Modes -> Check 'Remote notifications'
@@ -83,9 +106,6 @@ Currently some configuration options are supported:
+ `volume`: The volume of the player. From 0.0 to 1.0.
+ `automaticallyUpdateNowPlayingInfo`: If you want to handle updating of the `MPNowPlayingInfoCenter` yourself, set this to `false`. Default is `true`.
## Plans
* Ability to queue items
## Author
Jørgen Henrichsen
+1 -1
View File
@@ -8,7 +8,7 @@
Pod::Spec.new do |s|
s.name = 'SwiftAudio'
s.version = '0.2.0'
s.version = '0.3.0'
s.summary = 'Easy audio streaming for iOS'
# This description is used to generate tags and improve search results.
+27
View File
@@ -0,0 +1,27 @@
//
// APError.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 25/03/2018.
//
import Foundation
public struct APError {
enum LoadError: Error {
case invalidSourceUrl(String)
}
enum PlaybackError: Error {
case noLoadedItem
}
enum QueueError: Error {
case noPreviousItem
case noNextItem
case invalidIndex(index: Int, message: String)
}
}
@@ -21,18 +21,6 @@ protocol AVPlayerWrapperDelegate: class {
}
public struct APError {
enum LoadError: Error {
case invalidSourceUrl(String)
}
enum PlaybackError: Error {
case noLoadedItem
}
}
class AVPlayerWrapper {
struct Constants {
@@ -215,7 +203,7 @@ class AVPlayerWrapper {
*/
func stop() {
try? pause()
reset()
reset(soft: false)
}
/**
@@ -265,7 +253,7 @@ class AVPlayerWrapper {
private func load(from url: URL, playWhenReady: Bool) {
reset()
reset(soft: true)
_playWhenReady = playWhenReady
// Set item
@@ -283,8 +271,10 @@ class AVPlayerWrapper {
/**
Reset to get ready for playing from a different source.
*/
private func reset() {
avPlayer.replaceCurrentItem(with: nil)
private func reset(soft: Bool) {
if !soft {
avPlayer.replaceCurrentItem(with: nil)
}
playerTimeObserver.unregisterForBoundaryTimeEvents()
playerItemNotificationObserver.stopObservingCurrentItem()
}
@@ -351,7 +341,6 @@ extension AVPlayerWrapper: AVPlayerItemNotificationObserverDelegate {
// MARK: - AVPlayerItemNotificationObserverDelegate
func itemDidPlayToEndTime() {
self.reset()
delegate?.AVWrapperItemDidComplete()
}
+3 -1
View File
@@ -7,7 +7,6 @@
import Foundation
public enum SourceType {
case stream
case file
@@ -25,6 +24,7 @@ public protocol AudioItem {
}
public struct DefaultAudioItem: AudioItem {
public var audioUrl: String
@@ -70,4 +70,6 @@ public struct DefaultAudioItem: AudioItem {
public func getArtwork(_ handler: @escaping (UIImage?) -> Void) {
handler(artwork)
}
}
+38 -11
View File
@@ -24,20 +24,33 @@ public protocol AudioPlayerDelegate: class {
}
public class AudioPlayer {
/**
The main AudioPlayer.
- warning: DO NOT USE THIS CLASS, use `SimpleAudioPlayer` or `QueuedAudioPlayer`
*/
public class AudioPlayer: AVPlayerWrapperDelegate {
let wrapper: AVPlayerWrapper
let nowPlayingInfoController: NowPlayingInfoController
let remoteCommandController: RemoteCommandController
var _currentItem: AudioItem?
public weak var delegate: AudioPlayerDelegate?
public var currentItem: AudioItem?
public let remoteCommandController: RemoteCommandController
public var currentItem: AudioItem? {
return _currentItem
}
/**
Set this to false to disable automatic updating of now playing info for control center and lock screen.
*/
public var automaticallyUpdateNowPlayingInfo: Bool = true
/**
Default remote commands to use for each playing item
*/
public var remoteCommands: [RemoteCommand] = []
// MARK: - Getters from AVPlayerWrapper
/**
@@ -133,8 +146,8 @@ public class AudioPlayer {
- parameter item: The AudioItem to load. The info given in this item is the one used for the InfoCenter.
- parameter playWhenReady: Immediately start playback when the item is ready. Default is `true`. If you disable this you have to call play() or togglePlay() when the `state` switches to `ready`.
*/
public func load(item: AudioItem, playWhenReady: Bool = true) throws {
func loadItem(_ item: AudioItem, playWhenReady: Bool = true) throws {
print("Loading: \(item)")
switch item.getSourceType() {
case .stream:
try self.wrapper.load(fromUrlString: item.getSourceUrl(), playWhenReady: playWhenReady)
@@ -142,9 +155,10 @@ public class AudioPlayer {
try self.wrapper.load(fromFilePath: item.getSourceUrl(), playWhenReady: playWhenReady)
}
self.currentItem = item
self._currentItem = item
set(item: item)
setArtwork(forItem: item)
enableRemoteCommands(forItem: item)
}
/**
@@ -189,10 +203,20 @@ public class AudioPlayer {
Set the remote commands that should be activated and handled.
Calling this will disable all earlier enabled commands, so include all commands you need.
*/
public func enableRemoteCommands(_ commands: [RemoteCommand]) {
func enableRemoteCommands(_ commands: [RemoteCommand]) {
self.remoteCommandController.enable(commands: commands)
}
func enableRemoteCommands(forItem item: AudioItem) {
if let item = item as? RemoteCommandable {
print("Enabling remote commands for item")
self.enableRemoteCommands(item.getCommands())
}
else {
self.enableRemoteCommands(remoteCommands)
}
}
// MARK: - NowPlayingInfo
/**
@@ -205,8 +229,13 @@ public class AudioPlayer {
updatePlaybackValues()
}
public func add(property: NowPlayingInfoKeyValue) {
self.nowPlayingInfoController.set(keyValue: property)
}
func set(item: AudioItem) {
guard automaticallyUpdateNowPlayingInfo else { return }
nowPlayingInfoController.set(keyValues: [
MediaItemProperty.artist(item.getArtist()),
MediaItemProperty.title(item.getTitle()),
@@ -238,12 +267,10 @@ public class AudioPlayer {
// MARK: - Private
private func reset() {
self.currentItem = nil
self._currentItem = nil
}
}
extension AudioPlayer: AVPlayerWrapperDelegate {
// MARK: - AVPlayerWrapperDelegate
func AVWrapper(didChangeState state: AVPlayerWrapperState) {
updatePlaybackValues()
+171
View File
@@ -0,0 +1,171 @@
//
// QueueManager.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 24/03/2018.
//
import Foundation
class QueueManager<T> {
private var _items: [T] = []
/**
All items held by the queue.
*/
public var items: [T] {
return _items
}
public var nextItems: [T]? {
return Array(_items[_currentIndex + 1..<items.count])
}
public var previousItems: [T] {
return Array(_items[0..<_currentIndex])
}
private var _currentIndex: Int = 0
/**
The index of the current item.
Will be populated event though there is no current item (When the queue is empty).
*/
public var currentIndex: Int {
return _currentIndex
}
/**
The current item for the queue.
*/
public var current: T? {
if _items.count > _currentIndex {
return _items[_currentIndex]
}
return nil
}
/**
Add a single item to the queue.
- parameter item: The `AudioItem` to be added.
*/
public func addItem(_ item: T) {
_items.append(item)
}
/**
Add an array of items to the queue.
- parameter items: The `AudioItem`s to be added.
*/
public func addItems(_ items: [T]) {
_items.append(contentsOf: items)
}
/**
Get the next item in the queue, if there are any.
Will update the current item.
- throws: `APError.QueueError`
- returns: The next item.
*/
@discardableResult
public func next() throws -> T {
let nextIndex = _currentIndex + 1
if _items.count > nextIndex {
_currentIndex = nextIndex
return _items[nextIndex]
}
else {
throw APError.QueueError.noNextItem
}
}
/**
Get the previous item in the queue, if there are any.
Will update the current item.
- throws: `APError.QueueError`
- returns: The previous item.
*/
@discardableResult
public func previous() throws -> T {
let previousIndex = _currentIndex - 1
if previousIndex >= 0 {
_currentIndex = previousIndex
return _items[previousIndex]
}
else {
throw APError.QueueError.noPreviousItem
}
}
/**
Jump to a position in the queue.
Will update the current item.
- parameter index: The index to jump to.
- throws: `APError.QueueError`
- returns: The item at the index.
*/
@discardableResult
func jump(to index: Int) throws -> T {
guard index != currentIndex else {
throw APError.QueueError.invalidIndex(index: index, message: "Cannot jump to the current item")
}
guard index >= 0 && items.count > index else {
throw APError.QueueError.invalidIndex(index: index, message: "The jump index has to be positive and smaller thant the count of current items (\(items.count))")
}
_currentIndex = index
return _items[index]
}
/**
Move an item in the queue.
- parameter fromIndex: The index of the item to be moved.
- parameter toIndex: The index to move the item to.
- throws: `APError.QueueError`
*/
func moveItem(fromIndex: Int, toIndex: Int) throws {
guard fromIndex != _currentIndex else {
throw APError.QueueError.invalidIndex(index: fromIndex, message: "The fromIndex cannot be equal to the current index.")
}
guard fromIndex >= 0 && fromIndex < _items.count else {
throw APError.QueueError.invalidIndex(index: fromIndex, message: "The fromIndex has to be positive and smaller than the count of current items (\(items.count)).")
}
guard toIndex >= 0 && toIndex < _items.count else {
throw APError.QueueError.invalidIndex(index: toIndex, message: "The toIndex has to be positive and smaller than the count of current items (\(items.count)).")
}
_items.insert(_items.remove(at: fromIndex), at: toIndex)
}
/**
Remove an item.
- parameter index: The index of the item to remove.
- throws: APError.QueueError
- returns: The removed item.
*/
@discardableResult
public func remove(atIndex index: Int) throws -> T {
guard index != _currentIndex else {
throw APError.QueueError.invalidIndex(index: index, message: "Cannot remove the current item!")
}
guard index >= 0 && _items.count > index else {
throw APError.QueueError.invalidIndex(index: index, message: "Index for removal has to be postivie and smaller than the count of current items (\(items.count)).")
}
return _items.remove(at: index)
}
}
@@ -0,0 +1,88 @@
//
// QueuedAudioPlayer.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 24/03/2018.
//
import Foundation
/**
An audio player that can keep track of a queue of AudioItems.
*/
public class QueuedAudioPlayer: AudioPlayer {
let queueManager: QueueManager = QueueManager<AudioItem>()
/**
Set wether the player should automatically play the next song when a song is finished.
Default is `true`.
*/
public var automaticallyPlayNextSong: Bool = true
public override var currentItem: AudioItem? {
return queueManager.current
}
public var previousItems: [AudioItem]? {
return queueManager.previousItems
}
public var nextItems: [AudioItem]? {
return queueManager.nextItems
}
public func add(item: AudioItem, playWhenReady: Bool = true) throws {
if currentItem == nil {
queueManager.addItem(item)
try self.loadItem(item, playWhenReady: playWhenReady)
}
else {
queueManager.addItem(item)
}
}
public func add(items: [AudioItem], playWhenReady: Bool = true) throws {
if currentItem == nil {
queueManager.addItems(items)
try self.loadItem(currentItem!, playWhenReady: playWhenReady)
}
else {
queueManager.addItems(items)
}
}
public func next() throws {
let nextItem = try queueManager.next()
try self.loadItem(nextItem, playWhenReady: true)
}
public func previous() throws {
let previousItem = try queueManager.previous()
try self.loadItem(previousItem, playWhenReady: true)
}
public func removeItem(atIndex index: Int) throws {
try queueManager.remove(atIndex: index)
}
public func jumpToItem(atIndex index: Int, playWhenReady: Bool = true) throws {
let item = try queueManager.jump(to: index)
try self.loadItem(item, playWhenReady: playWhenReady)
}
func moveItem(fromIndex: Int, toIndex: Int) throws {
try queueManager.moveItem(fromIndex: fromIndex, toIndex: toIndex)
}
// MARK: - AVPlayerWrapperDelegate
override func AVWrapperItemDidComplete() {
super.AVWrapperItemDidComplete()
if automaticallyPlayNextSong {
try? self.next()
}
}
}
@@ -8,6 +8,9 @@
import Foundation
import MediaPlayer
public protocol RemoteCommandable {
func getCommands() -> [RemoteCommand]
}
public class RemoteCommandController {
@@ -36,11 +39,13 @@ public class RemoteCommandController {
}
private func enableCommand<Command: RemoteCommandProtocol>(_ command: Command) {
center[keyPath: command.commandKeyPath].isEnabled = true
center[keyPath: command.commandKeyPath].addTarget(handler: self[keyPath: command.handlerKeyPath])
}
private func disableCommand<Command: RemoteCommandProtocol>(_ command: Command) {
center[keyPath: command.commandKeyPath].removeTarget(self)
center[keyPath: command.commandKeyPath].isEnabled = false
center[keyPath: command.commandKeyPath].removeTarget(self[keyPath: command.handlerKeyPath])
}
private func enable(command: RemoteCommand) {
@@ -0,0 +1,25 @@
//
// SimpleAudioPlayer.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 24/03/2018.
//
import Foundation
/**
A simple audio player that keeps on item at a time.
*/
public class SimpleAudioPlayer: AudioPlayer {
/**
Load an AudioItem into the manager.
- parameter item: The AudioItem to load. The info given in this item is the one used for the InfoCenter.
- parameter playWhenReady: Immediately start playback when the item is ready. Default is `true`. If you disable this you have to call play() or togglePlay() when the `state` switches to `ready`.
*/
public func load(item: AudioItem, playWhenReady: Bool = true) throws {
try self.loadItem(item, playWhenReady: playWhenReady)
}
}