CarPlay Audio APP開發紀錄_2

陳小嬰
Parenting 數位研發
12 min readJul 3, 2022

繼上一篇的開發指南、權限申請和Framework之後,這篇終於要來寫CarPlay功能啦~

這篇只講我有實作到的三個Template:TabBarTemplate、CPListTemplate、CPNowPlayingTemplate,文章範例的最低版本為iOS14。

CPTemplateApplicationSceneDelegate

首先回顧一下, 上一集最後實踐了CPApplicationDelegate接口和設定根視圖,這是開啟CarPlay的第一關。

class MyCarPlaySceneDelegate: UIResponder, CPTemplateApplicationSceneDelegate {    
var
interfaceController: CPInterfaceController?
func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, didConnect interfaceController: CPInterfaceController) {
self
.interfaceController = interfaceController
let
item = CPListItem(text: "My title", detailText: "My subtitle")
let
section = CPListSection(items: [item])
let
listTemplate = CPListTemplate(title: "Home", sections: [section])
interfaceController.setRootTemplate(listTemplate, animated: true, completion: nil)
}
}

這邊特別感謝Arthur大大指點,如醍醐灌頂般讓我理解了UIWindowSceneDelegate和CPTemplateApplicationSceneDelegate之間的關係,也因為之前APP的重構就有使用Combine,所以只有View不同,API的部分則是兩個Scene共用。

CPTabBarTemplate

Tab bar模板很簡單,設定標題、圖片,搞定!

let tab1 = CPListTemplate(title: NSLocalizedString("tab1", comment: ""), sections: [])
tab1.tabImage = UIImage(named: "tab1_icon")
tab1.tabTitle = NSLocalizedString("tab1", comment: "")
let tab2 = CPListTemplate(title: NSLocalizedString("tab2", comment: ""), sections: [])
tab2.tabImage = UIImage(named: "tab2_icon")
tab2.tabTitle = NSLocalizedString("tab2", comment: "")
let tab = CPTabBarTemplate(templates: [
tab1,
tab2
])
tab.delegate = selfinterfaceController.setRootTemplate(tab, animated: true, completion: nil)

CPListTemplate

List模板有兩種Cell顯示方式:CPListItem、CPListImageRowItem。

第一層:CPListSection

header設定標題,sectionIndexTitle用於顯示右側滑軌索引。

let section1 = CPListSection(  items: [    CPListItem(text: "item1", detailText: "subTitle", image: UIImage(named: "item1")),    CPListItem(text: "item2", detailText: "subTitle", image: UIImage(named: "item2"))  ],  header: "Section1",  sectionIndexTitle: "")let section2 = CPListSection(  items: [    CPListItem(text: "item3", detailText: "subTitle", image: UIImage(named: "item3"))  ],  header: "Section2",  sectionIndexTitle: "")let tab1 = CPListTemplate(title: "tab1", sections: [section1, section2])tab1.tabImage = UIImage(named: "icon")tab1.tabTitle = "tab1"

第二層:CPListItem

這是一般的Item,包含標題、次標題、圖片,userInfo可以放自訂物件。

var itemList: [CPListItem] = []for item in list {  let item = CPListItem(text: item.title, detailText: nil, image: nil)  item.userInfo = item  itemList.append(item)}let section = CPListSection(items: itemList)self?.tab1?.updateSections(sectionList)

第三層:CPListImageRowItem

這是可以顯示圖片陣列的RowItem,包含標題、圖片陣列,要注意的是無論Grid圖片的數量還是尺寸都是有限制的。

var sectionList: [CPListSection] = []var itemList: [CPListImageRowItem] = []var imageList: [UIImage] = []for index in 0..<list.count {  // 超出網格圖片數量限制就不顯示
if index >= CPMaximumNumberOfGridImages {
break } let item = list[index] if let url = URL(string: item.imagePath), let data = try? Data(contentsOf: url), let image = UIImage(data: data) { // 取得網格圖片系統尺寸
let size = CPListImageRowItem.maximumImageSize.width
let image = image.resize(width: size, height: size) imageList.append(image) }}let rowItem = CPListImageRowItem(text: title, images: imageList)itemList.append(rowItem)sectionList.append(CPListSection(items: itemList, header: nil, sectionIndexTitle: nil))

點擊事件:

  • handler作用於RowItem區塊的點擊
  • listImageRowHandler作用於圖片的點擊。
let item = CPListImageRowItem(text: title, images: imageList)item.userInfo = myObjectitem.handler = { [weak self] (item, completion) in}item.listImageRowHandler = imageHandler

從userInfo取出對應的物件

private func imageHandler(_ item: CPSelectableListItem, index: Int, completion: @escaping () -> Void) {    if let myObject = item.userInfo as? MyObject {    }}

CPListItem要到iOS15才能開關響應點擊

let item = CPListItem(text: "title", detailText: "subTitle", image: UIImage(named: "icon"))if #available(iOS 15.0, *) {    item.isEnabled = true}

CPNowPlayingTemplate

當前播放頁面要實踐兩個protocol:CPNowPlayingTemplateObserver、CPTabBarTemplateDelegate。

extension MyCarPlaySceneDelegate: CPNowPlayingTemplateObserver, CPTabBarTemplateDelegate {  /// 建立tab模板  func buildTabTemplate(_ interfaceController: CPInterfaceController) {    let tab = CPTabBarTemplate(templates: [])    tab.delegate = self    interfaceController.setRootTemplate(tab, animated: true, completion: nil)  }  // - MARK: NowPlaying  func showNowPlaying() {    let nowPlaying = CPNowPlayingTemplate.shared    nowPlaying.tabImage = UIImage(named: "icon")    nowPlaying.tabTitle = "play"    nowPlaying.add(self)    interfaceController?.pushTemplate(nowPlaying, animated: false, completion: nil)  }  func nowPlayingTemplateAlbumArtistButtonTapped(_ nowPlayingTemplate: CPNowPlayingTemplate) {

// 點擊專輯創作者
} func nowPlayingTemplateUpNextButtonTapped(_ nowPlayingTemplate: CPNowPlayingTemplate) { // NowPlaying頁面右上角點擊下一首按鈕 }}

剩下的就跟鎖屏控制一樣,透過nowPlayingInfo更新。

鎖屏控制設定參考 ↓

MPPlayableContentDelegate

MPPlayableContentManager

CPNowPlayingTemplateObserver

MPContentItem

有了List和NowPlaying,一個CarPlay播放APP就完成啦~

CarPlay坑洞紀錄

坑洞1

模擬器上消失的當前播放按鈕,在實機上是有顯示的?

這個受iOS版本和CarPlay版本影響,若按鈕沒顯示,但同一個位置點下去還是可以觸發點擊事件的。

坑洞2

M1不能跑實機會閃退

這個目前無法重現,看來已經修掉BUG了。

坑洞3

超過數量限制就閃退

官方文件上說明每台CarPlay都有不同的顯示數量限制,所以從系統取得數量限制,超過不顯示,才能免於閃退。

CPListTemplate.maximumSectionCountCPListTemplate.maximumItemCountCPMaximumNumberOfGridImages

雖然坑洞還是有,但比起其他發展中的項目(例如SwiftUI),CarPlay還是頗穩定的,想嘗試的開發者們不要害怕,Just do it!

2022的WWDC上,Apple發表新的CarPlay要攻佔儀表板,也放寬CarPlay APP的類型限制,看來在不久的將來,CarPlay上的APP能變得更多元更豐富,希望這篇文章能幫助到未來要開發CarPlay的iOS通靈師們。

--

--

陳小嬰
Parenting 數位研發

喜愛動物又注重環保的iOS工程師就是我。Write the code change the world.