アプリ開発の逆引き辞典

アプリ開発のTipsをまとめました

SwiftUIで ふたつの画面を連続してモーダル表示する

下図のように 画像を選択 → 画像の切り出し(クロッピング) と複数の画面を連続してモーダル表示させつつ遷移させたい。

f:id:ch3cooh393:20200725120719p:plain

モーダルA → モーダルB と連続して遷移できるが、モーダルBがなぜか閉じれなくなってしまう。

実行環境

  • Xcode 11.6
  • iOS 13.6

問題の挙動

モーダルA → モーダルB と連続させたい。しかし実装するとモーダルBが閉じれなくなってしまう不具合が発生する。

import SwiftUI

struct SampleTwoModalView : 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("select images & crop images") })
        }
        .sheet(isPresented: $sheetIsPresented) {
            if (self.currentSheet == .imagePick) {
                ImagePickerView(sourceType: .photoLibrary, onCanceled: {
                    // on cancel
                }) { (image) in
                    //画像を選択するとクロッピング画面へ遷移させる
                    DispatchQueue.main.async {
                        self.currentSheet = .imageCrop
                        self.sheetIsPresented = true
                    }
                }
            } else if (self.currentSheet == .imageCrop) {
                ImageCropView(originalImage: UIImage(), onCanceled: {
                    // on cancel
                }) { (image) in
                    // on success
                }
            }
        }
    }
}

連続してモーダル表示させたい場合にはどうすればよいのか?

解決編

モーダルAを閉じたあと、1秒待ってからモーダルBを表示する。すると、モーダルBが閉じられるようになる。

import SwiftUI

struct SampleTwoModalView : View {
    
    enum SheetType {
        case imagePick
        case imageCrop
    }
    
    @State private var currentSheet: SheetType = .imagePick
    @State private var sheetIsPresented = false
    @State private var image: UIImage? = nil
    
    var body: some View {
        VStack {
            Button(action: {
                self.currentSheet = .imagePick
                self.sheetIsPresented = true
            }, label: { Text("select images & crop images") })
        }
        .sheet(isPresented: $sheetIsPresented) {
            if (self.currentSheet == .imagePick) {
                ImagePickerView(sourceType: .photoLibrary, onCanceled: {
                    // on cancel
                }) { (image) in
                    self.image = image
                    
                    //画像を選択するとクロッピング画面へ遷移させる
                    DispatchQueue.main.asyncAfter(wallDeadline: .now() + .milliseconds(1000)) {
                        self.currentSheet = .imageCrop
                        self.sheetIsPresented = true
                    }
                }
            } else if (self.currentSheet == .imageCrop) {
                ImageCropView(originalImage: self.image!, onCanceled: {
                    // on cancel
                }) { (image) in
                    // on success
                }
            }
        }
    }
}

ただし、正しい正解とは思えないので、他に対応方法があれば教えて欲しい。

(2020/07/29追記) 解決編その2

dismissアニメーション中に次の画面をモーダル表示させることで、SwiftUI内部のステータス管理が不正な状態になってしまうのが原因ではないかと考えています。今後のSwiftUIの修正に期待したい部分です。またSwiftUI側ではdismissが完了したタイミングを取得できないため、dismissが終了してすぐに次の画面へ遷移できません。

私の結論としては、Xcode11.6時点ではUIImagePickerControllerなどのUIKitを使っている以上、SwiftUIで連続してスムーズにモーダル画面を表示するのは難しいので、UIKit側でdismissのタイミングを取る、でした。

本質部分ではないので、ここでは該当部分のみを転記したいと思います。イメージとしては以下の通りです。

public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
    guard let image = info[.originalImage] as? UIImage else {
        picker.dismiss(animated: true) {
            self.onImagePicked(nil)
        }
        return
    }
    
     // UIKit側で閉じるのを待ってからSwiftUI側に画像を渡す
    picker.dismiss(animated: true) {
        self.onImagePicked(image)
    }
}

ソースコードは下記のリポジトリで参照可能です。

参考記事

ひとつのViewから複数の画面をモーダルで表示させる方法は、以下の記事に書いた。