Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails以外の開発一般

既存のiOSアプリをvisionOS(Apple Vision Pro)に対応する

はじめに

8月21日の記事を担当させていただく yoshi.k と申します。弊社では主にモバイルアプリの開発を担当しています。
最近は目の下にクマをつけたポケモンたちを酷使して、カビゴンの餌を集めさせております。

さて、少し前に行われたWWDC2023で、待望のApple製MRデバイスであるApple Vision Proが発表されましたね。
AppleのMR端末のお話は以前から噂にはなっていましたが、ついにきたかと胸躍りました。
そんなApple Vision Proですが、執筆時点ではXcode15のベータ版にて、Apple Vision Proのシミュレーターにて動作の確認が可能となっています。

昨年執筆した「SwiftUIの勉強にTechRacho Feed Readerを作ってみた」でTechRachoの記事を読み込むアプリを作成しましたが、今回はこのアプリをApple Vision Proシミュレーターで動かせるようにしたいと思います。

開発環境

  • MacBook Pro 2.3(16-inch, 2019) GHz 8コアIntel Core i9
  • Xcode15 beta6

iOSアプリをvisionOSで動かす

注意点

正直なところ、この方法ではすでにアプリが存在する場合には、やることはあまりありません。
しかし、注意点があります。

当然ながらハードウェアやvisionOSに備わっていない機能は使うことはできません。
これらのフレームワークを使用するコードは、可能な限り別のソースファイルに移動し、それらのファイルはiOSバージョンのアプリにのみインクルードすることが推奨されています。
それができない場合には、下記のような形でiOS, visionOSで処理を分岐します。

#if os(visionOS)
   // visionOS code
#elseif os(iOS)
   // iOS code
#endif

下記のフレームワークはvisionOS SDKでは利用できません。

  • ActivityKit
  • AdSupport
  • AppClip
  • AutomatedDeviceEnrollment
  • BusinessChat
  • CarKey
  • CarPlay
  • Cinematic
  • ClockKit
  • CoreLocationUI
  • CoreMediaIO
  • CoreNFC
  • CoreTelephony
  • DeviceActivity
  • DockKit
  • ExposureNotification
  • FamilyControls
  • FinanceKit
  • FinanceKitUI
  • ManagedSettings
  • ManagedSettingsUI
  • Messages
  • MLCompute
  • NearbyInteraction
  • OpenAL
  • ProximityReader
  • RoomPlan
  • SafetyKit
  • ScreenTime
  • SensorKit
  • ServiceManagement
  • Social
  • Twitter
  • WidgetKit
  • WorkoutKit

また、下記のフレームワークはvisionOSでは挙動が変わります。

フレームワーク  差異
ARKit iOSとvisionOSで異なるAPIを利用する必要がある、詳しくは公式ドキュメントを参照
AVFoundation キャプチャーインターフェースが使用できない、サービス利用可否のチェック機能があるのでそれで確認する
CallKit VoIP機能は利用できるが、電話番号認証、着信拒否などの携帯電話関連サービスの利用が不可
ClockKit visionOSでは何もしない
CoreHaptics visionOSは触覚フィードバックの代わりに音声フィードバックが使われる
CoreLocation 標準の位置情報サービスを使って誰かの位置情報を要求することはできるが他のほとんどのサービスは利用できない、サービス利用可否のチェック機能があるのでそれで確認する
CoreMotion 気圧計のデータは利用できないが、他のほとんどのセンサーは利用可能、サービス利用可否のチェック機能があるのでそれで確認する
HealthKit と HealthKitUI Healthデータは利用できない、サービス利用可否のチェック機能があるのでそれで確認する
MapKit ユーザートラッキング機能が利用できない
MediaPlayer 一部のAPIは利用できない
MetricKit デバイス上の診断ログを収集し、レポートを作成することはできるが、メトリクスを収集できない
MusicKit 一部のAPIが利用できない
NearbyInteraction visionOSではなにもしない、サービス利用可否のチェック機能があるのでそれで確認する
PushToTalk Push to Talkサービスが利用できない、PTChannelManagerの作成時にエラーが出ないか確認する
SafariServices SFSafariViewController で開くリンクは、Safariアプリで開く
UIKit 最大2つの同時タッチ入力を報告する、また、複数の指を必要とするズームや回転のジェスチャーを含めすべてのgesture recognizerの入力は正しく処理する、2本以上の指を必要とするカスタムジェスチャー認識機能を利用している場合には、visionOSで1回または2回のタッチのみをサポートするように更新する必要がある ※要確認ですが標準ジェスチャーは2本指タップは認識するが、カスタムジェスチャーは1本指のジェスチャーのみ認識するのかなと筆者は解釈しています
VisionKit DataScannerViewController APIは使用不可、その他の機能は利用可能
WatchConnectivity iPhoneとApple Watch間の接続のみをサポートしている、サービス利用可否のチェック機能があるのでそれで確認する

visionOSで実行する

