keegan3d
keegan3d

Reputation: 11275

SwiftUI: close modal

I know this question has been asked and answered before. Not sure if this changed/broke in Beta 4 for SwiftUI, but I can't seem to get the isPresented solution to work to dismiss a modal shown with sheet.

Here is a simple example of what I tried, I thought this would work, but clicking "Close" does nothing and when I inspect self.isPresented?.value it's nil.

struct DetailView: View {
    @Environment(\.isPresented) var isPresented: Binding<Bool>?
    var body: some View {
        Button(action: {
            self.isPresented?.value = false
        }) {
            Text("Close")
        }
    }
}

struct ContentView: View {
    @State private var showingModal = false
    var body: some View {
        Button(action: {
            self.showingModal = true
        }) {
            Text("Show detail")
        }.sheet(isPresented: $showingModal) {
            DetailView()
        }
    }
}

Update based on suggestion, this works. Seems like too much book keeping to me, hope this gets updated.

struct DetailView: View {
    @Binding var showingModal: Bool
    var body: some View {
        Button(action: {
            self.showingModal = false
        }) {
            Text("Close")
        }
    }
}

struct ContentView: View {
    @State private var showingModal = false
    var body: some View {
        Button(action: {
            self.showingModal = true
        }) {
            Text("Show detail")
        }.sheet(isPresented: $showingModal) {
            DetailView(showingModal: self.$showingModal)
        }
    }
}

Upvotes: 2

Views: 5426

Answers (4)

shim
shim

Reputation: 10116

Another solution is to add a delegate property to your SwifUI view which passes the dismiss action back to the presenter.

protocol MySwiftUIViewDelegate: class {
    func myDismissAction()
}

struct MySwiftUIView {
    weak var delegate: MySwiftUIViewDelegate?
    
    var body: some View {
        Button("Dismiss") {
            self.delegate?.myDismissAction()
        }
    }
}

class MyViewController: UIViewController, MySwiftUIViewDelegate {
    func presentMyView() {
        var myView = MySwiftUIView()
        myView.delegate = self

        let hostingViewController = UIHostingController(rootView: myView)
        present(vc, animated: true, completion: nil)
    }

    // MARK: - MySwiftUIViewDelegate

    func myDismissAction() {
        dismiss(animated: true)
    }
}

While this may seem a bit convoluted, it is also arguably better to make the presenter responsible for handling the dismissal, so that the view doesn't have to know how it was presented (e.g. push vs modal), thus making your code more modular. Plus you might want other delegated methods, depending on what you're working on, so you might have a delegate protocol already. And it gives you a convenient place to execute any additional code when the view is dismissed.

(Although keep in mind that depending on the modal presentation style / settings, users may also be able to dismiss by pulling the view down.)

Upvotes: 3

kvaruni
kvaruni

Reputation: 842

A somewhat neater solution could be to define a callback function:

struct DetailView: View {
    var dismiss: () -> ()

    var body: some View {
        Button(action: dismiss) {
            Text("Close")
        }
    }
}

struct ContentView: View {
    @State private var showingModal = false

    var body: some View {
        Button(action: {
            self.showingModal = true
        }) {
            Text("Show detail")
        }.sheet(isPresented: $showingModal) {
            DetailView(dismiss: { self.showingModal = false })
        }
    }
}

The benefit over the extra bookkeeping is that DetailView no longer needs to be aware that it is a modal, allowing it to be used in different contexts. Furthermore, you keep all the relevant code for showing and dismissing the modal inside the original view.

Now, regardless of which method you use to dismiss the modal, you should be wary that modals are still very buggy, even in beta 6. I lost too much time on various situations where none of these solutions work as they should:

  • when you put the Button in ContentView inside a List (or ScrollView, as List is just a special type of ScrollView) then the button only works once. You can show the modal and dismiss it, but you won't be able to show it again ...
  • when you are using a NavigationView and you add the Button to the .navigationBarItems, then you will be able to show the modal as often as you like. However, the Close button in DetailView will not work ...

So far I have not been able to make a dismiss button work correctly in these situations. This limits the use of a modal quite a bit in real-life apps. There is every chance that this will be fixed for the GM, but just be aware of these issues until they are known to be fixed.

Upvotes: 2

backslash-f
backslash-f

Reputation: 8193

Beta 6

Use presentationMode from the @Environment.

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

    var body: some View {
        VStack {
            Text("Ohay!")
            Button("Close") {
                self.presentationMode.wrappedValue.dismiss()
            }
        }
    }
}

Upvotes: 11

matt
matt

Reputation: 535139

In the presenting view, configure a State bool set to false, and pass the binding into the sheet call. To present, set it to true. But also pass the binding into the second view, so that a button there can set it to false again.

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

struct Modal : View {
    @Binding var isPresented : Bool
    var body: some View {
        Button("Done", action: {self.isPresented = false})
    }
}

Upvotes: 2

Related Questions