mcpField
C
carplay

carplay

github.com/dpearson2699開発1.2k3.1k 回インストール13 週間前に更新
リポジトリ

carplay について

Swift開発者向けのCarPlay開発スキルです。カテゴリ別の権限とシステムテンプレートを使うアプリについて、シーン設定、ナビゲーション、オーディオ、通信、地点表示、シミュレーターでのテストを扱います。

主な機能

  1. 01CarPlayカテゴリ権限の設定
  2. 02CarPlayシーンとデリゲートの構成
  3. 03システムテンプレートによる画面構築
  4. 04ナビ・オーディオ・通信フローの実装
  5. 05地点情報と詳細の表示
  6. 06CarPlay Simulatorでのテスト

活用シーン

  1. 01車載ナビアプリの開発
  2. 02オーディオの閲覧と再生画面の表示
  3. 03車載メッセージ・連絡先画面の構築
  4. 04充電・駐車・飲食地点の表示

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/dpearson2699/swift-ios-skills --skill carplay

インストール後、Claude Code で/carplayを入力して読み込まれたか確認します。

同梱ファイルevals/evals.jsonreferences/carplay-patterns.md

詳細

CarPlay

車載ディスプレイ向けに、カテゴリのエンタイトルメントを持つテンプレートベースの CarPlay アプリを構築します。 対象範囲:Swift 6.3、iOS 26 以降。

完全なナビゲーションセッション、ダッシュボードシーン、高度なテンプレート構成など、拡張パターンについてはreferences/carplay-patterns.mdを参照してください。

対象範囲の境界:CarPlay フレームワークを使用する完全なアプリでは、カテゴリのエンタイトルメント、CPTemplateApplicationScene、CPTemplateApplicationSceneDelegate、CPInterfaceController、およびシステムのCPTemplateナビゲーションを使用します。CarPlay に表示される WidgetKit ウィジェットと ActivityKit Live Activities は、別個のシステム体験です。その実装はそれぞれの領域に振り分け、CarPlay 固有の検証はここで行ってください。

目次

エンタイトルメントとセットアップ

Apple の CarPlay エンタイトルメント申請フォームからカテゴリ固有のエンタイトルメントを申請し、追加条項に同意した後、承認されたカテゴリキーを以下のようにプロビジョニングしてください。

カテゴリ別のエンタイトルメントキー
エンタイトルメントカテゴリ
com.apple.developer.carplay-audioオーディオ
com.apple.developer.carplay-communicationコミュニケーション
com.apple.developer.carplay-mapsナビゲーション
com.apple.developer.carplay-chargingEV 充電
com.apple.developer.carplay-parking駐車
com.apple.developer.carplay-quick-orderingクイックフード注文
プロジェクトの構成
  1. デベロッパーポータルの「追加機能」で App ID を更新します。
  2. 更新した App ID 用の新しいプロビジョニングプロファイルを生成します。
  3. Xcode で自動署名を無効にし、CarPlay プロビジョニングプロファイルを読み込みます。
  4. エンタイトルメントキーをtrueに設定したEntitlements.plistを追加します。
  5. Code Signing Entitlements のビルド設定にEntitlements.plistのパスを設定します。
主な型
型役割
CPTemplateApplicationSceneCarPlay ディスプレイ用の UIScene サブクラス
CPTemplateApplicationSceneDelegateシーンの接続/切断ライフサイクル
CPInterfaceControllerルートテンプレートの設定、テンプレートのプッシュ、表示、ポップを行う CarPlay 提供のコントローラー
CPTemplateすべての CarPlay テンプレートの抽象基底クラス
CPSessionConfiguration車載ディスプレイの制限とコンテンツスタイル

シーンの構成

Info.plistで CarPlay シーンを宣言し、CarPlay の接続時に応答するようCPTemplateApplicationSceneDelegateを実装します。

Info.plist のシーンマニフェスト
<key>UIApplicationSceneManifest</key>
<dict>
    <key>UIApplicationSupportsMultipleScenes</key>
    <true/>
    <key>UISceneConfigurations</key>
    <dict>
        <key>CPTemplateApplicationSceneSessionRoleApplication</key>
        <array>
            <dict>
                <key>UISceneClassName</key>
                <string>CPTemplateApplicationScene</string>
                <key>UISceneConfigurationName</key>
                <string>CarPlaySceneConfiguration</string>
                <key>UISceneDelegateClassName</key>
                <string>$(PRODUCT_MODULE_NAME).CarPlaySceneDelegate</string>
            </dict>
        </array>
    </dict>
</dict>
シーンデリゲート(ナビゲーション以外)

ナビゲーション以外のアプリが受け取るのはインターフェースコントローラーのみです。ウィンドウはありません。

import CarPlay

