Adding CarPlay support to Swift Radio (iOS 14+)

Four things stand between your app and a working CarPlay integration: an Info.plist update, two entitlement keys, a scene delegate, and a bit of state sync. This guide walks through each one.

1. Add CarPlay scenes to Info.plist

First step is updating your Info.plist to include CarPlay scenes. In SwiftRadio/Info-CarPlay.plist, you’ll add a UIApplicationSceneManifest that connects your app scene with the CarPlay scene delegate:

<key>UIApplicationSceneManifest</key>
<dict>
  <key>UIApplicationSupportsMultipleScenes</key>
  <true/>
  <key>UISceneConfigurations</key>
  <dict>
    <key>UIWindowSceneSessionRoleApplication</key>
    <array>
      <dict>
        <key>UISceneConfigurationName</key>
        <string>Default Configuration</string>
        <key>UISceneDelegateClassName</key>
        <string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
      </dict>
    </array>
    <key>CPTemplateApplicationSceneSessionRoleApplication</key>
    <array>
      <dict>
        <key>UISceneClassName</key>
        <string>CPTemplateApplicationScene</string>
        <key>UISceneConfigurationName</key>
        <string>CarPlay Configuration</string>
        <key>UISceneDelegateClassName</key>
        <string>$(PRODUCT_MODULE_NAME).CarPlaySceneDelegate</string>
      </dict>
    </array>
  </dict>
</dict>

You’ll also want to add these CarPlay-specific entries:

  • UIBrowsableContentSupportsSectionedBrowsing = true
  • UISupportedExternalAccessoryProtocols = ["com.apple.carplay"]
  • NSCarPlayAudioUsageDescription with a brief explanation of why your app needs audio access

2. Add the CarPlay entitlements

Next, make sure your entitlements file includes the necessary permissions for CarPlay. In SwiftRadio/SwiftRadio.entitlements, add:

<dict>
  <key>com.apple.developer.playable-content</key>
  <true/>
  <key>com.apple.developer.carplay-audio</key>
  <true/>
</dict>

These entitlements let your app play audio and integrate properly with CarPlay.

3. Build the CarPlay scene delegate

The CarPlay scene delegate manages the CarPlay interface lifecycle. When the CarPlay interface connects, it sets up a list of radio stations, updates it as needed, and responds to user selections. Here’s how that looks in code:

final class CarPlaySceneDelegate: UIResponder, CPTemplateApplicationSceneDelegate {
  private var interfaceController: CPInterfaceController?

  func templateApplicationScene(
    _ scene: CPTemplateApplicationScene,
    didConnect interfaceController: CPInterfaceController
  ) {
    self.interfaceController = interfaceController

    let template = CPListTemplate(title: "Radio Stations", sections: [])
    interfaceController.setRootTemplate(template, animated: false, completion: nil)

    if StationsManager.shared.stations.isEmpty {
      StationsManager.shared.fetch { [weak self] _ in
        self?.reloadStations(on: template)
      }
    } else {
      reloadStations(on: template)
    }

    StationsManager.shared.addObserver(self)
  }

  private func reloadStations(on template: CPListTemplate) {
    let items = StationsManager.shared.stations.map { station -> CPListItem in
      let item = CPListItem(text: station.name, detailText: station.desc)
      station.getImage { image in item.setImage(image) }
      item.handler = { _, completion in
        StationsManager.shared.set(station: station)
        FRadioPlayer.shared.play()
        completion()
      }
      return item
    }

    template.updateSections([CPListSection(items: items)])
  }
}

extension CarPlaySceneDelegate: StationsManagerObserver {
  func stationsManager(
    _ manager: StationsManager,
    stationsDidUpdate stations: [RadioStation]
  ) {
    guard let template = interfaceController?.rootTemplate as? CPListTemplate else { return }
    DispatchQueue.main.async { self.reloadStations(on: template) }
  }
}

This delegate leverages the shared StationsManager and FRadioPlayer singleton, so when users pick a station on CarPlay, it seamlessly updates the app’s data sources.

4. Keep the App UI aligned with CarPlay

Finally, don’t forget to keep your app’s UI in sync with what’s happening in CarPlay. In your StationsViewController, initialize the now-playing bar when the app launches from a CarPlay session like this:

// StationsViewController.viewDidLoad
updateNowPlayingButton(station: manager.currentStation)
updateHandoffUserActivity(userActivity, station: manager.currentStation)
startNowPlayingAnimation(player.isPlaying)

This way, your app’s state stays consistent with the station selected on the CarPlay dashboard.

CarPlay support shipped as part of Swift Radio v3. If you’re looking for the older MPPlayableContentManager approach (pre-iOS 14), see the archived version.