[VisionOS開発メモ]RealityViewのupdateクロージャーが呼ばれるタイミング
VisionOSを開発してる際に、3Dモデルを表示するRealityViewの仕様が分からず、調べたのでメモしておきたいと思います。経緯としては、updateという関数が出るとどうしてもUnityのupdateがよぎってしまい、update呼ばれるはずでは。。ということが多々起こってしまったためです。ここはSwiftUIの世界。
RealityViewのパラメータ
RealityViewのパラメータは、4つあります。こちらの記事がとても詳しく書かれているので、その説明を引用すると下記のようになります。
- make: A closure to set up and configure the initial content asynchronously.
- placeholder: A view displayed while the initial content is being loaded.
- update: An optional closure to update the content in response to state changes.
- attachments: To add views into your RealityKit content.
ざっくり日本語で和訳すると
- make: RealityViewの初期化
- placeholder: ロード前に表示するView
- update: 状態が変化した場合に呼ばれるオプショナルなクロージャー
- attachments: Attachmentの追加
となります。
今回は、このパラメータの中のupdateクロージャーについての話になります。
上の定義では状態が変化した際に呼ばれる
と書かれていますが、どうゆう状態まで指しているのかがわかりません。コード上のRealityKitのコメントでは
- update: An optional closure that updates the ``RealityView``instance's content as the view's state changes.
View`s State という表現がされており、Viewの変更があった場合にupdateが呼ばれるようです。
結論
調べたものの結論を先に書いておきます。足りないものもたくさんあると思うので、今上がっているものだけではないと思っていただければ幸いです。
【updateが呼ばれる場合】
- makeクロージャーの処理が終了時
- updateクロージャーで、@Stateのついた値が利用され、更新された時
- updateクロージャーで、@Observableのついた値が利用され、更新された時
【updateが呼ばれない場合】
- makeクロージャーで、@Stateや@Observable等の付いた値が利用され、更新された時
- makeクロージャーで、生成したEntityのポジションが変更された時
- makeクロージャーで、contentにAddしたEntityに他のEntityをAddChildした時
- attachmentsクロージャで定義されているAttachmentのView内で@Stateや@Observable等の付いた値が利用され、更新された時
updateが呼ばれる場合について
基本的には、SwiftUIのView内部で@Stateや@Observableなどの監視している値が利用され、更新された際に再描画される場合と同じ仕様です。具体的には、updateクロージャー内で@Stateや@Observableの値が利用・更新されるとupdateが呼ばれます。それらの値が定義されていない場合は、呼ばれません。
また、makeクロージャーの処理が完了したタイミングで一度だけupdateが呼ばれることを確認しています。makeクロージャーで重い処理がありawait等があった場合は、その処理の後に呼ばれます。
updateが呼ばれない場合について
以下、ほとんどがUnity脳の自分が呼ばれるかもなーと思ったら呼ばれなかったという話なので、上の説明で分かる方は、ここでスキップでも良いです。
makeクロージャーで、@Stateや@Observable等の付いた値が利用され、更新された時
RealityViewがView内で定義されているのでmakeで監視された値を利用していた場合もupdateが更新されるのかなーと思っていたところ更新されなかったという話です。updateの内部で@Stateや@Observableが利用された際のみでupdateの更新を行うようです。
makeクロージャーで、生成したEntityのポジションが変更された時
これが始まりなのですが、Entityのポジションなどの情報が更新されたとしてもupdateが呼ばれるわけではありません。なので、下のような形でRealityViewでEntityを生成し、そのEntityをドラッグする操作をしたとしてもupdateは呼ばれないので注意してください。
一度、@observableなクラスや@StateなプロパティにEntityの操作情報を書き込み、updateクロージャの中で書き込んだ監視可能な値を利用したらupdateが呼ばれるようになります。
var body: some View {
RealityView { content, attachments in
content.add(self.root)
// エンティティの生成
if let immersiveContentEntity = try? await Entity(named: "Scene", in: realityKitContentBundle) {
root.addChild(immersiveContentEntity)
}
}
update: { content, attachments in
//こので監視された値を使わなければ呼ばれない
print("update")
} .gesture(dragGesture)
}
var dragGesture: some Gesture {
DragGesture()
.targetedToAnyEntity()
.onChanged { value in
let entityPos = value.convert(value.location3D, from: .local, to: value.entity.parent!)
// ドラッグしているEntityのポジションを変更する
value.entity.position = entityPos
// AppModelでDragPosを保存
appModel.dragPos = entityPos
}
}
}
makeクロージャーで、contentにAddしたEntityに他のEntityをAddChildした時
シーンにEntityを追加した場合もupdateは呼ばれません。具体的には、下のようにRealityViewのcontentにrootを先に追加し、その後で時間差でrootにEntityを追加した場合にはupdateは呼ばれませんでした。追加するタイミングはmakeクロジャーで可能ではあるので利用するシチュエーションがないかもしれませんが。
struct ImmersiveView: View {
var root: Entity = Entity()
var body: some View {
RealityView { content, attachments in
content.add(self.root)
// エンティティの生成
Task{
// 5秒間待機する。
try? await Task.sleep(nanoseconds: 5 * 1_000_000_000)
// Entityを生成して、rootに追加
if let immersiveContentEntity = try? await Entity(named: "Scene", in: realityKitContentBundle) {
root.addChild(immersiveContentEntity)
}
}
}
update: { content, attachments in
print("update")
}
}
}
attachmentsクロージャで定義されているAttachmentのView内で@Stateや@Observable等の付いた値が利用され、更新された時
attachmentsクロージャー内ではAttachmetという形でViewを登録します。このViewの中で@Stateや@Observableの値を利用した場合もupdateクロージャーは呼ばれませんでした。しかし、Attachmentで定義されたView上では値が更新されます。
終わりに
はい。結論以下は、あまり意味のない試行錯誤になってしまいましたが、これでUnityのupdateと全然違う事がわかったかと思います。(自戒)
冗談はさておき、大事なところはSwiftUIでいうところのView内で監視されている値が更新された際にViewが更新される機能がupdateクロージャーで利用できるということです。同じViewModelを通常のViewとRealityViewで共有したい場合等にupdateクロージャーが活躍するのではないかと思います。
Discussion