quanshousio/ToastUI

Attempt to present toast whose view is not in the window hierarchy

OccultSlolem opened this issue · 7 comments

Pre-requisites:

  • [*] Yes, I looked through both open and closed issues looking for what I needed.
  • [*] No, I did not find what I was looking for.

When attempting to trigger a toast to appear programmatically, I get the following error:

[Presentation] Attempt to present <_TtGC7ToastUI26ToastViewHostingControllerGV7SwiftUI15ModifiedContentGVS_9ToastViewVS1_9EmptyViewVS1_4TextS4__GVS1_30_EnvironmentKeyWritingModifierVS_17AnyToastViewStyle___: 0x10193f040> on <_TtGC7SwiftUI29PresentationHostingControllerVS_7AnyView_: 0x104f11380> (from <_TtGC7SwiftUI29PresentationHostingControllerVS_7AnyView_: 0x104f11380>) whose view is not in the window hierarchy.

The relevant section of my code looks like this:

@State private var presentingToast = false
@State private var uploadProgress = 0.0
var body: some View {
    VStack {
        Button(action: {
            print("Show toast")
        }, label: {
            Text("")
        })
        .toast(isPresented: $presentingToast) {
            ToastView("Uploading...")
                .toastViewStyle(DefiniteProgressToastViewStyle(value: $uploadProgress))
        }
        .hidden()
   

When I set presentingToast to true, it gives me the aforementioned error. I've also tried removing the hidden modifier and putting in some words in the Text, but that doesn't make a difference.

Your Environment

  • Swift Version: 5
  • Xcode Version: 12.3
  • Operating System Version: macOS Big Sur 11.1
  • Device or Simulator: iPhone 8 Plus

It seems to me that you're creating a dummy hidden button just to attach the .toast modifier, and then trigger the toast somewhere else, right? Would you mind providing more information about what you're trying to accomplish here? How did you trigger the toast programmatically?

Yep, that's correct. The gist of the situation is that this toast is for a View where a user can upload an image to Firebase Storage, however before the toast is presented it verifies that the photo is correctly formatted. If it isn't, then it shows an alert (which I've also set to be programmatically triggered in almost the exact same way, tying it to a dummy hidden button. It goes something like this:

@State private var presentingToast = false
@State private var uploadProgress = 0.0
var body: some View {
    VStack {
        Button(action: {
            print("Show toast")
        }, label: {
            Text("")
        })
        .toast(isPresented: $presentingToast) {
            ToastView("Uploading...")
                .toastViewStyle(DefiniteProgressToastViewStyle(value: $uploadProgress))
        }
        .hidden()
...
}

//Function gets called after an ImagePicker is dismissed
func loadImage() {
    guard let inputImage = inputImage else { return } //This is pulled from an ImagePicker sheet set up elsewhere
    local data = inputImage.pngData()!
    //Do other checks on the image...
    ...
    //If all checks are OK
    presentingToast = true //In theory, the toast should present itself here, but the aforementioned error happens here (when I step to this line in the debugger, this is where the error pops itself up)
    ...
}

How did you implement the callback after the user has picked an image? Are you calling your loadImage() after the picker has been successfully dismissed? I'm guessing the error might due to the picker hasn't been dismissed yet, so we are prematurely presenting the toast.

It's tied to the onDismiss parameter of the sheet modifier

@State private var showingImagePicker = false
@State private var inputImage: UIImage?
...

var body: some View {
    VStack(alignment: .leading) {
        ...
        Button(action: {
            showingImagePicker = true
        }, label: {
            Text("Upload photo")
            Image(systemName: "chevron.right")
        })
        ...
    }
    .sheet(isPresented: $showingImagePicker, onDismiss: loadImage) {
        ImagePicker(image: self.$inputImage)
    }
}

func loadImage() {
    ...
}

Try calling your function using onDisappear modifier on ImagePicker view instead.

.sheet(isPresented: $showingImagePicker) {
  ImagePicker(image: $inputImage)
    .onDisappear {
      loadImage()
    }
}

Would you mind showing me how do you implement the ImagePicker? Are you calling presentationMode.wrappedValue.dismiss() in imagePickerController(_:didFinishPickingMediaWithInfo:) delegate method of UIImagePickerController to dismiss the picker after the user has picked an image?

Ah, yep, that did the trick. Thanks so much!

As for the ImagePicker, it's pretty much exactly as you said. I have a UIViewControllerRepresentable struct that has a Coordinator class inside it that instantiates an ImagePicker in its constructor, and then on the imagePickerController(_:didFinishPickingMediaWithInfo:) it calls the dismiss method on that ImagePicker.

By calling presentationMode.wrappedValue.dismiss(), it immediately fires onDismiss callback of the sheet modifier, but at that time UIImagePickerController has not been successfully dismissed, hence the error. Using onDisappear will ensure the view has been successfully dismissed (the same as viewDidDisappear in UIKit). I'm glad it helped.