LoginSignup
31
22

More than 3 years have passed since last update.

[SwiftUI]型消去を利用した任意のViewをPresent(sheet)する手法

Last updated at Posted at 2019-09-28

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を返す方法です。

この挙動に関する解説は

@1amageek SwiftUIのsheetを出し分ける

をお読みください。

上記記事では、IdentifiableViewの性質をenumに持たせ
.sheet(item: self.$presentation) { $0 }
のように遷移させています。こうすることで、親Viewは複数の要素を管理する必要もなくなり、分岐のロジックをもつ必要もなくなります。

しかし、この手法はViewが遷移先がどこで管理されているかを知る必要があり、また遷移先はpresentationで宣言された型に限定されます。

IdentifiableViewの性質を持たせつつ、より柔軟に遷移先を制御する方法を考えてみました。

実装は、次のように型消去を用いて具体的な型を作ります。

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は、IdentifiableViewの性質を持つ構造体です。
実態はinit?<V>(view: V?) where V: View & Identifiableで受け取ったViewになります。
IdentifiableViewはそれぞれAnyHashableAnyViewに分解されて保持されました。
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)
31
22
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
22