iOS 14 (SwiftUI) Sheet Modals not dismissing and acting randomly weird

I have been googling this topic for a while and it seems everybody is experiencing some kind of issues with dismissing modals after the new iOS 14 release, but I have not found anybody also struggling with my specific case.

After upgrading to iOS14 (14.0.1 included), all my sheet modals in my app started to act randomly weird:
  • Sometimes the modals act perfectly as usual

  • Sometimes (most of the time) I have to kill the app because the modal is not dismissed and I cannot go back to the main view

  • Sometimes a modal is automatically (non-desired) closed right after it opens, and after that, the modal works fine (and so it is not closed until I say so)

Does anybody know what is going on with iOS14 and sheet modals? Should we close modals in a different way than in iOS 13?

I tried closing the modals in two different ways: either by using the Binding<Bool> method, or by using the Environment presentationMode method, and while both methods were all working fine in iOS13, now in iOS14 they are all randomly (more than 50% of the time that I open the app) failing.

Accepted Reply

I finally came to a solution. I managed to detect where the issue came from, or at least, what was causing the weird modal behaviour. To be honest, I solved the issue but I am still not understanding why this is causing such an issue. I still don't see the relationship between, and moreover, this was something not happening in iOS13, so...

In any case, let me give you two examples of the very same app, one reproducing the issue and the other one without the issue.

App 1: No issue in iOS13 but weird modal behaviour in iOS14



First let me write the MainView and the ModalView:

Code Block
// a simple tabview with just one tab with a simple button opening the modal
struct MainView: View {
    @State var showNewSuitModal = false
    var body: some View {
        TabView {
            Button(action: {
                self.showNewSuitModal = true
            }) {
                ZStack {
                    Circle()
                        .foregroundColor(Color.white)
                        .frame(width: 40, height: 40)
                        .shadow(radius: 5)
                        .opacity(0.5)
                    Image(systemName: "plus")
                        .resizable()
                        .frame(width: 20, height: 20)
                        .foregroundColor(Color.black)
                    }
            }
            .foregroundColor(Color.white)
            .padding(.bottom,20)
            .padding(.trailing,10)
            .sheet(isPresented: $showNewSuitModal) { // <-- the modal acting weird
                ModalView(isPresented: self.$showNewSuitModal)
            }
            .tabItem({
  Image(systemName: "gauge")
                Text("Tab").fontWeight(.ultraLight)
            })
        }
        .navigationBarBackButtonHidden(true)
        .navigationBarTitle("")
    }
}
// A simple modal with just a closing/dismissing button
struct ModalView: View {
    @Binding var isPresented: Bool
    var body: some View {
        Button(action: {
            self.isPresented = false
        }) {
            ZStack {
                Circle()
                    .foregroundColor(Color.white)
                    .frame(width: 40, height: 40)
                    .shadow(radius: 5)
                    .opacity(0.5)
                Image(systemName: "minus")
                    .resizable()
                    .frame(width: 20, height: 20)
                    .foregroundColor(Color.black)
                }
        }
        .foregroundColor(Color.white)
        .padding(.bottom,20)
        .padding(.trailing,10)
    }
}


And now the LaunchView, where the actual issue was detected:

Code Block  
// The launching view including just a "next" button which is linking us to
// the MainView (the purpose of this first view is to show a disclaimer app)
struct LaunchView: View {
     var body: some View {
         NavigationView {
             NavigationLink(destination: MainView()) {
                 HStack (alignment: .center) {
                     HStack {
                         Image(systemName: "play")
                             .font(.system(size: 16))
                         Text("Next")
                             .fontWeight(.semibold)
                             .font(.system(size: 16))
                     }
                     .padding()
                     .foregroundColor(.white)
                     .background(Color.red)
                     .cornerRadius(40)
                     .shadow(radius: 5)
                }
                .padding()
             }
             .navigationBarTitle("Test App")
         }
         .navigationViewStyle(StackNavigationViewStyle())
     }
 }


This app is the one having issues, and all the issues come from the fact that the LaunchView is using a NavigationLink to jump to the next view.

Let's rewrite the app in another way so the NavigationLink is not used.

App 2: Issue solved (as far it seems for now)



MainView and ModalView are exactly the same than in App 1.
LaunchView is as follows:

Code Block
struct LaunchView: View {
    @State var disclaimerView = true
    var body: some View {
        NavigationView {
            VStack {
                if self.disclaimerView {
                    DisclaimerView(buttonClick: $disclaimerView)
                } else {
                    MainView()
                }
            }
            .navigationBarTitle("Test App")
        }
        .navigationViewStyle(StackNavigationViewStyle())
    }
}
struct DisclaimerView: View {
    @Binding var buttonClick: Bool
    var body: some View {
        VStack {
            Button(action: {
                self.buttonClick.toggle()
            }) {
                HStack {
                    Image(systemName: "play")
                        .font(.system(size: 16))
                    Text("Next")
                        .fontWeight(.semibold)
                        .font(.system(size: 16))
                }
            }
            .padding()
            .foregroundColor(.white)
            .background(Color.red)
            .cornerRadius(40)
            .shadow(radius: 5)
        }
    }
}


