26
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

第二のドワンゴAdvent Calendar 2020

Day 18

iOS14 で始める CarPlay

Last updated at Posted at 2020-12-18

はじめに

みなさん、CarPlay をご存知でしょうか?
実はiOS7以降のiOSには CarPlay という機能が搭載されており、対応車種であればカーナビに iPhone を繋いで、カーナビ上で CarPlay 対応された iPhone アプリを操作できるようになっています。
対応車種はこちら
canavi.gif

本稿ではその CarPlay 対応アプリのサンプルとしてナビゲーションアプリを取り上げ、地図を表示させるところまで説明します。

準備

実際にカーナビにつなげる CarPlay のアプリの開発を始めるに当たってXcodeを開く前に色々と準備をする必要があります。

  1. CarPlay の開発申請
  2. Provisioning Profile の設定

なお、とりあえず Simulator でいいから作ってみたい、という人はここの項目を飛ばしてもらってCarPlay アプリを実装するの項目に行ってもらって結構です。

CarPlay の開発申請

まず、iOS アプリ開発の基本として実機デバッグするには Apple Developer Program (以下ADP) に試験用端末の登録と Provisioning Profile の登録が必要なのですが CarPlay の場合はその Provisioning Profile に CarPlay 用の設定を行わないといけません。
ですが ADP の Provisioning Profile の設定画面みてもありません。

その設定を行うにはまず Apple に申請を ここ から行う必要があります(要 ADP アカウント)。
スクリーンショット 2020-12-17 1.40.55.png

その際、開発する CarPlay 向けアプリがどのようなものか App Type のセレクトボックスから以下のカテゴリから選びます

  1. Audio - 音楽アプリ
  2. Automaker - 車両組み込み機能アプリ
  3. Communication - チャットアプリ
  4. EV Charging - 充電スポット検索アプリ
  5. Navigation - カーナビアプリ
  6. Parking - 駐車場検索アプリ
  7. Quick Food Ordering - レストラン検索アプリ

上記カテゴリ以外のアプリは開発は許されておらず、また、2つ以上を組み合わせるアプリを作ることも許されてはいません。

この中で特殊なのが 2 の Automaker です。エアコンの操作やラジオと言った車両に組み込まれてる機能を操作するためのアプリです。
もちろん車両側からそのAPIが提供されている必要があるため実際には開発する際は車両 or 周辺機器メーカーと協業する必要がでるでしょう。

なお、申請を行えるのは ADP アカウント保持者のみです。 InHouseバイナリを配布できる法人向けアカウント Apple Developer Enterprise Program(以下 ADEP )アカウントで申請しようとすると、この画面ではなく以下のような権限がないことを伝える画面が表示されます。
スクリーンショット 2020-12-17 1.54.36.png

実はこれは結構厄介で、
現状では CarPlay 対応させると InHouse ビルドができなくなることを意味しています。

業務で iOS アプリを開発してる人のほとんどが社内テスト用に InHouse ビルドしてると思うのですが、
CarPlay 対応の開発をする際はビルドコンフィルグ等をいじって InHouse ビルド時は コンパイル範囲から CarPlay を外すと言った処理が必要になってくるでしょう。(もしくは TestFlight を使うか)

Provisioning Profile の設定

CarPlay の申請が通ると Provisioning Profile 作成画面に新たに Entitlement を設定する項目が追加されます
スクリーンショット 2020-12-17 2.41.14.png
そこの項目に App Type で設定したカテゴリを選択します。
以上で CarPlay を実機デバッグするための事前準備は終了です。次に実際に CarPlay を実装していくために必要な設定及びコードを説明していきます。

CarPlay 向けアプリを実装する

では実際にiOSアプリを CarPlay 対応させていきましょう。
CarPlay 対応アプリと書いてしまうと tvOS や watchOS アプリのように専用の Bundle Identifier を用意して
ターゲットを新たに作るというイメージを持つ人がいるかもしれませんが実はそうではなく、すでにある iOS アプリのUIをカーナビ向けに最適化させる作業になります。
どちらかというと iPhone アプリの iPad 対応の方が感覚的に近いです。

では、はじめに、で述べたようにナビゲーションアプリを作っていきます

Simulator について

前段で実機デバッグするための準備について書きましたが、ずっと車両やカーナビに繋いで開発していくのも難しいため Simulator を使うことになると思います。
CarPlay の Simulator は実は iPhone Simulator に組み込まれており以下の図の様に I/O → External Display → CarPlay を選択して表示できます
スクリーンショット 2020-12-18 16.41.37.png