iOSと同じアプリをvisionOSで実行するのは非常に簡単です。

  1. まず、Xcodeを開いたら、該当プロジェクトを選択し、既存アプリのターゲットを選択
  2. 「General」タブの「Supported Destinations」を確認する
  3. ここに「Apple Vision」が存在しない場合には、「+」を押して「Apple Vision」を追加する

あとは、Apple Vision Proシミュレーターを選択すれば、Apple Vision上で動かすことができます。
iOSアプリをそのまま実行した場合には、1枚のウィンドウとして空間上に表示されます。

実装中に遭遇した問題

当アプリではCocoaPodsで外部ライブラリなどを導入していますが、試しに実装した時点(8月14日)では、ビルド時に以下のエラーが発生することがありました。

DT_TOOLCHAIN_DIR cannot be used to evaluate LIBRARY_SEARCH_PATHS, use TOOLCHAIN_DIR instead

どうやら、Xcode beta5以降から発生するCocoaPodsの問題のようで、現在修正中の模様です。
v1.13.0で解消されるようなので、同じような問題が発生した場合は時間が解決してくれると思います。
https://github.com/CocoaPods/CocoaPods/pull/12009

iOSの共通処理を利用しつつ、visionOS用のUI(コンテンツ)を作成

折角なので、内部を共通処理にしつつ、visionOS用のUIを表示するアプリを作成しました。
visionOSのみのAPIを使用するため、visionOS専用のアプリとして作成していますが、同じターゲットでも実装方法があるかもしれません。

ちなみにvisionOS専用アプリにCocoaPodsのライブラリを導入する方法が分からなかったのと、今回の本質の部分からは外れるので、CocoaPodsのライブラリを利用しているデータ取得部分にStubを使っています。

ターゲットの追加

新規にターゲットを追加する場合には、まず以下の手順を行います。

  1. ターゲット一覧の下の「+」を押す
  2. visionOSタブを選択
  3. appを選択
  4. Nextを押す

iOSアプリの開発者なら見慣れた画面が新規作成画面が出てきますが、
Initial Scene, Immersive Space Renderer, Immersive Space というオプションがあるかと思います。

今回は、Initial SceneにVolumeを選択し、他はNoneを選びます。
ターゲット名を「TechRachoFeedReaderForVision」にし、Finishで新しくターゲットを追加しました。
ターゲットを追加すると、いくつかのファイルが追加されます。

あとは、共通で使うファイルをターゲットに追加しましょう。

なお、ターゲット追加時にRealityKitContentというパッケージが追加されてますが、Xcode15 beta6ではターゲット追加後にXcodeを再起動しないでビルドすると、存在しないエラーとなります。
これにハマり半日溶けました...

各オプションについて

Initial Scene
  • window
    • 2次元コンテンツを表示するように初期設計されます
    • 平面寸法で使用者がサイズは変更可能だが、深さは固定されます
  • volume
    • 3次元コンテンツを表示するように初期設計されます
    • サイズはアプリで制御され、使用者が制御できません
Immersive Space
  • None
    • 共有スペースとなり、他のアプリと一緒に表示されます
  • Mixed
    • フルスペースとなり、他のアプリが非表示となります
    • コンテンツの周囲は見える状態となります
  • Progressive
    • フルスペースとなり、他のアプリが非表示となります
    • コンテンツの周囲(180°)の映像を排除し、没入型コンテンツを提供します
  • Full
    • フルスペースとなり、他のアプリが非表示となります
    • 完全に周囲のアプリ環境を囲んで見えなくします
Immersive Space Renderer

最近できたオプションらしくあまりドキュメントが見当たりませんでしたが、オプション内容的にImmersive Spaceの時のレンダラーとしてRealityKitかMetalかを選ぶものかと思います。

visionOS用UI(コンテンツ)として実装

ターゲットを追加した際に追加されたTechRachoFeedReaderForVisionApp.swiftを見てみましょう。

import SwiftUI

@main
struct TechRachoFeedReaderForVisionApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }.windowStyle(.volumetric)
    }
}

WindowGroupに見慣れないwindowStyleというモディファイアが追加されています。
windowStyleはvisionOSのモディファイアとなっており外観を決定します。
先ほどvolumeを選択したため、.volumetricが設定されています。

.volumetricは、3Dボリュームウィンドウを作成するウィンドウスタイルとなります。

さて、3D空間上で実際にアプリが動くのか確認してみます。
ターゲットを追加した際に追加された、ContentView.swiftを少し編集して実行してみます。

import SwiftUI
import RealityKit
import RealityKitContent

struct ContentView: View {

    @State var enlarge = false

    var body: some View {
        let usecase = ArticleUsecase(remoteRepository: ArticleRepositoryStub(),
                                     cacheRepository: ArticleCacheRepositoryImpl())
        ZStack(alignment: .bottom) {
            ArticleListView(viewModel: ArticleListViewModel(usecase: usecase))

            RealityView { content in
                let model = ModelEntity(
                             mesh: .generateSphere(radius: 0.1),
                             materials: [SimpleMaterial(color: .white, isMetallic: true)])
                content.add(model)
            } update: { content in
                if let scene = content.entities.first {
                    let uniformScale: Float = enlarge ? 1.4 : 1.0
                    scene.transform.scale = [uniformScale, uniformScale, uniformScale]
                }
            }
            .gesture(TapGesture().targetedToAnyEntity().onEnded { _ in
                enlarge.toggle()
            })

            Toggle("Enlarge RealityView Content", isOn: $enlarge)
                .toggleStyle(.button)
        }
    }
}

