Rock Your App’s Playback Experience with Now Playing in iOS

Mayank Kumar Gupta
6 min readMay 1, 2023

--

Media Playback Controls using LockScreen and Control Centre in iOS using Swift

If you’ve ever used an iPhone or iPad to listen to music, you’ve probably seen the Now Playing screen. This screen shows you the title and artist of the currently playing song, as well as controls for pausing, skipping, and adjusting the volume. But there’s more to Now Playing than just that — it’s actually a powerful tool for controlling your music playback, and it’s also an important part of the iOS ecosystem.

In this post, we’ll take a closer look at Now Playing in iOS, exploring its features and capabilities, as well as some of the ways it can be used in your apps.

Basics of Now Playing

Let’s start with the basics. When you’re playing music on your iPhone or iPad, you can access the Now Playing screen by swiping down from the top-right corner of the screen to access the Control Center, which includes a Now Playing widget that shows you the current track. Alternatively, you can see the NowPlaying in Lock screen.

Lock screen media controls (Now Playing)

Okay, now we understand the Now Playing, how do we implement it? Well, we can follow these steps to implement it in your own apps.

  1. Setting the AvAudioSession.
  2. Enabling Background modes for audio.
  3. Setting up the remote commands.
  4. Configuring the NowPlaying Controls and displaying information.
  5. Error Handling and interruption Handling.

Setting the AvAudioSession

I am not going in much detail in setting up the Audio session, you can refer to this documentation it very well explains the AVAudioSession.

// Get the singleton instance.
let audioSession = AVAudioSession.sharedInstance()
do {
// Set the audio session category, mode, and options.
try audioSession.setCategory(.playback, mode: .moviePlayback, options: [])
try audioSession.setActive(true)
} catch {
print("Failed to set audio session category.")
}

Enabling Background modes for audio

Select your app’s target in Xcode and select the Capabilities tab. Under the Capabilities tab, set the Background Modes switch to ON and select the “Audio, AirPlay, and Picture in Picture” option under the list of available modes.

Source — Apple documentation (Link)

Setting up the remote commands

Each remote command is a MPRemoteCommand from the Media Player framework that responds to remote command events.

Examples of MPRemoteCommand — playCommand, pauseCommand, nextTrackCommand, etc. (Reference)

Now, we need to register for different remote commands and respond to each command accordingly. For this we can use the shared instance of MPRemoteCommandCenter and handle events.

I prefer using Enums because they are value types and offer more functionality in Swift, making them a powerful tool for this purpose. We can further use CaseIterable protocol to encapsulate various type of remote commands and also handle the remote command registering within enum itself.

Something like this ..

enum NowPlayableCommand: CaseIterable {

case play, pause, togglePlayPause,
nextTrack, previousTrack,
changePlaybackRate, changePlaybackPosition,
skipForward, skipBackward,
seekForward, seekBackward
}

// MARK:- MPRemoteCommand

extension NowPlayableCommand {
var remoteCommand: MPRemoteCommand {

let commandCenter = MPRemoteCommandCenter.shared()

switch self {
case .play:
return commandCenter.playCommand
case .pause:
return commandCenter.pauseCommand
case .togglePlayPause:
return commandCenter.togglePlayPauseCommand
case .nextTrack:
return commandCenter.nextTrackCommand
case .previousTrack:
return commandCenter.previousTrackCommand
case .changePlaybackRate:
return commandCenter.changePlaybackRateCommand
case .changePlaybackPosition:
return commandCenter.changePlaybackPositionCommand
case .skipForward:
return commandCenter.skipForwardCommand
case .skipBackward:
return commandCenter.skipBackwardCommand

case .seekForward:
return commandCenter.seekForwardCommand
case .seekBackward:
return commandCenter.seekBackwardCommand
}
}
// Adding Handler and accepting an escaping closure for event handling for a praticular remote command
func addHandler(remoteCommandHandler: @escaping (NowPlayableCommand, MPRemoteCommandEvent)->(MPRemoteCommandHandlerStatus)) {

switch self {
case .skipBackward:
MPRemoteCommandCenter.shared().skipBackwardCommand.preferredIntervals = [10.0]

case .skipForward:
MPRemoteCommandCenter.shared().skipForwardCommand.preferredIntervals = [10.0]

default:
break
}
self.remoteCommand.addTarget { event in
remoteCommandHandler(self,event)
}
}
}

Configuring the NowPlaying Controls and displaying information

Now, we will create a protocol that our video player class can conform to, something like this..

protocol NowPlayable {
var supportedNowPlayableCommands: [NowPlayableCommand] { get }

func configureRemoteCommands(remoteCommandHandler: @escaping (NowPlayableCommand, MPRemoteCommandEvent)->(MPRemoteCommandHandlerStatus))
func handleRemoteCommand(for type: NowPlayableCommand, with event: MPRemoteCommandEvent)-> MPRemoteCommandHandlerStatus

func handleInterruption(for type: NowPlayableInterruption)

func handleNowPlayingItemChange()
func handleNowPlayingItemPlaybackChange()

func addNowPlayingObservers()

func setNowPlayingInfo(with metadata: NowPlayableStaticMetadata)
func setNowPlayingPlaybackInfo(with metadata: NowPlayableDynamicMetaData)

func resetNowPlaying()
}

Supported now playable commands are the remote commands you wish to show in the Now playing and can respond to actions.

Function setNowPlayingInfo is used to set the nowplaying information.
Here we use the shared instance of MPNowPlayingInfoCenter and pass a dictionary.

func setNowPlayingInfo(with metadata: NowPlayableStaticMetadata) {
let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default()

var nowPlayingInfo = [String:Any]()

nowPlayingInfo[MPNowPlayingInfoPropertyIsLiveStream] = metadata.isLive
nowPlayingInfo[MPMediaItemPropertyTitle] = metadata.title
nowPlayingInfo[MPMediaItemPropertyArtist] = metadata.artist
nowPlayingInfo[MPMediaItemPropertyArtwork] = metadata.artworkImage
nowPlayingInfo[MPMediaItemPropertyAlbumArtist] = metadata.albumArtist
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = metadata.albumTitle

nowPlayingInfoCenter.nowPlayingInfo = nowPlayingInfo
}

MPNowPlayingInfoCenter shared instance has nowPlayingInfo dictionary. We can create and populate metadata to a temp dictionary and set the nowPlayingInfo dictionary from that temp.

Here I am setting the static metadata like artwork, title, etc. For setting up the dynamic metadata like player rate, seek position, etc we can use other dictionary keys. For more keys, refer here.

We can call the setNowPlayingInfo whenever new playback item is played.
And we will also see we need to call setNowPlayingInfo multiple times for syncing the dynamic player metadata like player rate, seek position, etc with nowPlaying information.

Now, the function configureRemoteCommands() enable us to register for remote commands and can be implemented something like this.

func configureRemoteCommands(remoteCommandHandler: @escaping  (NowPlayableCommand, MPRemoteCommandEvent)->(MPRemoteCommandHandlerStatus)) {

guard supportedNowPlayableCommands.count > 1 else {
assertionFailure("Fatal error, atleast one remote command needs to be registered")
return
}

supportedNowPlayableCommands.forEach { nowPlayableCommand in
nowPlayableCommand.removeHandler()
nowPlayableCommand.addHandler(remoteCommandHandler: remoteCommandHandler)
}

}

I am using escaping closure to pass the control or the handling of the remote command events up to the Video player class where your video player class conforms to NowPlaying. This way we have our code clean and defined responsibilities.

At the Video player class, where it conforms to NowPlayable protocol, we can implement the

func handleRemoteCommand(for type: NowPlayableCommand,
with event: MPRemoteCommandEvent)

Here, we can compare which type of NowPlayableCommand is there and based on that we can respond to player changes. For example,

switch type {
case .play :
player.resume()

case .pause:
player.pause()

Note that we don’t need to manually call this function. Since we registered for remote commands(see the NowPlayableCommand Enum) this function will be called automatically. We just need to respond to changes from remote commands.

Finally, observe the video player using Observers and then at each player event change(like seek, pause, etc), we need to call setNowPlayingInfo(with the dynamic Metadata) which will further create a dictionary and pass it to MPNowPlayingInfoCenter shared instance nowPlaying dictionary. (Refer to above implementation of the function)

Final Notes

Now Playing is a powerful tool in iOS, providing users with a convenient way to control their music playback. By integrating Now Playing into your app, you can provide a more seamless and integrated music playback experience for your users. This is exactly what I did when I worked on the Cricbuzz app, which has more than 100 million downloads. By adding Now Playing, we allowed users to have more control from the lockscreen and the feedback we received was overwhelmingly positive. Users loved being able to seamlessly control their music playback while keeping up with the latest cricket scores and news.

If you’re working on an app that involves music playback, or just want to provide a better user experience for your users, consider integrating Now Playing into your app. It’s a powerful tool that can make a big difference in how your users interact with your app.

References to help further:

  1. https://medium.com/@varundudeja/showing-media-player-system-controls-on-notification-screen-in-ios-swift-4e27fbf73575
  2. https://developer.apple.com/documentation/mediaplayer/mpnowplayinginfocenter

Hope you guys find this useful. If you liked this or have some feedback or follow-up questions please comment below or reach out to me on Linkedin.

Thanks for Reading!

--

--

Mayank Kumar Gupta
0 Followers

The goal isn’t to live forever, the goal is to create something that will.