SwiftUI的ビューとは?
SwiftUI的ビューとInterface Builder的ビュー
SwiftUI標準のNavigationSplitView
にしても、以下の自作のInspectableView
にしても、ツールバーがあると勝手にサイドバーの表示非表示をトグル的に切り替える操作を行うボタンを付けてしまうという振る舞いをします。
ビュー自身がサイドバーのスプリッタをドラッグして非表示にすることができてしまい、かつ表示を戻す方法がないため、ボタンやメニューなどビューの外から、サイドバーの表示非表示の切り替えられるようにする必要があります。
ビューが「サイドバーの表示非表示のトグルをさせる」という外部インターフェイスを持っていれば済みそうな話ですが、SwiftUI標準のNavigationSplitView
は、サイドバーの表示非表示を切り替える、というようなメソッドは持っていません。
SwiftUIは「ビューの状態を表す変数群、というようなものがあって、それをビューが表現する」というような考え方ですね。
Interface Builder的なビューであれば、IBAction
で定義されたビューの機能を用いて、ビューをコントローラが操作するという感じですが、SwiftUI的ビューだと、サイドバーが表示されているか表示されていないかは、ビューの外側にある変数が管理して、それをビューは反映する、という感じになります(という理解。違ってたらごめんなさい)。
Interface BuilderはMVCという感じで、SwiftUI=MVVM、VMの部分が「ビューの状態を表す変数群」ということなのかなと思います。
で、分かったようなつもりになっていざ実装するとちょっと問題にぶつかりました。
外からは表示非表示しかコントロールできない(させない)ので、ビューの@Binding
変数は、サイドバーがvisibleかどうかだけ、幅はビュー自身が管理できればOK(@State
変数で管理)、という感じで考えていました。
で、サイドバーをドラッグして小さくした結果、(ビュー自身が判断して)非表示にしたとき、visibleかどうかを切り替えたうえ、幅を0にします。で次に外部からvisibleかどうかの変数を書き換えると、再描画、つまりvar body
が呼び出されます。その中で幅0だと困るので最小幅まで戻そうとしますが、そうするとvar body
の中で幅の変数(=@State
)を書き換えてしまうことになるため「挙動が未定義です」と言われます。
とは言え幅の変数を@Binding
にしても、var body
の中で書き換えたら同じことで、外でvisibleと幅の変数の整合性を取るしかない気がします。
ビューの操作として、ドラッグして最小幅までいったときに非表示にするとか幅=0にするとかいうのは、ビュー側の話であって、外の人には関係ない話のはずで、非表示→表示に戻したとき幅を0→最小幅に戻してやる必要があるなんてのを外の人が知る必要はないと思います。
@Binding
の変数が変わる(or 変わった?)タイミングから、var body
が呼ばれるタイミングの間のどこかで、こうした@
のついた変数間のオーケストレーションというかコンダクティングというか、調和処理的なものを行う仕掛けが要るんじゃないかと思っています。
didSet
とかwillSet
とかって、前はあったけど今はないとかいう話を読むこともあります。どうなってるのかなと思います。
この問題に関しては、なんとなく「非表示にする際に幅0にする」のをやめれば解決するような気もしますね。
すべての「複数の内部変数が1つの外部変数によって連動して変わるようなケースで、var body
で連動させようとすると怒られる問題」がそんな感じなのかが気になるところです。
もうひとつの懸念である、NavigationSplitView
を使ってるとツールバーがあったときに、勝手にツールバーにボタンを追加する問題、に関しては、もう少し問題点を整理する必要がありそうです。
@Binding
の変数が変わる(or 変わった?)タイミングから、var body
が呼ばれるタイミングの間のどこかで、こうした@のついた変数間のオーケストレーションというかコンダクティングというか、調和処理的なものを行う仕掛けが要るんじゃないかと思っています。
これに関して、以下の機能の実装で行った「var body
中で、DispatchQueue.main.async
を使って@State
変数を更新する」という手法が使えるような気がします。
別途検証します。