There are already a few tutorials about building building an app that plays music on iOS. Why am I writing a whole new post about it then? Most of the existing tutorials focus on the really basic concepts about a music player app. This is going to be different. We will set up a full fledged music player with it's own music library, ability to create custom playlists, streaming songs online etc.

This is not a post about accessing the existing music on user's device, so if you are looking for that, feel free to jump back to Google and resume your search. This also isn't about the low level use of MediaPlayer framework. We will be using a library to handle this.

Building the Music Player

The first thing we are going to tackle in the post is creating the Music Player. We are using StreamingKit, so we have most of our work cut for us, but it still requires a bit of handling logic around it to implement all the features that a Music Player of this generation needs.

Initialization

To play music with StreamingKit, we need a STKAudioPlayer. Check out all the options for the audio player in the source.

var audioPlayer: STKAudioPlayer!

// MARK: Init
override init() {  
    super.init()
    resetAudioPlayer()
}

private func resetAudioPlayer() {  
    var options = STKAudioPlayerOptions()
    options.flushQueueOnSeek = true
    options.enableVolumeMixer = true
    audioPlayer = STKAudioPlayer(options: options)

    // Set up audio player
    audioPlayer.meteringEnabled = true
    audioPlayer.volume = 1
    audioPlayer.delegate = self
}

Handling a queue

Next thing a music player needs is to handle a queue of music files. You could get away with handling just one file per player and simplify the logic, but since we are focusing on a full fledged music player, we will see how to handle the queue. We create a class Track to handle the properties of each queue element. I will be skipping the implementation since it would be pretty specific depending on the app you are working on, but the main properties it should be supporting are identifier and a url (the URL for the music file, local/streaming).

private(set) var queue: [AlbumFile]

// MARK: Public API
internal func playWithQueue(queue: [AlbumFile], index: Int = 0) {  
    guard index >= 0 && index < queue.count else {
        return
    }
    self.queue = queue
    audioPlayer.clearQueue()
    if let url = queue[index].url {
        audioPlayer.playURL(url)
    }
    for i in 1 ..< queue.count {
        audioPlayer.queueURL(queue[Int((index + i) % queue.count)].url ?? NSURL())
    }
    currentIndex = index
    loop = false
}

Controlling the playback

While we are adding a public API to the Music Player, let's also add simple helper functions to control the playback.

internal func stop() {  
    audioPlayer.stop()
    queue = []
    currentIndex = -1
}

internal func play(file: AlbumFile) {  
    audioPlayer.playURL(file.url ?? NSURL())
}

internal func next() {  
    guard queue.count > 0 else {
        return
    }
    currentIndex = (currentIndex + 1) % queue.count
    playWithQueue(queue, index: currentIndex)
}

internal func prev() {  
    currentIndex = max(0, currentIndex - 1)
    playWithQueue(queue, index: currentIndex)
}

Handling audio player events

STKAudioPlayer communicates several events during playback to its delegate like when an item starts playing, finishes buffering, finishes playing or encounters an error. Let's see how we can handle these events to perform meaningful operations in the music player.

  • A track starts playing: Update the local state to be communicated later to the UI
  • A track finishes buffering: Update info in the now playing center (check out the next post to find out how to interface with the now playing center)
  • Audio player state changes: Update the local state to be communicated later to the UI
  • A track finishes playing: Switch to the next track
  • Unexpected error occurs: Reset and try to play again or inform the user about the error depending on your app requirements.
// MARK: Audio Player Delegate
func audioPlayer(audioPlayer: STKAudioPlayer, didStartPlayingQueueItemId queueItemId: NSObject) {  
    if let index = (queue.indexOf { $0.url == queueItemId }) {
        currentIndex = index
    }
}

func audioPlayer(audioPlayer: STKAudioPlayer, didFinishBufferingSourceWithQueueItemId queueItemId: NSObject) {  
    updateNowPlayingInfoCenter()
}

func audioPlayer(audioPlayer: STKAudioPlayer, stateChanged state: STKAudioPlayerState, previousState: STKAudioPlayerState) {  
    self.state.value = state
    if state != .Stopped && state != .Error && state != .Disposed {
    }
    updateNowPlayingInfoCenter()
}

func audioPlayer(audioPlayer: STKAudioPlayer, didFinishPlayingQueueItemId queueItemId: NSObject, withReason stopReason: STKAudioPlayerStopReason, andProgress progress: Double, andDuration duration: Double) {  
    if let index = (queue.indexOf { $0.url == audioPlayer.currentlyPlayingQueueItemId() }) {
        currentIndex = index
    }

    if stopReason == .Eof {
        next()
    } else if stopReason == .Error {
        stop()
        resetAudioPlayer()
    }
}

func audioPlayer(audioPlayer: STKAudioPlayer, unexpectedError errorCode: STKAudioPlayerErrorCode) {  
    print("Error when playing music \(errorCode)")
    resetAudioPlayer()
    playWithQueue(queue, index: currentIndex)
}

Supporting background audio

In order to play music while your app is in the background, you must do some additional set up.

  • Update your Info.plist file to include the audio in the UIBackgroundModes array.
  • Initialize your AudioSession somewhere near the beginning of your app. A good point to do this is in AppDelegate.application(application:, didFinishLaunchingWithOptions:).
func setupMusicPlayer() {  
    do {
        try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
    } catch {
        print("Failed to setup audio")
    }
}

Playing music

After we wrap everything up in a class (named MusicPlayer for the tutorial), we are good to go. Since you would normally be supporting just one instance of the music player in your app, it's a good idea to create a singleton and use it all around your app. Here's how you can set it up.

class MusicPlayer: NSObject, STKAudioPlayerDelegate {  
    private static var instance = MusicPlayer()
    // …

    // MARK: Static
    internal static func sharedInstance() -> MusicPlayer {
        return instance
    }
}

Here's how you can use it from other parts of the UI.

MusicPlayer.sharedInstance().playWithQueue(tracks, index: indexPath.row)