Add CarPlay support to Swift Radio
In this tutorial, we will add CarPlay support to the open-source radio app SwiftRadio and test it using the Simulator / CarPlay Simulator.
Setting up the project
First let’s start by cloning the project, or downloading it directly from GitHub:
git clone https://github.com/analogcode/Swift-Radio-Pro
After running the project using the Simulator, we want to make sure that we have the CarPlay menu available under Hardware > External Displays > CarPlay:
If you can’t find the CarPlay menu, open the Terminal and run the following command:
defaults write com.apple.iphonesimulator CarPlay -bool YES
Now we need to add an entitlement file to the app and add the two following entry:
com.apple.developer.playable-content
as a Boolean/YES
value.
To generate the file automatically we can toggle the PushNotification in our app target > capabilities and turn it off later:
The SwiftRadio.entitlements
file should look like this:
When we run the app again, we’ll be able to see our CarPlay app:
Add playableContentManager
Next, let’s jump to the project and start adding some code.
First, we need to import the MediaPlayer framework in our AppDelegate class, and add a new property playableContentManager
:
// AppDelegate.swift
import UIKit
import MediaPlayer
import FRadioPlayer
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
// CarPlay
var playableContentManager: MPPlayableContentManager?
// ...
}
Add delegates and StationsManager
observer
To separate CarPlay logic, we can create a new file extension for the AppDelegate
class, and name it AppDelegate+CarPlay.swift
.
First, let’s add a setup function to initiate our playableContentManager
property, set both the delegate
and dataSource
properties to self
(AppDelegate
), and also add it as a StationsManager
observer:
// AppDelegate+CarPlay.swift
import Foundation
import MediaPlayer
extension AppDelegate {
func setupCarPlay() {
playableContentManager = MPPlayableContentManager.shared()
playableContentManager?.delegate = self
playableContentManager?.dataSource = self
StationsManager.shared.addObserver(self)
}
}
Next, we add an extension for the delegate
protocol:
// AppDelegate+CarPlay.swift
extension AppDelegate: MPPlayableContentDelegate {
func playableContentManager(_ contentManager: MPPlayableContentManager, initiatePlaybackOfContentItemAt indexPath: IndexPath, completionHandler: @escaping (Error?) -> Void) {
completionHandler(nil)
}
func beginLoadingChildItems(at indexPath: IndexPath, completionHandler: @escaping (Error?) -> Void) {
completionHandler(nil)
}
}
Then, add the MPPlayableContentDataSource
required function into another extension:
// AppDelegate+CarPlay.swift
extension AppDelegate: MPPlayableContentDataSource {
func numberOfChildItems(at indexPath: IndexPath) -> Int {
return 0
}
func contentItem(at indexPath: IndexPath) -> MPContentItem? {
return nil
}
}
Finally, we add an extension for StationsManagerObserver
:
// AppDelegate+CarPlay.swift
extension AppDelegate: StationsManagerObserver {
func stationsManager(_ manager: StationsManager, stationsDidUpdate stations: [RadioStation]) {
// code here
}
func stationsManager(_ manager: StationsManager, stationDidChange station: RadioStation?) {
// code here
}
}
Implement playableContentManager
delegate
Let’s start by implementing initiatePlaybackOfContentItemAt
delegate method, this function will be triggered when the user selects a station from the CarPlay list:
// MPPlayableContentDelegate
func playableContentManager(_ contentManager: MPPlayableContentManager, initiatePlaybackOfContentItemAt indexPath: IndexPath, completionHandler: @escaping (Error?) -> Void) {
DispatchQueue.main.async {
// Check if the user tapped the second section (first section will be reserved for the list tab name)
if indexPath.count == 2 {
// Getting the selected station
let station = StationsManager.shared.stations[indexPath[1]]
// Setting the station, this will trigger the player playback
StationsManager.shared.set(station: station)
// Tell the `contentManager` the playing identifier, we are using the station name as an ID here.
contentManager.nowPlayingIdentifiers = [station.name]
}
completionHandler(nil)
}
}
The next function in the delegate section is beginLoadingChildItems
:
// MPPlayableContentDelegate
func beginLoadingChildItems(at indexPath: IndexPath, completionHandler: @escaping (Error?) -> Void) {
// We call the stations' manager fetch function to get the station list
StationsManager.shared.fetch {
completionHandler(nil)
}
}
Implement playableContentManager
datasource:
For the datasource
we can create a single tab to represent the stations’ list, to do so we need to add the following UIBrowsableContentSupportsSectionedBrowsing
Boolean / YES
value to our app’s info.plist
:
Now, let’s add the data needed in the datasource
, for numberOfChildItems
, the number of tabs is 1, and the number of items is the StationsManager.shared.stations
count
:
// MPPlayableContentDataSource
func numberOfChildItems(at indexPath: IndexPath) -> Int {
if indexPath.indices.count == 0 {
return 1
}
return StationsManager.shared.stations.count
}
In the contentItem(at indexPath: IndexPath) -> MPContentItem?
function we create an item of type MPContentItem
for each section (tab and list), for the tab we add:
// MPPlayableContentDataSource
func contentItem(at indexPath: IndexPath) -> MPContentItem? {
if indexPath.count == 1 {
// Tab section
let item = MPContentItem(identifier: "Stations")
item.title = "Stations"
item.isContainer = true
item.isPlayable = false
item.artwork = MPMediaItemArtwork(boundsSize: #imageLiteral(resourceName: "carPlayTab").size, requestHandler: { _ -> UIImage in
return #imageLiteral(resourceName: "carPlayTab")
})
return item
}
return nil
}
You can download the carPlayTab
icon images from here and add them to the project’s Images.xcassets.
For the stations’ list:
// MPPlayableContentDataSource
func contentItem(at indexPath: IndexPath) -> MPContentItem? {
if indexPath.count == 1 {
// Tab section
// ... Code added in the previous section
} else if indexPath.count == 2, indexPath.item < StationsManager.shared.stations.count {
// Stations section
let station = StationsManager.shared.stations[indexPath.item]
let item = MPContentItem(identifier: "\(station.name)")
item.title = station.name
item.subtitle = station.desc
item.isPlayable = true
item.isStreamingContent = true
station.getImage { image in
item.artwork = MPMediaItemArtwork(boundsSize: image.size) { _ -> UIImage in
return image
}
}
return item
} else {
return nil
}
}
Implement StaionsManager
observers
For the StationsManagerObserver
we need to implement both functions:
// StationsManagerObserver
func stationsManager(_ manager: StationsManager, stationsDidUpdate stations: [RadioStation]) {
// Reload playableContentManager when there is a stations update
playableContentManager?.reloadData()
}
// StationsManagerObserver
func stationsManager(_ manager: StationsManager, stationDidChange station: RadioStation?) {
// Check if the new station is not nil and update the `nowPlayingIdentifiers`
guard let station = station else {
playableContentManager?.nowPlayingIdentifiers = []
return
}
playableContentManager?.nowPlayingIdentifiers = [station.name]
}
Run using the iOS simulator
Finally, we need to call our setup
function in the AppDelegate
:
// AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// ...
setupCarPlay()
return true
}
And run our app again, and we’ll be able to see the list in our CarPlay display:
There is a small bug on the simulator causing the pause / play button to show out of sync in the first launch.
Testing on a device with the CarPlay Simulator
To run the app on an actual device or publish it to the App Store, you need to ask for a CarPlay entitlement from Apple using this form.
Thanks to @urayoanm for sharing his experience on this Github issue.
Once your request gets approved you will receive an email saying that the CarPlay entitlement has been added to your account, and you will be able to generate an explicit provisioning file with the CarPlay entitlement for your app.
Once this is done, you can add it to the project, and run the app on your device:
Now, we need to get the CarPlay simulator from Apple’s developer website by downloading the Additional Tools for Xcode.
In the Hardware
folder, we can launch the CarPlay Simulator
app, and connect our iPhone, we’ll get a full CarPlay experience on our Mac:
@end
All this code has been added to the SwiftRadio project in Xcode SwiftRadio-CarPlay target.
More infos about CarPlay: