CarPlay Audio APP開發紀錄_2
繼上一篇的開發指南、權限申請和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更新。
鎖屏控制設定參考 ↓
有了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通靈師們。