Add CarPlay support to SwiftRadio [archived]

Archived guide: This article describes the older MPPlayableContentManager integration. The code and screenshots belong to that version of Swift Radio. For the scene-based setup used by the current project, read Adding CarPlay support to Swift Radio (iOS 14+).

The first CarPlay integration in Swift Radio used MPPlayableContentManager. The app supplied a tree of stations and responded to playback requests; CarPlay built the browsing interface from that data.

This guide walks through that setup: enabling the simulator display, exposing the station list, and keeping the selected station in sync. It remains here for anyone maintaining a fork that uses the older API.

Setting up the project

  1. Start with a Swift Radio checkout that still uses MPPlayableContentManager. The project lives in this repository:

    git clone https://github.com/analogcode/Swift-Radio-Pro
    

    The default branch now uses CarPlay scenes. These examples are for an older checkout or an existing fork with the playable-content integration.

  2. Run the app in the iOS Simulator. In the version shown here, the CarPlay display is under Hardware > External Displays > CarPlay.

    The iOS Simulator hardware menu with the CarPlay option highlighted

    Older Simulator versions could expose the menu with this setting:

    defaults write com.apple.iphonesimulator CarPlay -bool YES
    
  3. Add the CarPlay entitlement so iOS will expose playable content.

    Xcode capabilities pane showing the entitlement toggle

    In the Xcode version shown here, toggling a capability such as Push Notifications created the SwiftRadio.entitlements file. The CarPlay key then had to be added manually: com.apple.developer.playable-content = YES. Push Notifications itself isn't needed for this integration.

    The generated entitlements file containing the playable content key
  4. Relaunch the app. You should now see the CarPlay shell.

    Empty CarPlay template showing SwiftRadio without content

Create the content manager

Start by declaring a shared MPPlayableContentManager on the app delegate:

// AppDelegate.swift
import UIKit
import MediaPlayer
import FRadioPlayer

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  var window: UIWindow?

  // CarPlay
  var playableContentManager: MPPlayableContentManager?

  // ...
}

Supply stations and handle playback

Keep CarPlay logic in an extension so it stays out of the main app bootstrap:

// AppDelegate+CarPlay.swift
import Foundation
import MediaPlayer

extension AppDelegate {
  func setupCarPlay() {
    playableContentManager = MPPlayableContentManager.shared()

    playableContentManager?.delegate = self
    playableContentManager?.dataSource = self

    StationsManager.shared.addObserver(self)
  }
}

The playback delegate receives an index path when a listener selects a row. Use it to find the station, select it through StationsManager, and report completion:

// AppDelegate+CarPlay.swift
extension AppDelegate: MPPlayableContentDelegate {
  func playableContentManager(
    _ contentManager: MPPlayableContentManager,
    initiatePlaybackOfContentItemAt indexPath: IndexPath,
    completionHandler: @escaping (Error?) -> Void
  ) {
    DispatchQueue.main.async {
      if indexPath.count == 2 {
        let station = StationsManager.shared.stations[indexPath[1]]
        StationsManager.shared.set(station: station)
        contentManager.nowPlayingIdentifiers = [station.name]
      }

      completionHandler(nil)
    }
  }

  func beginLoadingChildItems(
    at indexPath: IndexPath,
    completionHandler: @escaping (Error?) -> Void
  ) {
    StationsManager.shared.fetch {
      completionHandler(nil)
    }
  }
}

The data source describes two levels: a Stations container at the root, then one playable item per station. CarPlay uses the item's identifier to track what is playing.

// AppDelegate+CarPlay.swift
extension AppDelegate: MPPlayableContentDataSource {
  func numberOfChildItems(at indexPath: IndexPath) -> Int {
    if indexPath.count == 0 {
      return 1
    }

    return StationsManager.shared.stations.count
  }

  func contentItem(at indexPath: IndexPath) -> MPContentItem? {
    if indexPath.count == 1 {
      let item = MPContentItem(identifier: "Stations")
      item.title = "Stations"
      item.isContainer = true
      item.isPlayable = false
      return item
    }

    guard
      indexPath.count == 2,
      indexPath.item < StationsManager.shared.stations.count
    else {
      return nil
    }

    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) { _ in image }
    }

    return item
  }
}

The manager observer handles changes that originate elsewhere in the app. Reload the CarPlay list when stations arrive, and update nowPlayingIdentifiers when the selected station changes:

// AppDelegate+CarPlay.swift
extension AppDelegate: StationsManagerObserver {
  func stationsManager(
    _ manager: StationsManager,
    stationsDidUpdate stations: [RadioStation]
  ) {
    playableContentManager?.reloadData()
  }

  func stationsManager(
    _ manager: StationsManager,
    stationDidChange station: RadioStation?
  ) {
    guard let station else {
      playableContentManager?.nowPlayingIdentifiers = []
      return
    }

    playableContentManager?.nowPlayingIdentifiers = [station.name]
  }
}

Add UIBrowsableContentSupportsSectionedBrowsing = YES to Info.plist to enable sectioned browsing for this integration.

Info.plist showing the CarPlay sectioned browsing setting

Run it in the Simulator

Register CarPlay when the app launches:

// AppDelegate.swift
func application(
  _ application: UIApplication,
  didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
  // ...
  setupCarPlay()
  return true
}

The CarPlay template now lists stations and highlights the active stream.

CarPlay showing the SwiftRadio stations list CarPlay now playing screen for SwiftRadio

In the Simulator version used for this guide, the play/pause button could appear out of sync on first launch. Toggling playback once reset it.

Test on real hardware with CarPlay Simulator

  1. Request the CarPlay entitlement from Apple using the CarPlay contact form. Once approved, generate a provisioning profile that includes it.

    Xcode provisioning profile with the CarPlay entitlement
  2. Download the Additional Tools for Xcode package from Apple’s developer downloads and launch the CarPlay Simulator app found in the Hardware folder.

    The CarPlay Simulator dashboard on macOS CarPlay Simulator showing SwiftRadio now playing controls
  3. Connect your iPhone and run the CarPlay-enabled Swift Radio target. Check that selecting a station starts playback and that the now playing screen shows the selected station.

Resources