Entitlements ファイルを作成

開発するアプリが CarPlay 対応アプリであるということを設定する必要があります。
Entitlements ファイルを作り Provisioning Profile に設定されたカテゴリIDを有効化させます。
今回作るナビゲーションアプリの場合、カテゴリIDは com.apple.developer.carplay-maps になります

<dict>
	<key>com.apple.developer.carplay-maps</key>
	<true/>
</dict>

CarPlay 用 SceneDelegate を作る

実は Entitlements ファイルを作成した時点で CarPlay からアプリを呼ぶことが可能になります。
が、この時点で CarPlay 側から呼びだすと CarPlay 用の SceneDelegate が設定されていないためクラッシュします。
そのため、まず以下の様な CarPlay 用の SceneDelegate を用意します。

CarPlaySceneDelegate.swift
class CarPlaySceneDelegate: UIResponder {
    var interfaceController: CPInterfaceController?
    var window: CPWindow?
}

extension CarPlaySceneDelegate: CPTemplateApplicationSceneDelegate {

    func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene,
                                  didConnect interfaceController: CPInterfaceController, to window: CPWindow) {
        self.interfaceController = interfaceController
    }
}

上記が CarPlay 用の SceneDelegate の最小限実装です。
CarPlay から呼ばれるために CPTemplateApplicationSceneDelagate を実装します。
CarPlay 上でアプリが呼ばれると上記の templateApplicationScene(_ templateApplicationScene:didConnect interfaceController:to window:) メソッドが呼ばれるのでその中で CarPlay 用の画面を作っていくことになります。

CarPlay 用の SceneDelegate を呼び出せるようにする

CarPlay 用の SceneDelegate を作ったら今度は CarPlay から呼び出せるように設定をします。
方法としては、Info.plist でやる方法と AppDelegate でやる方法の2つがあります

呼び分け方法その1 - Info.plist でやる方法

Info.plist に Application Scene Manifest という項目があります。
XML のキーとしては UIApplicationSceneManifest になります。
その中に項目として UISceneConfigurations を追加し、iPhone用の SceneDelegate クラス、 CarPlay 用の SceneDelegate クラスをそれぞれ以下のように追記します

	<key>UIApplicationSceneManifest</key>
	<dict>
		<key>UISceneConfigurations</key>
		<dict>
			<key>UIWindowSceneSessionRoleApplication</key>
                        <!-- "iPhone用のSceneDelegate設定"-->
			<array>
				<dict>
					<key>UISceneConfigurationName</key>
					<string>iPhone</string>
					<key>UISceneDelegateClassName</key>
					<string>$(PRODUCT_MODULE_NAME).IPhoneAppSceneDelegate</string>
				</dict>
			</array>

                        <!-- "CarPlay用のSceneDelegate設定"-->
			<key>CPTemplateApplicationSceneSessionRoleApplication</key>
			<array>
				<dict>
					<key>UISceneConfigurationName</key>
					<string>CarPlay</string>
					<key>UISceneDelegateClassName</key>
					<string>$(PRODUCT_MODULE_NAME).CarPlaySceneDelegate</string>
				</dict>
			</array>
		</dict>
	</dict>

これを記述することで自動的に呼び出し元が iPhone 画面なのか CarPlay 画面なのかが判定されそれに合わせた SceneDelegate が呼ばれるようになります。

呼び分け方法その2 - AppDelegate でやる方法

Info.plist だけでなく AppDelegate 側にコードを記述して出しわけの制御を書くこともできます。
iOS13から AppDelegate に画面情報( UIScene )を受け取るデリゲートメソッドを定義することができるようになりました。

AppDelegate.swift

    @available(iOS 13.0, *)
    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {

        switch connectingSceneSession.role {
        case .carTemplateApplication: // CarPlay 用のアプリとして起動された
            let config = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
            config.delegateClass = CarPlaySceneDelegate.self
            return config
        default: // それ以外
            let config = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
            config.delegateClass = IPhoneAppSceneDelegate.self
            return config
        }
    }

渡ってくる UISceneSessionrole プロパティをみるとアプリがどのシーンから起動されたのかがわかるので、
そこで出しわけをすることになります。

Info.plist で出しわけをする方がいいのか AppDelegate で出しわけをする方がいいのか正直悩ましいです。
アプリのデプロイ戦略に応じて変わると考えています。