With this approach, which is basically not using the NavigationLink in my root View, the modals started to act as they should.


I can't see why but I hope it helps others.

Replies

I'm having the same issue. I noticed late in the day yesterday that my simulator was running iOS 14.1 and my device was still on 14.0. I was having pretty good luck in the simulator getting my modals to close, so I updated the phone and it didn't help. Now this morning, the simulator won't close them either. Like you, I've tried both methods as well. It seems to only get stuck once a control on the view becomes first responder and the keyboard is activated. As long as I don't click into a TextView, the view closes just fine all day long. Help!!
I seem to be having some better luck now. While I can't for the life of me explain why it works one way and not the other, here's what I had to do to get a more consistent closing action for my modal view. And doing it this way, the environment presentationMode method works just fine.

Code Block
@State private var openComposer = false
var body: some View {
NavigationView {
ZStack {
Color("LightGray").edgesIgnoringSafeArea([.all])
ScrollView {
VStack {
<view stuff here>
}
}
.toolbar {
       ToolbarItem(placement: .navigationBarTrailing) {
         Button(action: {
           self.openComposer.toggle()
         }) {
           Image(systemName: "ellipsis.bubble.fill")
}
/*
NO, NO, NO - Causes problems placed here
.fullScreenCover(isPresented: $openComposer) {
ActivityCreatorView()
}
*/
       }
     }
   }
}
.fullScreenCover(isPresented: $openComposer) {
ActivityCreatorView()
}
}


So I guess the trick here is to tie the full screen cover views to the outermost container of the view and not to the buttons that trigger them. Here's wishing you similar luck!
  • So I guess the trick here is to tie the full screen cover views to the outermost container of the view and not to the buttons that trigger them. Here's wishing you similar luck!

    That saved me! Thank you so much!

  • Yes that worked!

