【iOS】音楽アプリで使えるCarPlayのUIについて

この記事は[レコチョク Advent Calendar 2023](https://qiita.com/advent-calendar/2023/recochoku)の15日目の記事となります。

## はじめに

こんにちは、後藤です。株式会社レコチョクでiOSアプリの開発をしています。
スキマスイッチの年末のライブを生きがいに今日この頃を過ごしています。

この記事では、CarPlayのAudioアプリを設計する際に使用できるUIの一覧について紹介します。

## 検証環境

Xcode 15.1

## 使用できるテンプレート

CarPlayアプリのUIは、グリッドやリストなどのテンプレートを用いて作成します。

使用できるテンプレートは、CarPlayのアプリの種類を表すエンタイトルメントによって決定されます（Audioアプリなら`com.apple.developer.carplay-audio`）。


![CarPlayTemplateList.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279002/872a3b5e-6856-7c43-37ec-e75ce5cce0be.png)



画像出典: [CarPlay App Programming Guide](https://developer.apple.com/carplay/documentation/CarPlay-App-Programming-Guide.pdf)

そのため、サポートされていないテンプレートを使用すると、実行時にクラッシュしてしまいます。例えば、AudioアプリではInformationやSearchなどのテンプレートは使用できません。

実際にAudioアプリでInfromationテンプレートを使うと以下のようなエラーが発生します。
```
Thread 12: "Unsupported object <CPInformationTemplate: 0x60000303fa80> <identifier: 7BB405DC-A106-4117-B33B-BE442EA07433, userInfo: (null), tabTitle: (null), tabImage: (null), showsTabBadge: 0> passed to pushTemplate:animated:completion:. Allowed classes: {(\n    CPListTemplate,\n    CPActionSheetTemplate,\n    CPTabBarTemplate,\n    CPAlertTemplate,\n    CPGridTemplate,\n    CPNowPlayingTemplate,\n    CPVoiceControlTemplate\n)}"
```




## Audioアプリで使用できるテンプレート一覧

以下はAudioエンタイトルメントで使用できるテンプレートの一覧です。

### Action Sheet

アクションシートは、アクションなどに応答して表示されるアラートの特定のスタイルです。現在のコンテキストに関連する2つ以上の選択肢を提示します。アクションシートを使用して、人々にタスクの開始を促したり、破壊的な操作を実行する前に確認を求めたりできます。

iOS 17以降で利用可能です。


![CarPlayActionSheetTemplate.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279002/a07e4a74-04c1-145c-48a6-9881563bbb8d.png)



```swift
        let action1 = CPAlertAction(
            title: "Action 1",
            style: .default
        ) { _ in
            // Action 1が選択された時の処理を記述
        }

        let action2 = CPAlertAction(title: "Action 2", style: .default) { _ in
            // Action 2が選択された時の処理を記述
        }

        let cancelAction = CPAlertAction(title: "キャンセル", style: .cancel) { _ in
            // キャンセルアクションの処理を記述
        }

        let actionSheetTemplate = CPActionSheetTemplate(
            title: "ActionSheetTemplate",
            message: "メッセージを表示できます",
            actions: [action1, action2, cancelAction]
        )
```


### Alert

アラートは、アプリの状態に関連する重要な情報を伝えます。アラートには、タイトルと1つ以上のボタンが含まれます。

[CarPlayのHuman Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/carplay)によると、「エラーは絶対に必要な場合に限り、表示する必要がある」とあります。

> A CarPlay app needs to handle errors gracefully and report them to people only when absolutely necessary.

そのため、アラートを必要以上に使用することは避ける必要があります。


![CarPlayAlertTemplate.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279002/feaff2ce-3f54-9593-8685-4134d66c3f86.png)



```swift
        let cancelAction = CPAlertAction(
            title: "キャンセル",
            style: .cancel
        ) { _ in
            // キャンセルが選択された時の処理を記述
        }

        let alertTemplate = CPAlertTemplate(
            titleVariants: ["AlertTemplate"],
            actions: [cancelAction]
        )
```

### Grid

グリッドテンプレートは、最大8つのアイコンとタイトルで表される選択肢を表示します。


![CarPlayGridTemplate.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279002/0cd42f07-ed10-96a8-695d-466bddd2e31d.png)



```swift
        var gridItems = [CPGridButton]()
        let symbols = [
            "house",
            "gear",
            "person",
            "star",
            "heart",
            "message",
            "car",
            "airplane"
        ]

        symbols.enumerated().forEach { index, symbol in
            if let image = UIImage(systemName: symbol) {
                let gridItem = CPGridButton(
                    titleVariants: ["Grid Item \(index + 1)"],
                    image: image
                ) { _ in
                    // アイテムがタップされた時の処理
                }
                gridItems.append(gridItem)
            }
        }

        let gridTemplate = CPGridTemplate(
            title: "Grid Title",
            gridButtons: gridItems
        )

        gridTemplate.tabImage = UIImage(systemName: "house")
```

### List

リストは、セクションに分割できる、単一の列の行のスクロールテーブルとしてデータを表示します。

また、一部の車種ではリストアイテム（`CPListItem`）の表示個数が12個に制限されることがあります。そのため、重要な情報をリストの先頭に配置するなど、12個に限定される場合を想定して設計することが必要です。


![CarPlayListTemplate.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279002/27784505-3dcd-b9de-2269-e3c391091b1a.png)



```swift
        // CPListItemを仮で10個作成
        let items = (1 ... 10).map { i in
            CPListItem(
                text: "Item \(i)",
                detailText: "詳細を書くこともできます"
            )
        }

        let section = CPListSection(items: items)

        let listTemplate = CPListTemplate(
            title: "ListTemplate",
            sections: [section]
        )
```

### Tab Bar

タブバーを使用することで、他のテンプレートをまとめることができます。各テンプレートは、タブバーの1つのタブを占め、異なるテンプレート間を切り替えることができます。

iOS 17.0の時点では、Audioアプリは最大4つのタブ、その他のアプリタイプでは最大5つのタブを持つことができます（将来的にタブの表示個数は変更される可能性があります）。


![CarPlayTabBarTemplate.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279002/a0f38d8d-306b-3ede-68f6-c75ada16030c.png)



アプリがオーディオを再生している場合、CarPlayはタブバーの右上隅に「再生中」ボタンを表示し、再生コントロールへのアクセスを容易にします。


![CarPlayTabBarNowPlaying.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279002/0f5ff739-d9b0-cb6a-e0ca-0598b094755c.jpeg)



ただし、タブバーに4つ以上のタブがある場合、「再生中」ボタンが表示されないことがあります。



```swift
        let gridTemplate1 = makeGridTemplate(tabTitle: "Grid1")
        let gridTemplate2 = makeGridTemplate(tabTitle: "Grid2")
        let gridTemplate3 = makeGridTemplate(tabTitle: "Grid3")
        let gridTemplate4 = makeGridTemplate(tabTitle: "Grid4")

        let tabBarTemplate = CPTabBarTemplate(
            templates: [
                gridTemplate1,
                gridTemplate2,
                gridTemplate3,
                gridTemplate4
            ]
        )

    // CPTabBarTemplateに表示させるためのTemplateを作成するためのメソッド
    func makeGridTemplate(tabTitle: String) -> CPGridTemplate {
        var gridItems = [CPGridButton]()
        let symbols = [
            "house",
            "gear",
            "person",
            "star",
            "heart",
            "message",
            "car",
            "airplane"
        ]

        symbols.enumerated().forEach { index, symbol in
            if let image = UIImage(systemName: symbol) {
                let gridItem = CPGridButton(
                    titleVariants: ["Grid Item \(index + 1)"],
                    image: image
                ) { _ in
                    // アイテムがタップされた時の処理
                }
                gridItems.append(gridItem)
            }
        }

        let gridTemplate = CPGridTemplate(
            title: tabTitle,
            gridButtons: gridItems
        )

        gridTemplate.tabImage = UIImage(systemName: "house")

        return gridTemplate
    }
```

### Now Playing

再生中の画面は、現在再生中のオーディオに関する情報（タイトル、アーティスト、経過時間、アルバムアートワークなど）を表示できます。

このテンプレートは特別で、ユーザーはCarPlayのホーム画面やアプリのナビゲーションバーにあるボタンから直接アクセスできます。遷移できる導線が多いため、再生中のトラックが変わるたびに、タイトル、アーティスト、アルバムアートワークなどの情報を更新する必要があります。



![CarPlayNowPlayingTemplate2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279002/9f99a4fa-d90e-a87b-9788-2ad3b87d6051.png)



```swift
        let repeatButton = CPNowPlayingRepeatButton { _ in
            // リピートボタンを押下した時の処理
        }

        let shuffleButton = CPNowPlayingShuffleButton { _ in
            // シャッフルボタンを押下した時の処理
        }

        // nowPlayingTemplateを作成
        let nowPlayingTemplate = CPNowPlayingTemplate.shared
        // ボタンの追加
        nowPlayingTemplate.updateNowPlayingButtons([repeatButton, shuffleButton])
```

## まとめ

本記事ではCarPlayのAudioエンタイトルメントで使用できるテンプレート一覧を紹介しました。CarPlayのUIでは、事前に用意されたテンプレートを使用するため、自由度は限られていますが、開発過程での迷いが少なくなるでしょう。

この記事が今後CarPlayのアプリを設計する上で役に立てば幸いです。

明日の[レコチョク Advent Calendar 2023](https://qiita.com/advent-calendar/2023/recochoku)は16日目「makerealでWebページ生成してみた」です。お楽しみに!

---
この記事はレコチョクのエンジニアブログの記事を転載したものとなります。

https://techblog.recochoku.jp/10037