ちなみに、両方書いた場合はどちらかが無視される、というわけではなくどうやら結果が合成されるようです。
これ以上は UIScene の概念の説明となり本稿の主題からずれるので説明しませんが、omochimetaru さんの記事が詳しいので
以下を読んでいただけるといいかもしれません。
https://qiita.com/omochimetaru/items/31df103ef98a9d84ae6b

CarPlay 向けの画面を用意する

では準備が全て終わったので CarPlay アプリを作っていきましょう。
以下のコードが CarPlay から呼び出された際に画面を表示する為の最小限のコードです。
ナビゲーションアプリを作るための UI テンプレートである CPMapTemplate をセットし、起動時にそのテンプレートが表示されるようにしています。

CarPlaySceneDelegate.Swift
class CarPlaySceneDelegate: UIResponder {
    var interfaceController: CPInterfaceController?
    var window: CPWindow?
}

extension CarPlaySceneDelegate: CPTemplateApplicationSceneDelegate {

    func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, 
                                  didConnect interfaceController: CPInterfaceController, 
                                  to window: CPWindow) {
        self.interfaceController = interfaceController
        self.window = window

        let mapTemplate = CPMapTemplate()
        interfaceController.setRootTemplate(mapTemplate, animated: true)
    }
}

CPTemplate とは

ViewController を起点として画面を構築していく iOS や Mac アプリと違い、CarPlay 対応には基本的に ViewController は出てきません。
代わりに CPTemplateApplicationSceneDelegate のデリゲートメソッドで渡ってくる CPInterfaceControllerCPTemplate を継承したクラスをセットして画面表示を行います。
CPTemplate 系クラスそのものには画面遷移を行うメソッドはないです。

したがって、画面遷移を行う場合は templateApplicationScene で渡ってくる CPInterfaceController を持ち回して実装することになります。

地図を表示する

さて、起動時にナビゲーション用のテンプレートが表示できるようになった、と言ってもテンプレートに何もセットしていないため
起動しても以下の様なただ黒い画面が表示されるだけです。
スクリーンショット 2020-12-17 11.07.42.png

なので簡単に地図を表示してみます。
前項で CarPlay 対応には基本的に ViewController は出てきません、と書いたのですが例外があります。
ナビーゲーションアプリの開発に使われる CPMapTemplate は地図表示部分に関してのみ ViewController を使うことになります。
具体的には CPWindowrootViewController プロパティに地図表示用の ViewController をセットすると
地図が表示されるようになります。
(MapViewController 自体のコードは非常に単純なものなので割愛)

CarPlaySceneDelegate.Swift

    func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, 
                                  didConnect interfaceController: CPInterfaceController, 
                                  to window: CPWindow) {
        self.interfaceController = interfaceController
        self.window = window

        let mapTemplate = CPMapTemplate()
        interfaceController.setRootTemplate(mapTemplate, animated: true) { _,_ in
           // MapViewController は内部的に MKMapView を保持した ViewController
           window.rootViewController = MapViewController()
        }
    }

すると以下のように Apple の地図が表示されるようになります。
carplaysample.gif

よくみると上に黒いバーがあるのがわかると思うのですが、ここにボタンを置いて行って様々なインタラクションを行えるようにしていくことになります。
地図のパンニングやルート表示についてをここで説明すると長くなりすぎるため、興味があるようでしたら
参考文献のGuideとサンプルコードを参照することをお勧めします

実装する上での留意点

以上が CarPlay のナビゲーションアプリで地図を表示するまでの実装になります。
様々なテンプレートが用意されているため実装量がすくなく結構簡単に作れることがわかったと思います。
(それまでの Apple とのやりとりの方がコストが高い)

ですが、実装していく上で何点か留意すべき点があったため記述します。

タップ範囲が決められてる

CPMapTemplate はほかのテンプレートと違い ViewController を表示できるのでデザインの自由度は高いように思えます。
(実際、地図以外も表示させることは可能です)
ですが、実は ViewController にタップイベントを伝搬させることはできません。
CPMapTemplate におかれたボタン経由でしかイベントを伝搬させることしかできず、地図を直接スワイプさせたりすることはできません。
そして CPMapTemplate のみならず他のテンプレートもボタンの表示数、領域等に制限があります。
結果的にそれらの条件を考慮する必要があり、自由度の高いデザインができず、どのアプリも UI は似たものになります。

カスタムテンプレートを作れない

CPMapTemplate の基底クラスである CPTemplate は他のテンプレートクラスでも使われており画面遷移機能を持たないので
UIView に近い印象を受けるのですが開発者側のほうで継承させてカスタムテンプレートを作る、ということはできません。

