Almost all apps present some sort of modal content, meaning content that is displayed on top of other content. For instance, sheets and popovers are system provided controls whose content is presented modally. It’s also quite common to show content by pushing views in a navigation stack, as well as to pop new windows that users can interact with on macOS.
No matter how the new content is presented -modally, pushed, or somewhat else- there is always one thing that remains constant. Users must be able to dismiss it. Although there are system-provided ways to do so most of the times, often developers include their own controls to make this possible as well.
In this post you are going to meet how to manage exactly that, starting with the recommended approach as of iOS 15 and onwards. For reasons of completeness, I’ll also cover how that was possible before iOS 15, and lastly, I’ll also mention a common, more manual and less recommended way to dismiss presented content.
Dismissing presented content
For the sake of the example, suppose that we have two SwiftUI views where the first one presents the second in a sheet. In the code example that follows, the showSheet
state property in ContentView
determines when the sheet shows up modally. Every time the button in the ContentView
is pressed, showSheet
becomes true
and the sheet appears presenting the contents of the ModalView
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
struct ContentView: View { @State private var showSheet = false var body: some View { Button(“Show Sheet”) { showSheet = true } .sheet(isPresented: $showSheet) { ModalView() } } } // This view is presented modally in a sheet on top of ContentView. struct ModalView: View { var body: some View { VStack { Text(“Modal View”) .font(.title2) .padding(.bottom, 40) Button(“Dismiss”) { } } } } |
Even though we can slide down to dismiss the sheet, the purpose is to make the button in ModalView
initiate the dismissal. SwiftUI simplifies that task, as it provides us with a dismiss action that’s accessible as an environment value.
In particular, that environment value is called dismiss
, and it’s an instance of a special type named DismissAction
. The first step towards hiding the presented view is to declare such an environment property in it:
1 2 3 4 5 6 7 |
struct ModalView: View { @Environment(\.dismiss) private var dismiss // Rest of the implementation… } |
The second and last step is to invoke dismiss
as a function in any place that’s necessary to perform a dismiss action. In this example, that’s the button’s action closure in ModalView
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
struct ModalView: View { @Environment(\.dismiss) private var dismiss var body: some View { VStack { // Other content… Button(“Dismiss”) { dismiss() } } } } |
Note that dismiss
is suffixed with parentheses; even though we declare it as a property, we call it as a function. That’s possible thanks to a particular method defined in the DismissAction
type, named callAsFunction()
. I won’t get into details about it here, that’s the topic of a different tutorial.
The above two additions is all you need in every view that provides custom controls to dismiss it. Just make sure that you use the dismiss
environment object in the presented view, not the presenting one.
💡 Tip: If you perform dismiss actions often, then you can make things a bit more faster by turning the @Environment(\.dismiss) private var dismiss
declaration into a custom code snippet.
Dismissing views before iOS 15
If you’re aiming on supporting iOS versions prior to 15, then you should resort in using a different (nowadays deprecated) API, yet similar to what already demonstrated above. Specifically, it’s another environment value that SwiftUI makes available as shown right next:
1 2 3 |
@Environment(\.presentationMode) private var presentationMode |
To dismiss the presented view using the presentationMode
environment value all we need is to call a dismiss()
method. That’s accessible through the wrappedValue
of presentationMode
:
1 2 3 |
presentationMode.wrappedValue.dismiss() |
Here’s ModalView
from the previous part making use of presentationMode
this time:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
struct ModalView: View { @Environment(\.presentationMode) private var presentationMode var body: some View { VStack { Text(“Modal View”) .font(.title2) .padding(.bottom, 40) Button(“Dismiss”) { presentationMode.wrappedValue.dismiss() } } } } |
Dismissing views using a binding value
Besides everything else already demonstrated in the previous parts, there is also another technique to dismiss modal views. It’s based on passing a binding to the state property that controls the appearance of the modal from the presenting to the presented view.
Being more precise, let’s see once again the ModalView
from the previous examples, only this time there are no environment values at all. Instead, we declare a binding property that will trigger the disappearance of the view:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
struct ModalView: View { @Binding var showSheet: Bool var body: some View { VStack { Text(“Modal View”) .font(.title2) .padding(.bottom, 40) Button(“Dismiss”) { showSheet = false } } } } |
Notice that showSheet
becomes false
in the action closure of the button, and that’s where the dismissal of the view is initiated.
The original binding value is provided as argument by the view that presents the modal, which in this case is the ContentView
. In the following updated version of it, you can see that the ModalView
instance is initialized and gets as argument the binding value of the showSheet
state property; the one that determines whether the modal view should appear or not:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
truct ContentView: View { @State private var showSheet = false var body: some View { Button(“Show Sheet”) { showSheet = true } .sheet(isPresented: $showSheet) { ModalView(showSheet: $showSheet) } } } |
When showSheet
becomes true
in the ContentView
, then the ModalView
appears. Later, when it becomes false
in ModalView
, then the latter is dismissed.
Although the method showcased in this part works fine, it’s not the recommended way to dismiss modal views. There are two reasons for that. The first is that SwiftUI already provides an environment value to perform a dismiss action, so we can avoid taking care of that manually. And that brings us to the second reason; passing a binding value as argument is a totally unnecessary step. But at the bottom end, it’s up to you to decide if you’re more fond of this way, even if it includes an additional step in the development workflow.
Conclusion
SwiftUI provides us with the needed tools to dismiss presented content, either that’s a modal or a pushed view in a navigation stack. The environment value you’ll use depends on the system version you’re targeting to, with the dismiss action being the way to go since iOS 15. Dismissing presented views is also possible by passing binding values that toggle their appearance, but as already said in the last part, it shouldn’t be the preferred method just because it works. In any case, it’s all up to you to pick your tools.
I hope you found this post valuable. Thanks for reading!