16

I am creating an App where the login / register part is inside a modal, which is shown if the user is not logged in.

The problem is, that the user can dismiss the modal by swiping it down...

Is it possible to prevent this?

var body: some View {
    TabView(selection: $selection) {
        App()
    }.sheet(isPresented: self.$showSheet) { // This needs to be non-dismissible
        LoginRegister()
    }
}

Second example:

I am using a modal to ask for information. The user should not be able to quit this process except by dismissing the modal with save button. The user has to input information before the button works. Unfortunately the modal can be dismissed by swiping it down.

Is it possible to prevent this?

krjw
  • 4,070
  • 1
  • 24
  • 49
  • 2
    Modals typically (as in most always) are made to be dismissed. Why not change your app design? Make your login be a complete `View`? Put it in a `ZStack` on top of whatever view you wish. Now you have 100% control (which you don't have with a modal) and can (a) animate presenting it, (b) keep it visible until your code says to dismiss it, and (c) animate dismissing it. –  Aug 07 '19 at 17:59
  • Starting from iOS 15 you can use `interactiveDismissDisabled` - see [this answer](https://stackoverflow.com/a/68009509/8697793). – pawello2222 Jun 16 '21 at 20:17

5 Answers5

16

iOS 15 and later:

Use .interactiveDismissDisabled(true) on the sheet, that's all.

Prev iOS 15:

You can try to do this by using a highPriorityGesture. Of course the blue Rectangle is only for demonstration but you would have to use a view which is covering the whole screen.

struct ModalViewNoClose : View {
    @Environment(\.presentationMode) var presentationMode
    
    let gesture = DragGesture()
    
    var body: some View {
        
        Rectangle()
            .fill(Color.blue)
            .frame(width: 300, height: 600)
            .highPriorityGesture(gesture)
            
            .overlay(
                VStack{
                    Button("Close") {
                        self.presentationMode.value.dismiss()
                    }.accentColor(.white)
                    Text("Modal")
                        .highPriorityGesture(gesture)
                    TextField("as", text: .constant("sdf"))
                        .highPriorityGesture(gesture)
                } .highPriorityGesture(gesture)
        )
            .border(Color.green)
    }
}
Marc T.
  • 5,090
  • 1
  • 23
  • 40
  • I will mark this as answer because it comes closest to what I asked for. Thanks it works great! – krjw Aug 08 '19 at 09:41
  • 3
    Unfortunately you can still use two or more fingers at once to dismiss the modal. Any thoughts? – user1302387 Feb 14 '20 at 20:11
  • @user1302387 Did you solve it or perhaps try my method at at https://stackoverflow.com/a/60939207/6433690? – R. J. Apr 08 '20 at 01:19
4

This is a common problem and a "code smell"... well not really code but a "design pattern smell" anyway.

The problem is that you are making your login process part of the rest of the app.

Instead of presenting the LoginRegister over the App you should really be showing either App or LoginRegister.

i.e. you should have some state object like userLoggedIn: Bool or something and depending on that value you should show either App or LoginRegister.

Just don't have both in the view hierarchy at the same time. That way your user won't be able to dismiss the view.

Fogmeister
  • 76,236
  • 42
  • 207
  • 306
  • Ok sorry I am in a bad mood! I know that I asked for an XY Problem, but I couldn't find anything on google and I thought maybe someone already tried that. I am merely experimenting, trying to figure out what's new. As I said this is an example use case... And you are right ... I don't know what I am doing... – krjw Aug 07 '19 at 16:35
  • 1
    @krjw no worries it’s all good. It can be frustrating when you feel like you’ve tried everything and aren’t getting anywhere. Hope you can solve your problem. – Fogmeister Aug 07 '19 at 19:40
  • 1
    I'd argue it isn't a bad idea to do something like this if you're "timing out" a user in a sensitive app (eg., banking/medical) - if a user doesn't use it for 5 minutes, show the auth screen modally, upon successful login dismiss. This allows the user to not lose their place in the app. – powerj1984 Jul 30 '20 at 21:41
4

If you dont mind using Introspect:

import Introspect

@available(iOS 13, *)
extension View {
    /// A Boolean value indicating whether the view controller enforces a modal behavior.
    ///
    /// The default value of this property is `false`. When you set it to `true`, UIKit ignores events
    /// outside the view controller's bounds and prevents the interactive dismissal of the
    /// view controller while it is onscreen.
    public func isModalInPresentation(_ value: Bool) -> some View {
        introspectViewController {
            $0.isModalInPresentation = value
        }
    }
}

Usage:

.sheet {
    VStack {
        ...
    }.isModalInPresentation(true)
}
Casper Zandbergen
  • 3,419
  • 2
  • 25
  • 49
3

iOS 15+

Starting from iOS 15 you can use interactiveDismissDisabled.

You just need to attach it to the sheet:

var body: some View {
    TabView(selection: $selection) {
        App()
    }.sheet(isPresented: self.$showSheet) {
        LoginRegister()
            .interactiveDismissDisabled(true)
    }
}

Regarding your second example, you can pass a variable to control when the sheet is disabled:

.interactiveDismissDisabled(!isAllInformationProvided)

You can find more information in the documentation.

pawello2222
  • 46,897
  • 22
  • 145
  • 209
0

theoretically this may help you (I didn't tryed it)

private var isDisplayedBind: Binding<Bool>{ Binding(get: { true }, set: { _ = $0 }) }

and usage:

content
    .sheet(isPresented: isDisplayedBind) { some sheet }
Andrew_STOP_RU_WAR_IN_UA
  • 9,318
  • 5
  • 65
  • 101