final class CarPlaySceneDelegate: UIResponder,
    CPTemplateApplicationSceneDelegate {

    var interfaceController: CPInterfaceController?

    func templateApplicationScene(
        _ templateApplicationScene: CPTemplateApplicationScene,
        didConnect interfaceController: CPInterfaceController
    ) {
        self.interfaceController = interfaceController
        interfaceController.setRootTemplate(buildRootTemplate(),
                                            animated: true, completion: nil)
    }

    func templateApplicationScene(
        _ templateApplicationScene: CPTemplateApplicationScene,
        didDisconnectInterfaceController interfaceController: CPInterfaceController
    ) {
        self.interfaceController = nil
    }
}
シーンデリゲート(ナビゲーション)

ナビゲーションアプリはインターフェースコントローラーとCPWindowの両方を受け取ります。 地図コンテンツを描画するには、ウィンドウのルートビューコントローラーを設定します。

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

    let mapTemplate = CPMapTemplate()
    mapTemplate.mapDelegate = self
    interfaceController.setRootTemplate(mapTemplate, animated: true,
                                        completion: nil)
}

テンプレートの概要

CarPlay には固定のテンプレートタイプが用意されています。アプリがコンテンツを提供し、システムが車載ディスプレイに表示します。

汎用テンプレート
テンプレート用途
CPTabBarTemplateタブ付きの子テンプレートを格納するコンテナー
CPListTemplateスクロール可能なセクション別リスト
CPGridTemplateタップ可能なアイコンボタンのグリッド(最大 8 個)
CPInformationTemplate最大 3 個のアクションを備えたキーと値の情報
CPAlertTemplate最大 2 個のアクションを備えたモーダルアラート
CPActionSheetTemplateモーダルアクションシート
カテゴリー別テンプレート
テンプレートカテゴリー
CPMapTemplateナビゲーション -- ナビゲーションバー付きの地図オーバーレイ
CPSearchTemplateナビゲーション -- 目的地の検索
CPNowPlayingTemplateオーディオ -- 共有の「再生中」画面
CPPointOfInterestTemplateEV 充電/駐車場/飲食 -- POI マップ
CPContactTemplateコミュニケーション -- 連絡先カード
ナビゲーション階層

pushTemplate(_:animated:completion:) を使用してテンプレートをスタックに追加します。 モーダル表示には presentTemplate(_:animated:completion:) を使用します。 戻るには popTemplate(animated:completion:) を使用します。 CPTabBarTemplate はルートに設定する必要があり、プッシュまたは表示することはできません。

CPTabBarTemplate
let browseTab = CPListTemplate(title: "Browse",
                               sections: [CPListSection(items: listItems)])
browseTab.tabImage = UIImage(systemName: "list.bullet")

let tabBar = CPTabBarTemplate(templates: [browseTab, settingsTab])
tabBar.delegate = self
interfaceController.setRootTemplate(tabBar, animated: true, completion: nil)
CPListTemplate
let item = CPListItem(text: "Favorites", detailText: "12 items")
item.handler = { selectedItem, completion in
    self.interfaceController?.pushTemplate(detailTemplate, animated: true,
                                           completion: nil)
    completion()
}

let section = CPListSection(items: [item], header: "Library",
                            sectionIndexTitle: nil)
let listTemplate = CPListTemplate(title: "My App", sections: [section])

ナビゲーションアプリ

ナビゲーションアプリでは com.apple.developer.carplay-maps を使用します。地図コンテンツの描画に CPWindow を受け取るのは、このカテゴリーだけです。ルートテンプレートには CPMapTemplate を設定する必要があります。

トリップのプレビューとルート選択
let routeChoice = CPRouteChoice(
    summaryVariants: ["Fastest Route", "Fast"],
    additionalInformationVariants: ["Via Highway 101"],
    selectionSummaryVariants: ["25 min"]
)
let trip = CPTrip(origin: origin, destination: destination,
                  routeChoices: [routeChoice])
mapTemplate.showTripPreviews([trip], textConfiguration: nil)
ナビゲーションセッションの開始
extension CarPlaySceneDelegate: CPMapTemplateDelegate {
    func mapTemplate(_ mapTemplate: CPMapTemplate,
                     startedTrip trip: CPTrip,
                     using routeChoice: CPRouteChoice) {
        let session = mapTemplate.startNavigationSession(for: trip)
        session.pauseTrip(for: .loading, description: "Calculating route...")

        let maneuver = CPManeuver()
        maneuver.instructionVariants = ["Turn right onto Main St"]
        maneuver.symbolImage = UIImage(systemName: "arrow.turn.up.right")
        session.upcomingManeuvers = [maneuver]

        let estimates = CPTravelEstimates(
            distanceRemaining: Measurement(value: 5.2, unit: .miles),
            timeRemaining: 900)
        session.updateEstimates(estimates, for: maneuver)
    }
}
地図ボタン
let zoomIn = CPMapButton { _ in self.mapViewController.zoomIn() }
zoomIn.image = UIImage(systemName: "plus.magnifyingglass")
mapTemplate.mapButtons = [zoomIn, zoomOut]
CPSearchTemplate
extension CarPlaySceneDelegate: CPSearchTemplateDelegate {
    func searchTemplate(_ searchTemplate: CPSearchTemplate,
                        updatedSearchText searchText: String,
                        completionHandler: @escaping ([CPListItem]) -> Void) {
        performSearch(query: searchText) { results in
            completionHandler(results.map {
                CPListItem(text: $0.name, detailText: $0.address)
            })
        }
    }

