モノトーンの伝説日記

Apex Legends, Splatoon, Programming, and so on...

watchOS 8 や 9 (SwiftUI 3, 4) で、`.sheet( ... )` 下で Close 機能や Navigation を正しく動くように実装する

 全てきれいに解決できる案があったので、シェアします。

watchOS 8.5 (S3 42mm), watchOS 9.4 (SE 40mm), watchOS 10.2 (U2 49mm) のプレビュー

1. 前提条件

 前提条件として、

  • watchOS 9+ では NavigationStack
  • watchOS 8 では NavigationView { ... }.navigationViewStyle(.stack)

を使用します。

また、今後の SwiftUI のバージョンアップによる副作用を避けるため、watchOS 10 ではデフォルトの閉じるボタンを使用します。watchOS 8 では既定の Cancel ボタンを Close ボタンに置き換える処理を行います。

2. 親ビュー

 遷移元のサンプルです。

import SwiftUI

struct ContentView: View {
  var body: some View {
    Text("Root View")
  }
}

struct NavigationHost: View {
  @State
  private var isPresented = false

  var body: some View {
    Group {
      if #available(watchOS 9.0, *) {
        NavigationStack {
          ContentView()
        }
      } else {
        NavigationView {
          ContentView()
        }
        .navigationViewStyle(.stack)
      }
    }
    .sheet(isPresented: $isPresented) {
      SheetViewHost()
    }
  }
}

3. シートビュー

import SwiftUI

extension View {
  func block<Content>(@ViewBuilder _ transform: (Self) -> Content) -> Content {
    transform(self)
  }
}

struct SheetView: View {
  @Environment(\.dismiss)
  private var dismiss

  var body: some View {
    Text("Sheet View")
      .block { content in
        if #available(watchOS 10.0, *) {
          content
        } else {
          content.toolbar {
            // watchOS 8 では既定のキャンセルボタンを置き換え、
            // watchOS 9 では閉じるボタンを追加します。
            ToolbarItem(placement: .cancellationAction) {
              Button("Close", role: .cancel, action: dismiss.callAsFunction)
            }
          }
        }
      }
  }
}

struct SheetViewHost: View {
  var body: some View {
    if #available(watchOS 9.0, *) {
      NavigationStack {
        SheetView()
      }
      .block { content in
        if #available(watchOS 10.0, *) {
          content
        } else {
          content.navigationBarHidden(true) // 肝!!
        }
      }
    } else {
      NavigationView {
        SheetView()
      }
      .navigationViewStyle(.stack)
      .navigationBarHidden(true) // 肝!!
    }
  }
}

最後に

 これだけ覚えて帰ってください。sheet 自体が NavigationStack の中になくても、親ビューで NavigationStack を使っている状態で sheet を用いたい場合、.navigationBarHidden(true) を使って親側のナビゲーション バーを非表示にすること。

 これで全てうまくいきます。