Add a Comment
Maybe this problem is connected to the problem I run into with iOS 14, where presenting a sheet sometimes (and if so, only the first time) reloads the (underlying) presenting view - which would
  1. ) dismiss the presented sheet (-> onDisappear is called), which would then

  2. ) load the view again (-> onAppear is called), which would then

  3. ) represent the sheet again (since state in the observed view model hasn't changed of course),

... which can trigger a chain of unexpected behavior.

Note: This had been working solid with iOS 13, and I'm still debugging. It's pretty obvious what's happening, but I have no idea yet why it it happening, why it is happening randomly and how to solve this.
@webbguyatwork

I've never used fullScreenCover yet, so it is just some guessing.

Line 21 (which you commented out) : to which object would fullScreenCover modifier apply ?

On line 29, it applies to NavigationView; it seems logical it works there.
After further research into my issue, I determined it had more to do with the way I was implementing tab item badges. I found some examples on YouTube to implement them over the top of the tab bar, but they seem to be overtaking the entire screen, blocking out my close buttons on the full screen modal. I've since disabled the tab item badges, and that's ultimately what fixed my problem.

@Claude31, I would agree that it makes more sense to have the fullScreenCover attached to the outermost view, in this case my NavigationView. I think that's how I'll do it from now on.
@webbguyatwork thanks a lot for your feedback.

In my case it does not seem related to the place where I "call" the sheet. See below:

Code Block
var body: some View {
ZStack (alignment: .bottomTrailing) {
  ...
}
.sheet(isPresented: $showNewSuitModal) { // <-- this is the randomly weird modal
  NewSuitView(appData: self.appData, suitsView: self)
}
.alert(isPresented: $showSuitUsedModal) {
  Alert(...)
}


I am calling the modal from the root ZStack itself (which is the stack containing the button from which I call to open the modal).

On the other hand, I am not using Tab Item badges.

Still struggling with this :(
Quick update: I managed to reproduce the issue with a very very simple app.
This should allow me to start cutting pieces of code (old-school debugging) and see what's happening.
I finally came to a solution. I managed to detect where the issue came from, or at least, what was causing the weird modal behaviour. To be honest, I solved the issue but I am still not understanding why this is causing such an issue. I still don't see the relationship between, and moreover, this was something not happening in iOS13, so...

In any case, let me give you two examples of the very same app, one reproducing the issue and the other one without the issue.

App 1: No issue in iOS13 but weird modal behaviour in iOS14



First let me write the MainView and the ModalView:

Code Block
// a simple tabview with just one tab with a simple button opening the modal
struct MainView: View {
    @State var showNewSuitModal = false
    var body: some View {
        TabView {
            Button(action: {
                self.showNewSuitModal = true
            }) {
                ZStack {
                    Circle()
                        .foregroundColor(Color.white)
                        .frame(width: 40, height: 40)
                        .shadow(radius: 5)
                        .opacity(0.5)
                    Image(systemName: "plus")
                        .resizable()
                        .frame(width: 20, height: 20)
                        .foregroundColor(Color.black)
                    }
            }
            .foregroundColor(Color.white)
            .padding(.bottom,20)
            .padding(.trailing,10)
            .sheet(isPresented: $showNewSuitModal) { // <-- the modal acting weird
                ModalView(isPresented: self.$showNewSuitModal)
            }
            .tabItem({
  Image(systemName: "gauge")
                Text("Tab").fontWeight(.ultraLight)
            })
        }
        .navigationBarBackButtonHidden(true)
        .navigationBarTitle("")
    }
}
// A simple modal with just a closing/dismissing button
struct ModalView: View {
    @Binding var isPresented: Bool
    var body: some View {
        Button(action: {
            self.isPresented = false
        }) {
            ZStack {
                Circle()
                    .foregroundColor(Color.white)
                    .frame(width: 40, height: 40)
                    .shadow(radius: 5)
                    .opacity(0.5)
                Image(systemName: "minus")
                    .resizable()
                    .frame(width: 20, height: 20)
                    .foregroundColor(Color.black)
                }
        }
        .foregroundColor(Color.white)
        .padding(.bottom,20)
        .padding(.trailing,10)
    }
}


And now the LaunchView, where the actual issue was detected:

Code Block  
// The launching view including just a "next" button which is linking us to
// the MainView (the purpose of this first view is to show a disclaimer app)
struct LaunchView: View {
     var body: some View {
         NavigationView {
             NavigationLink(destination: MainView()) {
                 HStack (alignment: .center) {
                     HStack {
                         Image(systemName: "play")
                             .font(.system(size: 16))
                         Text("Next")
                             .fontWeight(.semibold)
                             .font(.system(size: 16))
                     }
                     .padding()
                     .foregroundColor(.white)
                     .background(Color.red)
                     .cornerRadius(40)
                     .shadow(radius: 5)
                }
                .padding()
             }
             .navigationBarTitle("Test App")
         }
         .navigationViewStyle(StackNavigationViewStyle())
     }
 }


This app is the one having issues, and all the issues come from the fact that the LaunchView is using a NavigationLink to jump to the next view.

Let's rewrite the app in another way so the NavigationLink is not used.

App 2: Issue solved (as far it seems for now)



MainView and ModalView are exactly the same than in App 1.
LaunchView is as follows:

Code Block
struct LaunchView: View {
    @State var disclaimerView = true
    var body: some View {
        NavigationView {
            VStack {
                if self.disclaimerView {
                    DisclaimerView(buttonClick: $disclaimerView)
                } else {
                    MainView()
                }
            }
            .navigationBarTitle("Test App")
        }
        .navigationViewStyle(StackNavigationViewStyle())
    }
}
struct DisclaimerView: View {
    @Binding var buttonClick: Bool
    var body: some View {
        VStack {
            Button(action: {
                self.buttonClick.toggle()
            }) {
                HStack {
                    Image(systemName: "play")
                        .font(.system(size: 16))
                    Text("Next")
                        .fontWeight(.semibold)
                        .font(.system(size: 16))
                }
            }
            .padding()
            .foregroundColor(.white)
            .background(Color.red)
            .cornerRadius(40)
            .shadow(radius: 5)
        }
    }
}


With this approach, which is basically not using the NavigationLink in my root View, the modals started to act as they should.


I can't see why but I hope it helps others.
Thank you webbguyatwork.

I had enormous problems of sheets refusing to go away and navigation buttons stopping working after the first go - until I saw your comment.

I moved my call .sheet() call away from the button and to my top level View inside NavigationView and the problem has gone. Thanks!
I confirm as well that webbguyatwork's tip solved my modal dismiss issue. Using fullScreenCover on the outer most view solved the 'no dismiss' issue I was having. Thank you!
As someone else pointed out, moving the .sheet modifier (or .fullScreenCover, whichever applies to you) to the outermost view in the container seems to be the fix. The accepted answer with NavigationView may work as well as it just applies to the certain use case.
If you see this in the future, I recommend trying both and seeing what works best.

I have been having an issue on macOS with sheet views taking a long time to close. I am new to swiftUI and have been searching for days trying to find a solution for my issue and moving the .sheet modifier to the outermost view bracket instantly solved my issue. Thanks to everyone that contributed to getting this figured out

Will this be fixed in iOS16?