    func searchTemplate(_ searchTemplate: CPSearchTemplate,
                        selectedResult item: CPListItem,
                        completionHandler: @escaping () -> Void) {
        // Navigate to selected destination
        completionHandler()
    }
}

オーディオアプリ

オーディオアプリでは com.apple.developer.carplay-audio を使用します。リストに閲覧可能なコンテンツを表示し、再生コントロールには CPNowPlayingTemplate を使用します。オーディオの権限を持つアプリでは CPInformationTemplate を利用できません。

「再生中」テンプレート

CPNowPlayingTemplate は共有シングルトンです。MPNowPlayingInfoCenter からメタデータを読み込みます。新しいインスタンスを作成しないでください。

let nowPlaying = CPNowPlayingTemplate.shared
nowPlaying.isUpNextButtonEnabled = true
nowPlaying.isAlbumArtistButtonEnabled = true
nowPlaying.updateNowPlayingButtons([
    CPNowPlayingShuffleButton { _ in self.toggleShuffle() },
    CPNowPlayingRepeatButton { _ in self.toggleRepeat() }
])
nowPlaying.add(self) // Register as CPNowPlayingTemplateObserver
Siri アシスタントセル

INPlayMediaIntent をサポートするオーディオアプリでは、アシスタントセルを表示できます。コミュニケーションアプリでは .startCall とともに INStartCallIntent を使用します。

let config = CPAssistantCellConfiguration(
    position: .top, visibility: .always, assistantAction: .playMedia)
let listTemplate = CPListTemplate(
    title: "Playlists",
    sections: [CPListSection(items: items)],
    assistantCellConfiguration: config)

コミュニケーションアプリ

コミュニケーションアプリでは com.apple.developer.carplay-communication を使用します。メッセージリストと連絡先を表示し、Siri による発信に INStartCallIntent をサポートします。 CPMessageListItem には、アプリ側で指定する選択ハンドラーがありません。選択すると、その項目の電話番号/メールアドレス、未読状態、または既存の会話設定に基づいて、CarPlay が Siri による作成、読み上げ、返信の動作を実行します。

let leading = CPMessageListItemLeadingConfiguration(
    leadingItem: .star, leadingImage: nil, unread: true)
let trailing = CPMessageListItemTrailingConfiguration(
    trailingItem: .none, trailingImage: nil)

let message = CPMessageListItem(
    conversationIdentifier: "conv-123",
    text: "Jane",
    leadingConfiguration: leading,
    trailingConfiguration: trailing,
    detailText: "Meeting at 3pm",
    trailingText: "2:45 PM")

let messageList = CPListTemplate(title: "Messages",
                                 sections: [CPListSection(items: [message])])

POI アプリ

EV 充電、駐車場、飲食の注文アプリでは、CPPointOfInterestTemplate と CPInformationTemplate を使用して場所と詳細を表示します。 CPPointOfInterestTemplate には、最大 12 件の POI が表示されます。

CPPointOfInterestTemplate
let poi = CPPointOfInterest(
    location: MKMapItem(placemark: MKPlacemark(
        coordinate: CLLocationCoordinate2D(latitude: 37.7749,
                                           longitude: -122.4194))),
    title: "SuperCharger Station", subtitle: "4 available",
    summary: "150 kW DC fast charging",
    detailTitle: "SuperCharger Station", detailSubtitle: "$0.28/kWh",
    detailSummary: "Open 24 hours",
    pinImage: UIImage(systemName: "bolt.fill"))

poi.primaryButton = CPTextButton(title: "Navigate",
                                 textStyle: .confirm) { _ in }

let poiTemplate = CPPointOfInterestTemplate(
    title: "Nearby Chargers", pointsOfInterest: [poi], selectedIndex: 0)