Swift の言語仕様上、継承自体はできるのですが CPInterfaceController を使って画面に表示させようとすると
以下のエラーを吐いてクラッシュします

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Unsupported object <View.Test: 0x60000047e920> <identifier: F6B164D2-E640-4644-8C06-AAFB57AD7192, userInfo: (null), tabTitle: (null), tabImage: (null), showsTabBadge: 0> passed to setRootTemplate:animated:completion:. Allowed classes: {(
    CPContactTemplate,
    CPPointOfInterestTemplate,
    CPGridTemplate,
    CPMapTemplate,
    CPTabBarTemplate,
    CPInformationTemplate,
    CPListTemplate,
    CPNowPlayingTemplate,
    CPSearchTemplate
)}'

標準の組み込まれたテンプレートでなければいけない、ということらしいです。

カテゴリによっては使えないテンプレートがある

Apple に申請を出した際、CarPlay 向けにどのような機能を提供するかカテゴリを決める必要があることは申請の項目で説明しましたが、
その選んだカテゴリによって表示できる UI 、つまり CPTemplate 系のクラスが決まります。
対応表は以下のようになっています

スクリーンショット 2020-12-18 15.06.06.png

この表をみるとナビゲーションアプリは表示できる UI が多いのですが Now Playing 用と Point of Interest 用のテンプレートである
CPNowPlayingTemplateCPPointOfInterestTemplate が表示できないことになっています。
実際、表示しようとすると

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Unsupported object

というエラーを吐いて落ちます。

テンプレートによっては画面遷移の仕方に制限がある(し、公式ドキュメントにずれがある)

基本的に CPInterfaceControllerpresentTemplate 及び pushTemplate というメソッドを使って画面遷移をさせていくのですが
presentTemplate に関しては

CarPlay can only present one modal template at a time. templateToPresent must be one of CPActionSheetTemplate, CPAlertTemplate, or CPVoiceControlTemplate.

と制限があり
pushTemplate に関しては

use pushTemplate with a supported CPTemplate class such as CPGridTemplate, CPListTemplate, CPSearchTemplate, or
CPVoiceControlTemplate.

とあり指定されたクラスしか使えないような記述がされています。
が、実は CPMapTemplatepushTemplate を使って表示させることができたりします。
また、

All apps are limited to pushing up to 5 templates in depth, including the root template. Quick food ordering
apps are limited to 2 templates in depth.

とあり、pushTemplate を使って画面遷移したとしても5階層以上いけないようになっている様です。(未検証)

CarPlay 対応アプリを呼び出すには

カーナビ上で操作を完結させるために CarPlay 対応アプリから URL スキーム等つかって他の CarPlay アプリを呼び出したい時があると思います(ex. Calendar, music, etc...)
通常の iOS アプリでは UIApplication.shared.open(url:options:complicationHandler) を使ってたと思うのですが、
CarPlay 上でそれを使うと CarPlay 側ではなにも起きずカーナビに繋いでる iPhone 側の方で別アプリが呼び出されます。

CarPlay 側でアプリ呼び出しをしたい場合は CPTemplateApplicationSceneDelegate のデリゲートメソッドで渡される templateApplicationSceneopen(url:options:complicationHandler) の方をつかう必要があります。

CarPlaySceneDelegate.swift
func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene,
                              didConnect interfaceController: CPInterfaceController, to window: CPWindow) {
    // こちらを使うと iPhone 側でアプリが起動する
    UIApplication.shared.open(URL(string: "なんらかの URL スキーマ")!, options: nil, complicationHandler: nil)]
    // こちらを使うと CarPlay 側のアプリが起動する
    templateApplicationScene.open(URL(string: "なんらかの URL スキーマ")!, options: nil, complicationHandler: nil)
}

個人的には iPhone 側の方でも UIApplication.shared.open を使うのは基本的にやめて UIScene.open を使うべきな気はしています。

まとめ

以上、CarPlay 対応のナビゲーションアプリの作り方と注意点を紹介してみました。
読んでいただいてわかると思うのですがカーナビという安全に直結するアプリケーションの UI であるため
非常に縛りが多く、自由なレイアウトというのはできない設計になります。

とはいえ、主観ではありますが従来の広く販売されているカーナビと比べるとやはりだいぶ使いやすいです。
また、ナビゲーションアプリに関しては ViewController を使って地図を表示してるので
その気になれば動画や音楽を流せる気がしています(未検証)。

Apple の審査に落とされそうな気がしてますが審査に出さず個人でやる分には問題ないはずなので
どこかのタイミングで実験して記事にしたいなと考えています。

参考文献

26
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?