SwiftUIで ひとつのViewから複数のシートを表示する

ひとつのViewから複数の画面をモーダルへ遷移したい。ボタンを複数用意して、それぞれ対応する画面をモーダル表示させようとしたがうまくいかない。

.sheet() modifierを設定すれば、モーダル表示できることは知っているが、それぞれ異なる複数の画面への遷移の場合はうまくいかない。

実行環境

  • Xcode 11.6
  • iOS 13.6

問題の挙動

ひとつの画面上に「画像を選択するボタン」と「画像を切り出す(クロッピングする)ボタン」を用意した。

f:id:ch3cooh393:20200725115320p:plain

それぞれのボタンを押した時に、対応する画面をモーダル表示させたい。

import SwiftUI

struct SampleView : View {
    
    @State private var sheetIsPresented1 = false
    @State private var sheetIsPresented2 = false
    
    var body: some View {
        VStack {
            Button(action: {
                self.sheetIsPresented1 = true
            }, label: { Text("modal 1 (show picker)") })
            
            Button(action: {
                self.sheetIsPresented2 = true
            }, label: { Text("modal 2 (show cropper)") })
            
        }
        .sheet(isPresented: $sheetIsPresented1) {
            ImagePickerView(sourceType: .photoLibrary, onCanceled: {
                // on cancel
            }) { (image) in
                // on success
            }
        }
        .sheet(isPresented: $sheetIsPresented2) {
            ImageCropView(originalImage: UIImage(), onCanceled: {
                // on cancel
            }) { (image) in
                // on success
            }
        }
    }
}

modal 2ボタンはうまく反応するが、modal 1ボタンを押しても反応しなくなってしまった。どうすれば解決できるのか?

解決編

解決方法はふたつある。

ボタンにそれぞれ .sheet() メソッドを紐づける

ボタンに対して、処理が明確になるので個人的には好きだが、bodyプロパティがごちゃごちゃしてしまいがち。

import SwiftUI

struct SampleView : View {
    
    @State private var sheetIsPresented1 = false
    @State private var sheetIsPresented2 = false
    
    var body: some View {
        VStack {
            Button(action: {
                self.sheetIsPresented1 = true
            }, label: { Text("modal 1 (show picker)") })
            .sheet(isPresented: $sheetIsPresented1) {
                ImagePickerView(sourceType: .photoLibrary, onCanceled: {
                    // on cancel
                }) { (image) in
                    // on success
                }
            }
            
            Button(action: {
                self.sheetIsPresented2 = true
            }, label: { Text("modal 2 (show cropper)") })
            .sheet(isPresented: $sheetIsPresented2) {
                ImageCropView(originalImage: UIImage(), onCanceled: {
                    // on cancel
                }) { (image) in
                    // on success
                }
            }
        }
    }
}

.sheet()を 一箇所で定義する場合

どんなシートかを明示しておく必要があるが、処理する場所をまとめておくことができる。たとえば、モーダルA→モーダルBなど連続して画面を変えたい場合など、ボタンに紐付かない処理もこの方法でならスムーズに実装できる。

struct SampleView : View {
    
    enum SheetType {
        case imagePick
        case imageCrop
    }
    
    @State private var currentSheet: SheetType = .imagePick
    @State private var sheetIsPresented = false
    
    var body: some View {
        VStack {
            Button(action: {
                self.currentSheet = .imagePick
                self.sheetIsPresented = true
            }, label: { Text("modal 1 (show picker)") })
            
            Button(action: {
                self.currentSheet = .imageCrop
                self.sheetIsPresented = true
            }, label: { Text("modal 2 (show cropper)") })
        }
        .sheet(isPresented: $sheetIsPresented) {
            if (self.currentSheet == .imagePick) {
                ImagePickerView(sourceType: .photoLibrary, onCanceled: {
                    // on cancel
                }) { (image) in
                    // on success
                }
            } else if (self.currentSheet == .imageCrop) {
                ImageCropView(originalImage: UIImage(), onCanceled: {
                    // on cancel
                }) { (image) in
                    // on success
                }
            }
        }
    }
}

以上で、ひとつのViewから複数のシートを表示することができるようになった。

f:id:ch3cooh393:20200725120203p:plain