poiTemplate.pointOfInterestDelegate = self
CPInformationTemplate
let infoTemplate = CPInformationTemplate(
    title: "Order Summary", layout: .leading,
    items: [
        CPInformationItem(title: "Item", detail: "Burrito Bowl"),
        CPInformationItem(title: "Total", detail: "$12.50")],
    actions: [
        CPTextButton(title: "Place Order", textStyle: .confirm) { _ in
            self.placeOrder() },
        CPTextButton(title: "Cancel", textStyle: .cancel) { _ in
            self.interfaceController?.popTemplate(animated: true,
                                                  completion: nil) }])

CarPlay シミュレータでのテスト

  1. Xcode で iOS シミュレータを使ってビルドし、実行します。
  2. I/O > External Displays > CarPlay を選択します。

デフォルトのウィンドウサイズは 800x480、スケールは @2x です。ナビゲーションアプリでは、追加のオプションを有効にします。

defaults write com.apple.iphonesimulator CarPlayExtraOptions -bool YES
推奨テスト構成
構成ピクセル数スケール
最小748 x 456@2x
縦向き768 x 1024@2x
標準800 x 480@2x
高解像度1920 x 720@3x

シミュレータでは、iPhone のロック中の動作、Siri、カーラジオとのオーディオの共存、物理入力ハードウェア(ノブ、タッチパッド)をテストできません。可能な場合は、CarPlay 対応の実車またはアフターマーケットのヘッドユニットでテストしてください。 CarPlay の使用中に iPhone の操作を必要としないよう、主要な CarPlay フローを設計してください。

よくある間違い

やってはいけないこと:間違ったシーンデリゲートメソッドを使う

ナビゲーションアプリでは templateApplicationScene(_:didConnect:to:)(CPWindow を使用)を実装する必要があります。ナビゲーション以外のアプリでは templateApplicationScene(_:didConnect:)(ウィンドウなし)を使用します。間違ったメソッドを使うと、CarPlay の UI が表示されません。

やってはいけないこと:ナビゲーションウィンドウにカスタム UI を描画する

CPWindow は地図コンテンツ専用です。すべてのオーバーレイ、アラート、コントロールには CarPlay テンプレートを使用する必要があります。

やってはいけないこと:CPTabBarTemplate をプッシュまたは表示する

CPTabBarTemplate はルートにのみ設定できます。プッシュまたは表示すると失敗します。setRootTemplate(_:animated:completion:) を使用してください。

やってはいけないこと:CPNowPlayingTemplate をインスタンス化する

CPNowPlayingTemplate.shared を使用してください。新しいインスタンスを作成すると、問題が発生します。

禁止:CPMessageListItem にハンドラーを追加する

CPMessageListItem は Siri によって管理されます。CPListItem とは異なります。
message.handler は設定せず、コンテキストにはアイテムの設定と userInfo を使用してください。

禁止:ウィジェットを CarPlay テンプレートアプリとして扱う

CarPlay に表示されるウィジェットとライブアクティビティは、WidgetKit と ActivityKit に属します。このスキルは、カテゴリのエンタイトルメントを持つ CarPlay テンプレートアプリのシーンと、車内でのそれらの表示面の検証に使用してください。

禁止:車載ディスプレイの制限を無視する

CPSessionConfiguration.limitedUserInterfaces を確認し、リストテンプレートの maximumItemCount / maximumSectionCount を守ってください。

禁止:完了ハンドラーの呼び出しを忘れる

CPListItem.handler は、すべてのコードパスで完了ハンドラーを呼び出す必要があります。呼び出さないと、リストが読み込み中の状態のままになります。

レビューチェックリスト

  • Entitlements.plist の CarPlay エンタイトルメントキーが正しい
  • UIApplicationSupportsMultipleScenes が true に設定されている
  • Info.plist に CPTemplateApplicationSceneSessionRoleApplication シーンがある
  • シーンデリゲートのクラス名が UISceneDelegateClassName と一致している
  • 正しいデリゲートメソッドを使用している(CPWindow の有無)
  • 戻る前に didConnect でルートテンプレートを設定している
  • 切断時にインターフェイスコントローラーとウィンドウへの参照をクリアしている
  • CPTabBarTemplate はルートとしてのみ使用し、プッシュしていない
  • 新しいインスタンスではなく CPNowPlayingTemplate.shared を使用している
  • 通信の行では、カスタムハンドラーを設定せずに CPMessageListItem を使用している
  • WidgetKit / ActivityKit の表示面を CarPlay テンプレートアプリのコード外に振り分けている
  • リストにデータを入力する前に maximumItemCount / maximumSectionCount を確認している
  • CPListItem.handler がすべてのコードパスで完了ハンドラーを呼び出している
  • 地図のみのコンテンツは CPWindow のルートビューコントローラーに配置している(ナビゲーションアプリ)
  • iPhone がロックされていてもアプリが動作する
  • 最小、標準、高解像度のシミュレーターサイズでテストしている
  • 再生中でないときにオーディオセッションを非アクティブ化している

参考資料

コメント