😸

SwiftUIによるsheet + fullScreen (multiple sheet)

2021/12/25に公開

経緯

iOS14.4以下の場合multiple sheetに対応していない事象が発生しているが
解消方として、指定Itemにより表示するViewModifierを複数指定したり
PJにより対応方法は様々存在している。
が、Sheet + fullScreenCoverを組み合わせて使用している記事があまりなかった。

実施

カスタムModifier作成

extension View {
  func sheets<Content: View>(
    isPresented: Binding<Bool>,
    isFullScreenPresented: Binding<Bool>,
    onDismiss: @escaping () -> Void,
    @ViewBuilder content: @escaping () -> Content
    ) -> some View {
          self.modifier(
              ViewModifier(
                  isPresented: isPresented,
                  isFullScreenPresented: isFullScreenPresented,
                  onDismiss: onDismiss,
                  builder: content
              )
          )
      }
  }
  
  struct ViewModifier<V: View>: ViewModifier {
    let isPresented: Binding<Bool>
    let isFullScreenPresented: Binding<Bool>
    let onDismiss: () -> Void
    let builder: () -> V

    @ViewBuilder
    func body(content: Content) -> some View {
        if #available(iOS 15.0, *) {
            content
                .sheet(isPresented: isPresented, onDismiss: onDismiss, content: builder)
                .fullScreenCover(isPresented: isFullScreenPresented, content: builder)
        } else {
            Group {
                ZStack {
                    content
                        .sheet(isPresented: isPresented, onDismiss: onDismiss, content: builder)
                    EmptyView()
                        .fullScreenCover(isPresented: isFullScreenPresented, content: builder)
                }
            }
        }
    }
}

使用

struct SampleView: View {
    enum Present {
        case sheet
        case fullScren
    }
    @State var isPresented = false
    @State var isFullScreenPresented = false
    @State var present: Present = .sheet
    
    var body: some View {
        VStack {
            Button("showSheet") {
                present = .sheet
                isPresented.toggle()
            }
            Button("showFullScreen") {
                present = .fullScren
                isFullScreenPresented.toggle()
            }
        }
        .sheets(
            isPresented: $isPresented,
            isFullScreenPresented: $isFullScreenPresented,
            onDismiss: {
                /// ...
            }
        ) {
            switch present {
            case .sheet:
                /// ...
            case .fullScren:
                /// ...
            }
        }
    }
}

詳細

.sheet + .fullScreenCoverをViewBuilderでラップしたカスタムModifierの作成

self.modifier(
  ViewModifier(
    isPresented: isPresented,
    isFullScreenPresented: isFullScreenPresented,
    onDismiss: onDismiss,
    builder: content
  )
)
@ViewBuilder
    func body(content: Content) -> some View {
        if #available(iOS 15.0, *) {
            content
                .sheet(isPresented: isPresented, onDismiss: onDismiss, content: builder)
                .fullScreenCover(isPresented: isFullScreenPresented, content: builder)
        } else {
            Group {
                ZStack {
                    content
                        .sheet(isPresented: isPresented, onDismiss: onDismiss, content: builder)
                    EmptyView()
                        .fullScreenCover(isPresented: isFullScreenPresented, content: builder)
                }
            }
        }
    }

使用する際の注意点として
シート内容を返却するクロージャはisPresentedisFullScreenPresentedのどちらのBinding時にも返却されるため
意図的にViewClassにてStatesを使用し判定する必要がある(もう少しいい方法がありそうだが、肥大化する部分でもないので一時的にこちらを採用)

又、contentに対してsheetfullScreenCoverを設定してしまうと、iOS14.4未満での仕様上、後述したViewBuilderが後ガチしてしまうため、EmptyViewに対して設定を行う
※iOS15以降の場合はEmptyViewに対してViewBuilderを指定するとイベントが発火されない事象が発生するためiOS15.0以前のバージョンと切り分ける必要がある

enum Present {
  case sheet
  case fullScren
}

参考

https://developer.apple.com/documentation/swiftui/viewmodifier
https://developer.apple.com/documentation/swiftui/view/sheet(ispresented:ondismiss:content:)
https://zenn.dev/yorifuji/articles/swiftui-multiple-sheet

Discussion