はじめに
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
- 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で実行するのは非常に簡単です。
- まず、Xcodeを開いたら、該当プロジェクトを選択し、既存アプリのターゲットを選択
- 「General」タブの「Supported Destinations」を確認する
- ここに「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を使っています。
ターゲットの追加
新規にターゲットを追加する場合には、まず以下の手順を行います。
- ターゲット一覧の下の「+」を押す
- visionOSタブを選択
- appを選択
- 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に公開しているソースコードはこちらからご覧いただけます。
参考
- visionOSの紹介
- Develop your first immersive app
- Bringing your existing apps to visionOS
- Bringing your ARKit app to visionOS
- Adding 3D content to your app
- Checking whether your existing app is compatible with visionOS
- Checking whether your existing app is compatible with visionOS
- windowStyle