先ほどは、平面だったアプリの空間に奥行きが追加されていることがわかります。
奥行きがあることがわかりやすいように、記事一覧のほかに3Dオブジェクトなんかも追加しています。
実装を見てみればわかると思いますが、簡易的なものであればほぼiOSのUIの実装と変わらない感覚で実装が可能そうです。

あとは、好きなようにvisionOS用の表示を作るだけです。
今回は簡易的に四角いオブジェクトに取得したサムネイル画像を適用し並べるように実装しました。

実装

今回TechRachFeedReaderに追加した実装です。
RealityKit等の理解はまだ浅いので、解説は割愛させてください。

TechRachoFeedReaderForVisionApp.swift

import SwiftUI

@main
struct TechRachoFeedReaderForVisionApp: App {
    var body: some Scene {
        let usecase = ArticleUsecase(remoteRepository: ArticleRepositoryStub(),
                                     cacheRepository: ArticleCacheRepositoryImpl())

        WindowGroup {
            CubeListView(viewModel: CubeListViewModel(usecase: usecase))
        }.windowStyle(.volumetric)
    }
}

CubeListView.swift

import SwiftUI
import RealityKit

struct CubeListView: View {

    @ObservedObject var viewModel: CubeListViewModel

    init(viewModel: CubeListViewModel) {
        self.viewModel = viewModel
    }

    var body: some View {
        VStack {
            Button("更新") {
                viewModel.reload()
            }
            .padding(50)

            HStack {
                ForEach(viewModel.dataSource) { ball in
                    VStack {
                        Text(ball.article.title ?? "")
                            .foregroundColor(Color.white)
                            .font(.title)

                        RealityView { content in
                            var material = SimpleMaterial()
                            material.baseColor = MaterialColorParameter.texture(ball.texture)
                            let entity = ModelEntity(mesh: .generateBox(size: 0.2), materials: [material])
                            content.add(entity)
                        }
                        .frame(width: 300, height: 300)
                    }
                }
            }
        }
    }
}

BallListViewModel.swift

import Combine
import SwiftUI
import RealityKit

struct CubeEntry: Identifiable {
    var id: Article {
        self.article
    }
    let article: Article
    let texture: TextureResource
}

class CubeListViewModel: ObservableObject, Identifiable {
    @Published var dataSource: [CubeEntry] = []

    private let usecase: ArticleUsecase
    private var disposables = Set<AnyCancellable>()

    init(usecase: ArticleUsecase) {
        self.usecase = usecase
        setup()
    }

    func reload() {
        updateState(publisher: usecase.update())
    }

    private func setup() {
        updateState(publisher: usecase.getCache())
    }

    private func updateState(publisher: AnyPublisher<[Article], Error>) {
        publisher
            .receive(on: DispatchQueue.main)
            .sink(
                receiveCompletion: { _ in },
                receiveValue: { articles in

                    var textures :[CubeEntry] = []
                    for article in articles {
                        do {
                            let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent(article.title ?? "")
                            let data = try! Data(contentsOf: article.thumbnailImageURL!)
                            try! data.write(to: fileURL)

                            let texture = try TextureResource.load(contentsOf: fileURL)
                            textures.append(CubeEntry(article: article, texture: texture))
                        } catch {
                            // 何もしない
                            print(error.localizedDescription)
                        }
                    }
                    self.dataSource = textures;
                }
            )
            .store(in: &disposables)
    }
}

実機での確認について

作成したアプリを実機で確認する方法についてはいくつかあります。

Apple Vision Pro developer kitに申し込む

現在AppleはApple Vision Pro developer kitの申し込みを受け付けています。
審査が必要ですが、承認されればApple Vision Proを開発者に貸し出してくれます。

https://developer.apple.com/visionos/developer-kit/

Apple Vision Proで評価してもらうようリクエストする

TestFlight経由で、アプリの動作評価のリクエストをすることができます。
評価結果をキャプチャやクラッシュログとともに送ってくれるようです。

https://developer.apple.com/visionos/compatibility-evaluations/

Apple Vision Proデベロッパラボに参加する

定期的に開催されるApple Vision Proデベロッパラボにて作ったアプリを試すことが可能です。
事前申し込みが必要となります。

https://developer.apple.com/jp/visionos/labs/

終わりに

既存アプリをvisionOSに対応するのは非常に簡単ですが、折角ならvisionOSの強みを活かしたアプリを作りたいですね。

Apple Visionは次の「iPhone」になるのか...今後も目が離せません!

以前の記事で公開したTechRachoFeedReaderのソースに今回の更新分を適用しました。
GitHubに公開しているソースコードはこちらからご覧いただけます。

参考



CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。