91

Since SwiftUI is declarative there is no dismiss method. How can is add a dismiss/close button to the DetailView?

struct DetailView: View {
  var body: some View {
  Text("Detail")
  }
}

struct ContentView : View {
  var body: some View {
  PresentationButton(Text("Click to show"), destination: DetailView())
  }
}
lewis
  • 2,936
  • 2
  • 37
  • 72
Ugo Arangino
  • 2,802
  • 1
  • 18
  • 19
  • 1
    None of the examples I've seen have a method to dismiss a presented view, so I don't think there is one yet. – EmilioPelaez Jun 09 '19 at 22:38
  • I'm pretty sure that they will introduce it with the next beta release. Pop method is missing too. – Andrea Miotto Jun 10 '19 at 09:53
  • 2
    I think it's important to remember SwiftUI is a paradigm shift. We have to think more in terms of "state" and less in terms of writing out conditional statements, etc. So as others have written, it's more about listening to state via the `@Environment` or `@State` or other "Property Wrappers." This is a shift to the Observer Pattern in a declarative framework, for those who like complicated phrases :-) – davidrynn Jun 17 '19 at 15:15
  • There is now a very clean way to do this in Beta 5. See my answer below. BTW, the same method works for popping a navigation view. – Chuck H Jul 30 '19 at 20:50
  • Looks like in iOS 15 they introduced exactly what you wanted - the [DismissAction](https://developer.apple.com/documentation/swiftui/dismissaction?changes=latest_minor). See [this answer](https://stackoverflow.com/a/67893846/8697793). – pawello2222 Jun 08 '21 at 20:00

17 Answers17

141

Using @State property wrapper (recommended)

struct ContentView: View {
    @State private var showModal = false
    
    var body: some View {
       Button("Show Modal") {
          self.showModal.toggle()
       }.sheet(isPresented: $showModal) {
            ModalView(showModal: self.$showModal)
        }
    }
}

struct ModalView: View {
    @Binding var showModal: Bool
    
    var body: some View {
        Text("Modal view")
        Button("Dismiss") {
            self.showModal.toggle()
        }
    }
}

Using presentationMode

You can use presentationMode environment variable in your modal view and calling self.presentaionMode.wrappedValue.dismiss() to dismiss the modal:

struct ContentView: View {

  @State private var showModal = false

  // If you are getting the "can only present once" issue, add this here.
  // Fixes the problem, but not sure why; feel free to edit/explain below.
  @SwiftUI.Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>


  var body: some View {
    Button(action: {
        self.showModal = true
    }) {
        Text("Show modal")
    }.sheet(isPresented: self.$showModal) {
        ModalView()
    }
  }
}


struct ModalView: View {

  @Environment(\.presentationMode) private var presentationMode

  var body: some View {
    Group {
      Text("Modal view")
      Button(action: {
         self.presentationMode.wrappedValue.dismiss()
      }) {
        Text("Dismiss")
      }
    }
  }
}

enter image description here

LopesFigueiredo
  • 146
  • 1
  • 8
M Reza
  • 18,350
  • 14
  • 66
  • 71
  • Thank you for the hint with Environment. How to access `isPresented` for outside like in my example? – Ugo Arangino Jun 12 '19 at 16:38
  • This is news to me. Thanks! – Brad Jun 26 '19 at 14:01
  • Good find! However, for me (using Xcode 11 Beta 3) this only works once for me when the PresentationLink is used inside a List or navigationBarItems. I can present and dismiss the view once from each button. – Drarok Jul 09 '19 at 11:00
  • 5
    I also experienced the Beta 3 "presents only once" if using a List problem. However, Beta 4 seems to have broken the ability for the Modal to dismiss itself with the isPresented environment var in some cases. The above example still works, but my sample does not. I'm still trying to isolate the issue. – Chuck H Jul 19 '19 at 16:04
  • How looks view which presents ModalView? – Ramis Sep 21 '19 at 20:32
  • 4
    I notice in `Xcode Version 11.0 (11A419c)` that when using `self.presentationMode.wrappedValue.dismiss()` getting called that the `onDismiss` function on `.sheet(` is not getting called. When I dismiss the modal view by pulling down the callback gets called. – philipp Sep 24 '19 at 16:51
  • 2
    You can also just use `@Environment(\.presentationMode) var presentationMode` since Swift will infer the type via the specified keypath. – Kilian Nov 01 '19 at 22:33
  • 2
    This is wrong. You should be passing a state variable which is also used for isPresented, rather than messing with the presentationMode. – stardust4891 Nov 14 '19 at 20:36
  • 1
    I agree with @stardust4891. You should pass a state variable. Use the answer below. This could cause problems at a later stage. E.g. using with a TabView. – LateNate Aug 02 '20 at 12:28
  • 1
    I think it is better to pass a `@State` variable rather than use `PresentationMode`. `PresentationMode` will **not** always dismiss the modal. For example, if you have a `NavigationView` in your modal [like in this answer](https://stackoverflow.com/a/63909251/1016508), then calling `dismiss()` will only pop to the previous view if you have navigated to a different screen. – iMaddin Oct 19 '20 at 07:25
  • I used the approach of `@Environment(\.presentationMode)` and closing modals is now misbehaving in unpredicatable way. Falling back to passing state variable to modal. – 3h4x Nov 22 '20 at 15:10
  • Is there way we can move content at Top and also modelview we can set for half screen only? – Gaurav Thummar Dec 11 '20 at 19:34
33

iOS 15+

Instead of presentationMode we can now use DismissAction.

Here is an example from the documentation:

struct SheetView: View {
    @Environment(\.dismiss) var dismiss

    var body: some View {
        NavigationView {
            SheetContents()
                .toolbar {
                    Button("Done") {
                        dismiss()
                    }
                }
        }
    }
}
pawello2222
  • 46,897
  • 22
  • 145
  • 209
  • 1
    This is a nice and concise way for iOS 15. Still, I think - as most answers providing a solution utilising `@State` or `@Environment`, IMHO this is not the _correct_ way in _most_ use cases. This approach shifts the logic when a modal will be presented into the view. Logic in the views? The better approach IMHO is to utilise a "view model" or similar thing which performs the logic. In case of a modal it simply provides an appropriate "view state" which clearly defines when to show a modal and when not and also handles "dismiss" actions (function calls initiated by the user) instead of the view – CouchDeveloper Jul 02 '21 at 09:19
  • 1
    @CouchDeveloper respectfully this is terrible advice and is completely contrary to Apple's own guidance and instruction. A SwiftUI view is BOTH the View AND the ViewModel, there is absolutely no reason to separate the two besides looking busy and doubling your file count for nothing. There is a reason why apple built states, bindings, environment objects etc to be utilized within the swiftui view itself. Please feel free to search Apple's own developer forums for much more indepth clarification – Hajji Daoud Dec 12 '22 at 14:13
  • tl;dr this is the most correct way per Apple themselves, SwiftUI views are Views and ViewModels in one. – Hajji Daoud Dec 12 '22 at 14:14
  • @HajjiDaoud When you look at this example the action of the button represents the "computation" of the logic `dismiss()` - quite simple. Now imagine, this would be a rather complex computation, taking more state into account, not in the view and possibly involving services which require to await a result. Now, it will be more resilient, or even become mandatory, to let a model perform this computation. In this case you would just need a let constant determining the state of the modal which then is rendered accordingly. You won't need a @State variable holding the computation result. – CouchDeveloper Dec 12 '22 at 20:08
  • @HajjiDaoud But you are right: when you can solve it in a simple way, keep it simple. If it's more complex you can use ObservedObject and StateObject. With these things you can encapsulate state _and_ (complex) logic. ;) – CouchDeveloper Dec 12 '22 at 20:17
28

In Xcode 11 Beta 5, another way to do this is to use @State in the view that launches the modal, and add a binding in the modal view to control visibility of the modal. This doesn't require you to reach into the @Environment presentationMode variable.

struct MyView : View {
    @State var modalIsPresented = false

    var body: some View {
        Button(action: {self.modalIsPresented = true})  {
            Text("Launch modal view")
        }
        .sheet(isPresented: $modalIsPresented, content: {
            MyModalView(isPresented: self.$modalIsPresented)
        })
    }
}


struct MyModalView : View {
    @Binding var isPresented: Bool
    
    var body: some View {
        Button(action: {self.isPresented = false})  {
            Text("Close modal view")
        }
    }
}
lewis
  • 2,936
  • 2
  • 37
  • 72
thiezn
  • 1,874
  • 1
  • 17
  • 24
  • 4
    Kudos for hewing to the principles of SwiftUI with the declarative approach and single source of truth – Collierton Sep 02 '19 at 15:05
  • 2
    It only works the first time, if I close and try again to open the window it doesn't work anymore. – Mario Burga Sep 12 '19 at 23:09
  • 1
    It seems to work ok for me, perhaps you are changing the isPresented value somewhere else? For instance, if you dismiss the modal by pulling down, swiftUI automatically toggles the value. Instead of setting the value explicitly to true/false, try to use isPresented.toggle() instead – thiezn Sep 14 '19 at 10:28
  • If I create a project with only these two views, it works fine. But in my real-world app, it doesn't work. I get the same behaviour Mario is seeing. I do not set isPresented anywhere else. – Henning Jan 02 '20 at 13:47
  • @stardust4891 Why is this the correct answer? What is wrong with the other ones? – Scott Swezey Mar 15 '20 at 18:14
  • 1
    I agree with @stardust4891 it's a shame the presentation Mode got more upvotes for it's answer. When you look at the official documentation on wrappedValue, this is what Apple wrote: "This property provides primary access to the value’s data. However, you don’t access wrappedValue directly. Instead, you use the property variable created with the \@Binding attribute." It stimulates the use of bindings as shown in this answer. For a single source of truth. – Mark May 24 '20 at 13:30
  • 1
    Writing your own `@Binding var isPresented: Bool` is the most flexible option. It can even be declared in your vm as an `@Published`, instead of the `@State` as above. I was presenting a modal with a NavigationView, that I wanted to be able to dismiss the entire modal on any NavigationLink's "Done Button". Using presentationMode, resulted in me having to track more state than necessary. But simply binding to my vm allowed me to dismiss the modal easily from any Done button, by simply flipping `isPresented` to false. – hidden-username May 25 '20 at 15:23
  • 1
    If anyone is wondering how to make a preview work for the modal view above: Use `.constant` to create a binding with an immutable value, like this `MyModalView(isPresented: .constant(true))` – JacobF Aug 22 '20 at 19:55
20

Here's a way to dismiss the presented view.

struct DetailView: View {
    @Binding
    var dismissFlag: Bool

    var body: some View {
        Group {
            Text("Detail")
            Button(action: {
                self.dismissFlag.toggle()
            }) {
                Text("Dismiss")
            }
        }

    }
}

struct ContentView : View {
    @State var dismissFlag = false

    var body: some View {
        Button(action: {
            self.dismissFlag.toggle()
        })
        { Text("Show") }
            .presentation(!dismissFlag ? nil :
                Modal(DetailView(dismissFlag: $dismissFlag)) {
                print("dismissed")
            })
    }
}

enter image description here

SMP
  • 1,629
  • 7
  • 15
  • 1
    Thanks, But if user drag to dismiss, the toggle need to press twice. Can be workaround with switching the state `self.dismissFlag = true; self.dismissFlag = false;`. Workaround, not solution. Also looking a way to disable drag to dismiss. – DazChong Jun 10 '19 at 08:16
  • I think if you implemented `onDismiss` in the `Modal` constructor, you would be able to keep `dismissFlag` in sync. I haven't tried it to be sure. – Jerry Jun 28 '19 at 16:33
  • 1
    To verify this I've just tested what happens with the ```self.dismissFlag``` when dismissing the view using drag motion. Add ```onDismiss: { print(self.dismissFlag) }``` to your .sheet to test yourself. It seems it's automatically toggling the variable when dragging. Note, the onDismiss function only seems to be called when dragging the modal view away. If you close the modal by toggling the ```self.dismissFlag``` yourself the ```onDismiss``` isn't being called. (I'm on iOS 13 Beta 8) – thiezn Aug 22 '19 at 20:27
11

Seems that for Xcode 11 Beta 7 (this is on build 11M392r of Xcode) it's slightly different.

@Environment(\.presentationMode) var presentation


Button(action: { self.presentation.wrappedValue.dismiss() }) { Text("Dismiss") }
Tomm P
  • 761
  • 1
  • 8
  • 19
  • This is wrong. You should be passing a state variable which is also used for isPresented, rather than messing with the presentationMode. – stardust4891 Nov 14 '19 at 20:38
9

New in Swift 5.5 and SwiftUI 3:

@Environment(\.dismiss) var dismiss

Then in function or somewhere in body code, simply call:

self.dismiss()
Tom GODDARD
  • 641
  • 6
  • 9
7

You can implement this.

struct view: View {
    @Environment(\.isPresented) private var isPresented

    private func dismiss() {
        isPresented?.value = false
    }
}
iOSCS
  • 95
  • 1
  • Thank you for the hint with Environment. How to access `isPresented` for outside like in my example? – Ugo Arangino Jun 12 '19 at 16:38
  • 1
    This is wrong. You should be passing a state variable which is also used for isPresented, rather than messing with the presentationMode. – stardust4891 Nov 14 '19 at 20:37
6

Automatically pop if in Navigation or dismiss if Modal


Just take the presentationMode from the environment in the destination view and dismiss the wrappedValue from it:

struct DestinationView: View {
    @Environment(\.presentationMode) private var presentationMode

    var body: some View {
        Button("Dismiss") {
            self.presentationMode.wrappedValue.dismiss()
        }
    }
}


Demo ( pop / dismiss )

Pop Dismiss

Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
  • 2
    Thank you for posting this. This is why `PresentationMode` is probably not the best solution to dismiss a modal because it may instead pop to the previous view if you have a `NavigationView`. If you want to make sure that you dismiss a modal then you should pass a `@State` variable. – iMaddin Oct 19 '20 at 07:30
5

There is now a pretty clean way to do this in Beta 5.

import SwiftUI

struct ModalView : View {
    // In Xcode 11 beta 5, 'isPresented' is deprecated use 'presentationMode' instead
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
    var body: some View {
        Group {
            Text("Modal view")
            Button(action: { self.presentationMode.wrappedValue.dismiss() }) { Text("Dismiss") }
        }
    }
}

struct ContentView : View {
    @State var showModal: Bool = false
    var body: some View {
        Group {
            Button(action: { self.showModal = true }) { Text("Show modal via .sheet modifier") }
                .sheet(isPresented: $showModal, onDismiss: { print("In DetailView onDismiss.") }) { ModalView() }
        }
    }
}
Chuck H
  • 7,434
  • 4
  • 31
  • 34
  • This is wrong. You should be passing a state variable which is also used for isPresented, rather than messing with the presentationMode. – stardust4891 Nov 14 '19 at 20:38
  • 6
    Wrong in what context? Is your issue a matter of correctness or a preference of style? There are multiple other ways to accomplish the same task that also work just as well. Apple's own iOS 13 Release Notes documents this as a method to dismiss Modals and it works. Thanks. – Chuck H Nov 15 '19 at 23:03
  • This is interesting insight, however, it may or may not be a real problem. It was explained early on was that dismiss() was added as a convenience so a binding to the isPresented var would not have to be passed in to the Modal View in addition to the sheet modifier. All it does is set the isPresented var to false if it is true, otherwise (per the SwiftUI header file) it does nothing. – Chuck H Nov 17 '19 at 00:47
  • I think it is better to pass a `@State` variable rather than use `PresentationMode`. `PresentationMode` will **not** always dismiss the modal. For example, if you have a `NavigationView` in your modal [like in this answer](https://stackoverflow.com/a/63909251/1016508), then calling `dismiss()` will only pop to the previous view if you have navigated to a different screen. – iMaddin Oct 19 '20 at 07:27
3

Since PresentationButton is easy to use but hiding the state wich is undermining the predictive character of SwiftUI I have implemented it with an accessible Binding.

public struct BindedPresentationButton<Label, Destination>: View where Label: View, Destination: View {
    /// The state of the modal presentation, either `visibile` or `off`.
    private var showModal: Binding<Bool>

    /// A `View` to use as the label of the button.
    public var label: Label

    /// A `View` to present.
    public var destination: Destination

    /// A closure to be invoked when the button is tapped.
    public var onTrigger: (() -> Void)?

    public init(
        showModal: Binding<Bool>,
        label: Label,
        destination: Destination,
        onTrigger: (() -> Void)? = nil
    ) {
        self.showModal = showModal
        self.label = label
        self.destination = destination
        self.onTrigger = onTrigger
    }

    public var body: some View {
        Button(action: toggleModal) {
            label
        }
        .presentation(
            !showModal.value ? nil :
                Modal(
                    destination, onDismiss: {
                        self.toggleModal()
                    }
                )
        )
    }

    private func toggleModal() {
        showModal.value.toggle()
        onTrigger?()
    }
}

This is how it is used:

struct DetailView: View {
    @Binding var showModal: Bool

    var body: some View {
        Group {
            Text("Detail")
            Button(action: {
                self.showModal = false
            }) {
                Text("Dismiss")
            }
        }
    }
}

struct ContentView: View {
    @State var showModal = false

    var body: some View {
        BindedPresentationButton(
            showModal: $showModal,
            label: Text("Show"),
            destination: DetailView(showModal: $showModal)
        ) {
            print("dismissed")
        }
    }
}
Ugo Arangino
  • 2,802
  • 1
  • 18
  • 19
2

In Xcode 11.0 beta 7, the value is now wrapped, the following function is working for me:

func dismiss() {
    self.presentationMode.wrappedValue.dismiss()
}
Gareth Jones
  • 1,760
  • 1
  • 14
  • 24
1

The modal views in SwiftUI seem to be simple until you start using them in a List or Form views. I have created a small library which wraps all the edge cases and makes the using of modal views the same as NavigationView-NavigationLink pair.

The library is open-sourced here: https://github.com/diniska/modal-view. You can include it into the project using Swift Package Manager, or just by copying the single file that the library includes.

The solution for your code would be:

struct DetailView: View {
    var dismiss: () -> ()
    var body: some View {
        Text("Detail")
        Button(action: dismiss) {
            Text("Click to dismiss")
        }
    }
}

struct ContentView : View {
    var body: some View {
        ModalPresenter {
            ModalLink(destination: DetailView.init(dismiss:)) {
                Text("Click to show")
            }
        }
    }
}

Additionally, there is an article with full description and examples: How to present modal view in SwiftUI

Denis
  • 3,167
  • 1
  • 22
  • 23
1

You can use Presentation mode to dismiss. Declare

@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

And then when required, dismiss it by

self.presentationMode.wrappedValue.dismiss()
SoumiMaiti
  • 91
  • 10
0

Use Environment variable at PresentationMode. This GitHub link will maybe help you to solve the problem https://github.com/MannaICT13/Sheet-in-SwiftUI

This is simple solution:

struct ContentView2 : View {

    @Environment (\.presentationMode) var presentationMode

    var body : some View {
        VStack {
            Text("This is ContentView2")
            Button(action: {
                self.presentationMode.wrappedValue.dismiss()
            }, label: {
                Text("Back")    
            })    
        }
    }
}


struct ContentView: View {

    @State var isShowingSheet : Bool = false

    var body: some View {
        Button(action: {
            self.isShowingSheet.toggle()
        }, label: {
            Text("Click Here")
        }).sheet(isPresented: $isShowingSheet, content: {  
            ContentView2()
        })
    }
}
Dinsen
  • 2,139
  • 3
  • 19
  • 25
MannaICT13
  • 189
  • 2
  • 9
0

One way to do this might be to declare you own modifier for modal presentation and dismissal.

extension View {

  func showModal<T>(_ binding: Binding<Bool>, _ view: @escaping () -> T) -> some View where T: View {

    let windowHeightOffset = (UIApplication.shared.windows.first?.frame.height ?? 600) * -1

    return ZStack {

      self

      view().frame(maxWidth: .infinity, maxHeight: .infinity).edgesIgnoringSafeArea(.all).offset(x: 0, y: binding.wrappedValue ? 0 : windowHeightOffset)

    }

  }
}

Then you can use the modifier on any view that you wish to tell how to display a view and dismiss that view. Just like a popover or sheet modifier.

struct ContentView: View {

  @State var showModal = false

  var body: some View {

    Text("Show").foregroundColor(.blue).onTapGesture {
      withAnimation(.easeIn(duration: 0.75)) {
        self.showModal = true
      }
    }.showModal($showModal, {

      Text("Dismiss").foregroundColor(.blue).onTapGesture {
        withAnimation(.easeIn(duration: 0.75)) {
          self.showModal = false
        }
      }

    })


  }
}    

The presentation is full screen from the top, if you wish it to come from the side, change the transition inside the modifier to leading or trailing. Other transitions would work too, like opacity or scale.

enter image description here

jnblanchard
  • 1,182
  • 12
  • 12
0

SwiftUI 2 code sample (works with mobiles also)

(sample code doesnt work with swift 1, but you still can try it without @main block)

Full app sample for using sheets:

@main
struct TestAppApp: App {
    var body: some Scene {
        WindowGroup {
            SheetLink(text: "click me!", content: ChildView() )
                .padding(.all, 100)
        }
    }
}

struct ChildView: View {
    var body: some View {
        Text("this is subView!")
    }
}

enter image description here

and when subview is larger than main view:

enter image description here

And code behind this:

struct SheetLink<Content> : View where Content: View {
    @State var text: String
    @State var displaySheet = false
    @State var content: Content


    var body: some View {
        HStack {
            Button(text, action: { self.displaySheet = true } ).buttonStyle(PlainButtonStyle()).foregroundColor(.blue)
        }
        .sheet(isPresented: $displaySheet) {
            SheetTemplateView(isPresented: self.$displaySheet, content: content)
        }
    }
}

struct SheetTemplateView<Content> : View where Content: View {
    @Binding var isPresented: Bool
    @State var content: Content
    
    var body: some View {
        VStack{
            HStack{
                Button("Back!", action: { isPresented.toggle() } ).buttonStyle(PlainButtonStyle()).foregroundColor(.blue)
                Spacer()
            }
            Spacer()
            content
            Spacer()
        }
        .padding()
    }
}
Andrew_STOP_RU_WAR_IN_UA
  • 9,318
  • 5
  • 65
  • 101
-2

You can use SheetKit to dismiss all sheets

SheetKit().dismissAllSheets()

or present new UISheetPresentationController

sheetKit.present(with: .bottomSheet){
  Text("Hello world")
}
Bob Xu
  • 129
  • 2
  • 9