SwiftUIでは、モーダル表示をするためにsheet
というメソッドを利用します。
struct ContentView: View {
@State var isPresented: Bool = false
var body: some View {
VStack {
Button("present") {
self.isPresented.toggle()
}
}
.sheet(isPresented: $isPresented) {
Text("Presenting")
}
}
}
しかし、このsheet
メソッドは後勝ちで実装を保持するため複数回呼ぶと最後のsheetだけが有効になります。
この挙動を回避するにはsheet(item:content:)
を利用します。
これはBinding<Identifiable>
を受け取り、それをもとにViewを返す方法です。
この挙動に関する解説は
をお読みください。
上記記事では、Identifiable
とView
の性質をenumに持たせ
.sheet(item: self.$presentation) { $0 }
のように遷移させています。こうすることで、親Viewは複数の要素を管理する必要もなくなり、分岐のロジックをもつ必要もなくなります。
しかし、この手法はViewが遷移先がどこで管理されているかを知る必要があり、また遷移先はpresentation
で宣言された型に限定されます。
Identifiable
とView
の性質を持たせつつ、より柔軟に遷移先を制御する方法を考えてみました。
実装は、次のように型消去を用いて具体的な型を作ります。
struct AnyIdentifiableView: View, Identifiable {
typealias ID = AnyHashable
private let _id: ID
var id: ID {
return _id
}
private let _body: AnyView
var body: some View {
_body
}
init?<V>(view: V?) where V: View & Identifiable {
guard let view = view else { return nil }
self._body = AnyView(view)
self._id = view.id
}
}
extension Identifiable where Self: Hashable {
public typealias ID = Self
public var id: Self { self }
}
AnyIdentifiableView
は、Identifiable
とView
の性質を持つ構造体です。
実態はinit?<V>(view: V?) where V: View & Identifiable
で受け取ったViewになります。
Identifiable
とView
はそれぞれAnyHashable
とAnyView
に分解されて保持されました。
Identifiable
はHashableに準拠した型に限り、protocol extensionによってidを自動的に実装します。
これで、View & Identifiable
を自由に入れる事ができる型が出来ました。
そして、AnyIdentifiableView
は具体的な型なので、特に複雑なことをせずに@State
で宣言できます。
struct ContentView: View {
@State var presentation: AnyIdentifiableView? = nil
var body: some View {
VStack {
Button("present") {
self.isPresented.toggle()
}
}
.sheet(item: self.$presentation) { $0 }
}
}
ViewModelが複数ある場合でも、View, Hashable, Identifiable
に準拠した型を渡せば柔軟に遷移する事が出来るようになりました。
viewModel1.$presentation.map({ AnyIdentifiableView(view: $0) }).assign(to: \.presentation, on: self)
viewModel2.$presentation.map({ AnyIdentifiableView(view: $0) }).assign(to: \.presentation, on: self)