Starting from iOS 14, you can use the CarPlay framework to develop an audio CarPlay app (You can use it in iOS 12 if it is a navigation app), which provides some UI templates to support developers' custom interfaces.
If your project is compatible with iOS 13 and earlier, it needs to be developed using the MediaPlayer framework, which is forward compatible.
Therefore, if your app needs to use the CarPlay framework on iOS 14 and later and is compatible with iOS 13 and earlier, you need to maintain two sets of code, and the development effort may nearly double.
I will only consider iOS 14 and above here. If you want to support lower versions, please check out WWDC17 and WWDC18.
Requesting CarPlay Entitlements and configuring your project
First, you need to determine whether your app is suitable for CarPlay, go to the developer website to apply for the CarPlay entitlements of the corresponding app type, and configure your project. Only then can your project use CarPlay Simulator. Otherwise, your CarPlay Simulator cannot be opened (grayed out / disabled).
However, I noticed that as long as your CarPlay Simulator is enabled, even the apps that don't support CarPlay can open CarPlay Simulator. So you can run Apple's CarPlay Music sample app to enable the CarPlay Simulator.
Download CarPlay Music App:
If there is no option for CarPlay, try to download Additional Tools. For example, I'm using Xcode 13.4.
Download Additional Tools for Xcode 13.4:
However, you still need your project support to run and debug your CarPlay app with the CarPlay Simulator.
Therefore, requesting entitlements in advance is best if you plan to develop a CarPlay app because Apple's review will take time. During this period, you can look at the relevant development documents and use the CarPlay Simulator to debug and develop after the permission is applied, and the project is configured.
Requesting CarPlay Entitlements:
Running and debugging CarPlay apps with CarPlay Simulator
Every iPhone Simulator comes with a CarPlay Simulator, which you can open at:
I/O > External Displays > CarPlay
The standard CarPlay Simulator window size and scale is 800 x 480, @2x.
If your project is a navigation app, Apple recommends turning on the additional options of the CarPlay Simulator and entering the following commands in the terminal:
defaults write com.apple.iphonesimulator CarPlayExtraOptions -bool YES
The above commands allow you to set the window size and scale each time you start CarPlay Simulator to test to ensure your map content fits all the recommended configurations. Maybe it is only for the navigation apps because audio apps don't look good after changing the window size, so I recommend closing it.
defaults write com.apple.iphonesimulator CarPlayExtraOptions -bool NO
If you open CarPlay Simulator and don't see your app on the home screen, you probably forgot to add the corresponding entitlements. You need to add the Key com.apple.developer.carplay-audio to Entitlements.plist and set the Value to 1.
<key>com.apple.developer.carplay-audio</key>
<true/>
Check the link below for more info:
Known Issues
If you have a Mac with an M1 chip, you may not be able to use CarPlay Simulator.
Launching the CarPlay app will crash directly if your Xcode runs in Rosetta mode.
Running the Simulator as Rosetta also won't solve the issue. Unfortunately, there is currently no solution yet.
Check out the link to learn more:
Declare a CarPlay scene
Declare a scene in Info.plist.
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<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>CarPlayScene Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).CarPlaySceneDelegate</string>
</dict>
</array>
</dict>
</dict>
Implement CarPlaySceneDelegate
import CarPlay
class CarPlaySceneDelegate: UIResponder, CPTemplateApplicationSceneDelegate {
static let configurationName = "CarPlayScene Configuration"
var interfaceController: CPInterfaceController?
func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, didConnect interfaceController: CPInterfaceController) {
self.interfaceController = interfaceController
let item = CPListItem(text: "My text", detailText: "My detail text")
let section = CPListSection(items: [item])
let listTemplate = CPListTemplate(title: "Albums", sections: [section])
interfaceController.setRootTemplate(listTemplate, animated: true, completion: nil)
}
func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, didDisconnectInterfaceController interfaceController: CPInterfaceController) {
self.interfaceController = nil
}
}
The CPTemplateApplicationSceneDelegate protocol defines how CarPlay connects, disconnects, and responds to certain user actions in a scene. You must create and set the root template when CarPlay starts your app and connects its scene. Generally, we implement the following two methods:
func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, didConnect interfaceController: CPInterfaceController)
Notify the delegate that the CarPlay scene is connected. The CarPlay framework will call this method when the app is launched on the car screen, where the template is initialized.
The system automatically creates a CPInterfaceController instance (similar to UINavigationController) as the entry controller of our CarPlay app. Therefore, we only need to hold this instance in the callback.
In the above sample code, we created a list template CPListTemplate (similar to UITableView), which receives multiple sets of CPListSections, and the section contains multiple CPListItems (similar to UITableViewCell).
Finally, we set CPListTemplate as the root template of the app (similar to rootViewController).
func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, didDisconnectInterfaceController interfaceController: CPInterfaceController)
Notify the delegate that the CarPlay scene has been disconnected. This method will be called when the car is disconnected and can do some cleanup.
Displaying Content in CarPlay
The user interface for CarPlay
The CarPlay app user interface is composed of Template and Item, and the CarPlay app of the audio type, you can use CPTabBarTemplate, CPListTemplate, CPListImageRowItem, CPListItem, etc.
CPListTemplate: List template (similar to UITableView)
CPListItem: a generic list item
CPListImageRowItem: A list item that displays a series of images
CPMessageListItem: Represents a list of conversations or contacts
CPGridTemplate: Template for displaying and managing grid items
CPTabBarTemplate: TabBar template (similar to UITabBarController)
CPTabBarTemplate
CPTabBarTemplate is similar to UITabBarController in UIKit. Initialize with a set of CPTemplates, which can be used as the rootTemplate of interfaceController.
let tabBarTemplate = CPTabBarTemplate(templates: tabTemplates)
self.carplayInterfaceController!.setRootTemplate(tabBarTemplate, animated: true, completion: nil)
You should note that the templates of CPTabBarTemplate limit the number of templates. The maximum number you can get via the maximumTabCount class property. Its value depends on the rights added in Entitlements.plist. For example, the audio app can add up to 4 templates. If the number is exceeded, the app will crash.
/**
The maximum number of tabs that your app may display in a @c CPTabBarTemplate,
depending on the entitlements that your app declares.
@warning The system will throw an exception if your app attempts to display more
than this number of tabs in your tab bar template.
*/
open class var maximumTabCount: Int { get }
Additional information:
In "Enabling Your App for CarPlay" at WWDC17, Apple mentioned that CarPlay apps built with the MediaPlayer framework recommend using up to 4 tabs and shorter titles due to limited space and narrow screens in some cars. Moreover, when there is audio playing, the "Now Playing" button needs to be displayed in the upper right corner of the rootTemplate.
Enabling Your App for CarPlay:
You can set the tabTitle and tabImage of each template's tab, or you can set the tabSystemItem to use the system style (there are fewer styles available, and there is no way to customize). If you don't set tabSystemItem and tabImage is nil, then the tabBarItem will use UITabBarItem.SystemItem.more.
// custom tab style
playlistTemplate.tabTitle = "Playlists"
playlistTemplate.tabImage = UIImage(systemName: "list.star")
// Use the system tab style. If you already set both tabTitle and tabImage, tabSystemItem will not take effect
playlistTemplate.tabSystemItem = .favorites
// show red dot
playlistTemplate.showsTabBadge = true
At this time, you need to find your UI designer for custom images:
Design Icons and images
Important:
CPTabBarTemplate has a delegate property that conforms to the CPTabBarTemplateDelegate protocol.
You can refresh the selectedTemplate data in this method if needed.
We need to note that the difference from UITabBarController's tabBarController:didSelect viewController: is:
When the CPTabBarTemplate is rendered, and the first tab is selected by default, the delegate method will be called once, so you need to pay attention to whether you need to filter out the first call in the implementation of the delegate method.
Clicking the currently selected tab will also call the delegate method, so you must consider whether to filter this situation.
CPListTemplate
CPListTemplate is similar to UITableview. For example, a CPTabBarTemplate can be initialized with an array of CPListTemplate. CPListTemplate consists of items that conform to the CPListTemplateItem protocol and are similar to UITableviewCell. Usually, the audio apps use CPListItem and CPListImageRowItem.
CPListTemplate has a delegate property that conforms to the CPListTemplateDelegate protocol. It will be triggered when the user clicks on an item, and then we can push other Templates in this method implementation.
@available(iOS, introduced: 12.0, deprecated: 14.0)
func listTemplate(_ listTemplate: CPListTemplate, didSelect item: CPListItem, completionHandler: @escaping () -> Void)
Important:
Note that there is a completionHandler parameter here. When the listTemplate:didSelect item:completionHandler: method is called, a "loading activity indicator" is displayed on the right on didSelect before the completionHandler is called.
So the best practice is to ensure your audio or other things are ready before you call pushTemplate:templateToPush:animated: in the completion code block. Of course, you also must ensure that the completionHandler must be called. Such as in the case of an early exit. Otherwise, the "loading activity indicator" would have been there forever.
Also, CPListTemplateDelegate has been marked as deprecated in iOS 14. It is recommended to use the handler property of the CPSelectableListItem protocol to handle actions, which is an optional action block. CPListItem, CPListImageRowItem, etc. all follow the CPSelectableListItem protocol.
CPSelectableListItem
@available(iOS 14.0, *)
public protocol CPSelectableListItem : CPListTemplateItem {
var handler: ((CPSelectableListItem, @escaping () -> Void) -> Void)? { get set }
}
CPListImageRowItem
CPListImageRowItem can display modules consisting of a set of albums and initialized with text and images. The text is used to show the module name, and the image is used to show the covers of the first few albums.
let imagesCount = CPMaximumNumberOfGridImages // It depends on the available width of the car display
let allowedImageItems = imageItems.prefix(imagesCount)
let imageRow = CPListImageRowItem(text: imageRowTitle, images: allowedImageItems.map(\.image))
The click area of CPListImageRowItem can be divided into each image area and all areas except the image.
The action for each image is handled by setting the listImageRowHandler property of the CPListImageRowItem instance.
/**
A block that is called when the user selects one of the images in this image row item.
The user may also select the cell itself - for that event, specify a @c handler.
*/
open var listImageRowHandler: ((CPListImageRowItem, Int, @escaping () -> Void) -> Void)? // The image row item that the user selected.
Actions outside the image are handled by the handler property of the CPListImageRowItem instance.
/**
An optional action block, fired when the user selects this item in a list template.
*/
@available(iOS 14.0, *)
open var handler: ((CPSelectableListItem, @escaping () -> Void) -> Void)?
CPListItem
CPListItem can be used to display albums or audio.
For audio items, which generally consist of audio cover, audio name, and audio description, we can use the following constructor to initialize CPListItem.
let listItem = CPListItem(text: episode.title, detailText: episode.description)
The indicator icon on the right has two system styles (disclosureIndicator, cloud) and supports custom icons.
let listItem = CPListItem(text: podcast.title,
detailText: "",
image: nil,
accessoryImage: nil,
accessoryType: .disclosureIndicator)
Note: If the detailText of the CPListItem is nil, the system will reduce the view height, and the text will be centered, affecting the UI and overall harmony. Therefore, you can consider filling the detailText with the audio duration or other data when the audio has no subtitle.
CPNowPlayingTemplate
The audio playback view is the most crucial of the audio type CarPlay app. It uses CPNowPlayingTemplate, which is a singleton.
You can configure CPNowPlayingTemplate according to your needs, such as adding control buttons. For example, you can use some of the system buttons provided by the CarPlay framework or customize the buttons.
The important thing is that you should configure CPNowPlayingTemplate at the time of
func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, didConnect interfaceController: CPInterfaceController)
, not when you push to CPNowPlayingTemplate, because CPNowPlayingTemplate is not necessarily triggered by an active push but may also be triggered by "playing" and by the "Now Playing Button" in the upper right corner of the app or rootTemplate.
let nowPlayingTemplate = CPNowPlayingTemplate.shared
nowPlayingTemplateshared.add(self)
let repeatButton = CPNowPlayingRepeatButton {}
let playbackRateButton = CPNowPlayingPlaybackRateButton {}
nowPlayingTemplateshared.updateNowPlayingButtons([repeatButton, playbackRateButton])
Whether you use the CarPlay framework or the MediaPlayer framework to build a CarPlay app, MPNowPlayingInfoCenter and MPRemoteCommandCenter provide audio information for the playback interface and respond to remote playback control events.
In the CarPlay framework, some remote control events are handled by the handler of CPNowPlayingButton, such as playback repeat mode, playback rate, and so on.
But, of course, suppose your app is an audio-type app. In that case, it should already support these functions because the audio playback information and playback control of the iPhone lock screen and the control center is also provided through them. Therefore, we only need to optimize or enhance the function of CarPlay.
What we're going to do specifically is:
1. Set and update the nowPlayingInfo of MPNowPlayingInfoCenter, which contains the metadata of the currently playing audio, such as title, author, duration, etc.
Switch audio (previous, next, etc.)
Pause, resume, stop playback
Seek (skip titles, drag progress, etc.)
Update playback speed (display status of playback speed button in CPNowPlayingTemplate). If the current audio is not playing, set the playback speed to 0
...
nowPlayingInfo = [String: Any]()
nowPlayingInfo[MPMediaItemPropertyTitle] = currentEpisode.title
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = currentEpisode.podcastTitle
if let artwork = artwork {
nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork
}
// nowPlayingInfo[MPMediaItemPropertyArtist] = "Joe Bloggs"
// nowPlayingInfo[MPMediaItemPropertyReleaseDate] = "2022"
// nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = 600
// nowPlayingInfo[MPNowPlayingInfoPropertyDefaultPlaybackRate] = 1
// nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 1
// nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackQueueCount] = 50
// nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackQueueIndex] = 5
// ...
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
We should note that for the playback progress, that is, the update of the current audio playback position, the system will automatically infer it based on the previously provided playback position and playback rate. Therefore, it is unnecessary or not recommended to update nowPlayingInfo frequently, which is expensive.
In addition to nowPlayingInfo, some states need to be synchronized to the CarPlay app in other ways, such as:
Playback state: pause/play (display state of play/pause button in CPNowPlayingTemplate)
if #available(iOS 13.0, *) {
MPNowPlayingInfoCenter.default().playbackState = .playing
}
Repeat mode: sequential loop/single loop (display the repeat mode in CPNowPlayingTemplate)
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.changeRepeatModeCommand.currentRepeatType = .one
Respond to MPRemoteCommandCenter events. Respond to remote playback control events such as play, pause, switch songs, and so on.
Using the CarPlay framework: When using the CarPlay framework, the changeRepeatModeCommand and changePlaybackRateCommand remote control commands are no longer processed through target-action, but through the handlers of CPNowPlayingRepeatButton and CPNowPlayingPlaybackRateButton, but the isEnabled property of command still needs to be enabled. For example:
(*) When the command's isEnabled is true, the user clicks the repeat mode button, and the handler of CPNowPlayingRepeatButton will be triggered. Then you can update the playback mode of the app and synchronize the repeat mode state to CarPlay through the above method.
If your app also supports shuffle mode, you can add CPNowPlayingShuffleButton and enable changeShuffleModeCommand, and then work with CPNowPlayingRepeatButton to switch between the three modes.
(*) Since the app can only know that the user clicked the button, but doesn't know the user's intention, the best practice for the handler of CPNowPlayingPlaybackRateButton is:
Set a playback rate range. When the user clicks, increase the app playback rate, and synchronize to CarPlay by updating nowPlayingInfo.
If the current audio is playing at the fastest supported rate, continue to increase the playback rate to the minimum rate, and so on.
Follow me on:
Comments