WebSocket とかのデバッグ表示に Ornaments をつかう
WebSocket サーバをローカルで立てて遊んでいると、いま繋がっているのかどうかとか、どんなデータが渡されているかとかを逐一チェックしたくなる。最初は Xcode のログにそれを出していたものの、もっとグラフィカルに出したり、Mac の画面をいちいち見るのが面倒になってきた。
さて、このエントリは visionOS Advent Calendar 2024 の23日目。内容は、WebSocket とかのデバッグ表示に Ornaments をつかうというものだが、以下のエントリの続編的な意味合いにもなる。
ということで、既に WebSocket サーバーと Vision Pro からの接続環境はできているという前提で話が進んでいく。事前部分についての詳細は発表資料および記事を見ていただければという感じだが、簡単に書くと以下。
- Swift 製の Web アプリケーションフレームワーク Vapor は WebSocket を扱うことができるので、これを使って WebSocket サーバを建てた
- visionOS 側からは daltoniam/Starscream を使って WebSocket に接続
まさに今からローカル環境において Vision Pro から WebSocket で Vapor と接続していろいろやっていこうという段階である。
WebSocket を扱う場合は Web ブラウザであれば Chrome の開発ツールなどで接続のモニタリングができるが、Vision Pro だとそうもいかない。手っ取り早いのは Vision Pro で Mac 仮想ディスプレイを使い Xcode のログを表示しつつ確認という感じだろうか。
実は最初はそれをやっていたのだが、Mac 仮想ディスプレイでの開発は長時間やろうとすると結構疲れる。ということで、今回のエントリは、そういう接続情報をデバッグ的に見るに何か楽な方法はないものだろうかというのが主題。
Vision Pro 向けアプリを開発する際にどういう開発情報の表示の仕方があるか
まず最初に思いつくのは、別ウインドウで表示というスタイルだ。visionOS 向けアプリには複数の Window を持つアプリを作ることが可能だが、その別ウインドウ部分に開発時情報を出しておくという使い方である。
visionOS 2 以降では defaultWindowPlacement
というインスタンスメソッドを使って Window の位置を指定することができる。なので、この方式で専用の接続情報ウインドウを設けるというのが一つの方法。
あとは、アプリのレイアウト自体に影響を与えてもいいということであればデバッグ用の情報エリアを Window 内にパネル的に置いてもいいかもしれない。
もうちょっと手軽にちょっとした情報エリアが作れないものかということで今回の Ornaments を利用してみようというところに辿りついた。
Ornaments というのは Window の周りについている小さなツールバーのようなもので、Window より少し手前に表示され、微妙にウインドウからはみ出している。もっとも表示位置はコントロールできるので Window の外側に表示させることも可能だ。
公式のドキュメントにはこのような記述がある。
オーナメントは、関連するウインドウよりも少し手前にある、ウインドウに平行な平面に浮かびます。関連するウインドウが移動すると、オーナメントもウインドウとの位置関係を保ちながら移動します。ウインドウのコンテンツがスクロールしても、オーナメント内のコントロールや情報は影響を受けません。
Ornaments は少し語弊がある言い方をすると Window にくっついた小さな Window という感じのものだ。Ornaments にはさまざまな要素を含めることができ、VStack や HStack なんかも使える。
今回は、この Ornaments を使ってとりあえず、WebSocket に接続中か切断中かを表示するだけの小さな Ornaments を作成してみた。
接続時、
切断時、
とまあこんな感じ。比較的楽に作ることが出来る。 「Display Log」と書いているのはゆくゆくは JSON を表示したりなどをしようと考えているエリア。
コードは以下。
import RealityKit
import RealityKitContent
import SwiftUI
struct ContentView: View {
@ObservedObject var client = WebSocketController()
init() {
client = WebSocketController()
}
var body: some View {
VStack {
if client.isConnected {
Text("Connected.")
.padding(.top, 40)
.font(.largeTitle)
List {
ForEach(client.messages, id: \.self) { m in
Text(m)
}
}
}
}
.onAppear {
client.connect()
}
.onDisappear {
client.disconnect()
}
.ornament(
attachmentAnchor: .scene(.top),
contentAlignment: .bottom
) {
VStack {
if client.isConnected {
Label {
Text("Connected.")
.multilineTextAlignment(.center)
.font(.title)
} icon: {
Image(systemName: "bolt.horizontal.circle.fill")
.foregroundColor(.green)
.font(.title)
}
} else {
Label {
Text("Disconnected.")
.multilineTextAlignment(.center)
.font(.title)
} icon: {
Image(systemName: "bolt.horizontal.circle")
.foregroundColor(.red)
.font(.title)
}
}
Text("Display Log")
}
.padding()
.glassBackgroundEffect()
}
}
}
#Preview(windowStyle: .automatic) {
ContentView()
}
.ornament(attachmentAnchor: .scene(.top), contentAlignment: .bottom) { ... }
の部分が Ornaments を指定している箇所となる。
attachmentAnchor
は配置位置を指定することが出来、
Static メソッドとして scene(_:)
が用意されている。
static func scene(_ anchor: UnitPoint) -> OrnamentAttachmentAnchor
位置も数値で指定しなくてもある程度のコントロールが出来るようにアンカーが用意されているので、好きな位置を指定するといいだろう。
アンカーの種類については以下のドキュメントが詳しい。
ちなみに非表示にしたい場合は visibility
に .hidden
の指定をすることで表示させなくできる
.ornament(
visibility: .hidden,
attachmentAnchor: .scene(.top),
contentAlignment: .bottom
) {
...
}
こういう使い方をしていいもの?
とりあえず使いやすいから使ってしまったが、はたしてこういう使い方をしていいものなのだろうか? Ornaments のドキュメントを見てみる。日本語のドキュメントもあるのでそこから引用してみよう。
オーナメントはウインドウの上下左右どのエッジにも配置でき、ボタン、セグメントコントロール、その他のビューなどのUIコンポーネントを含めることができます。システムは、ツールバー、タブバー、ビデオ再生コントロールなどのコンポーネントの作成および管理にオーナメントを使用します。デベロッパは、オーナメントを使ってカスタムコンポーネントを作成できます。
よく使うコントロールや情報を、ウインドウの邪魔にならない一貫した位置に提示するときにはオーナメントの使用を検討する。オーナメントは常に関連ウインドウの近くにあるので、ユーザが見失うことはありません。
接続情報などは、よく使う情報に分類できるからこのような使い方でもいいようだ。ウインドウにくっついていて見失いにくいというのも良い。別ウインドウにすると背景側に重なったりすると見えなくなってしまったりすることもある。