5

Xcode: Version 11.3 (11C29)
Target: Simulator running iOS 13.3

I'm getting inconsistent behaviour that's hard to diagnose when working with sheets and SwiftUI. When I:
* Use a button in the NavigationView itself, I can consistently call up the sheet and dismiss it
* Use a button in the bar of the NavigationView (i.e. navigationBarItems), the behaviour is inconsistent: sometimes it toggles without issue, sometimes it will "lock up" and not respond for ~10 seconds before functioning again. Swiping around and performing other actions in the interface seem to help in "resetting" the functionality.

Minimum reproducible example:

import SwiftUI

struct ContentView: View {
    @State var isModalShowing = false

    var body: some View {
        NavigationView {
            VStack {
                List {
                    ForEach(1..<5) { number in
                        Text(String(number))
                    }
                }
                Button("First Modal Button") {
                    self.isModalShowing = true
                }
            }
            .navigationBarItems(leading:
                Button("Second Modal Button") {
                    self.isModalShowing = true
            })
        }
        .sheet(isPresented: $isModalShowing) {
            TestView(isPresented: self.$isModalShowing)
        }
    }
}

struct TestView: View {
    @Binding var isPresented: Bool
    var body: some View {
        VStack {
            Text("Hello SwiftUI!")
            Button("Dismiss") {
                self.isPresented = false
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

To note:
* Pressing First modal button works consistently
* Pressing Second modal button works inconsistently after dismissing the modal for the first time (try using this button ~5-10 times in a row, it should lock up, but it's hard to predict when it will)

There's already a few StackOverflow questions/answers about this, but none have worked for me so far and they seem to talk about the Xcode Betas, whereas I am on stable (hopefully 11.3 is stable). Different ways of passing the state (@Binding vs @Environment) didn't help. Moving around where the .sheet is called also didn't change anything. I think it's also related to the List and/or ForEach but I'm new to Swift and not sure how to debug further.

d00bsm3n
  • 93
  • 6

5 Answers5

5

I believe this is a bug. Though my answer isn't going to solve this problem, but can give you options to consider.

Out of curiosity, I did some debugging of the view hierarchy. With the current setup, initially the navigation bar button stays here (highlighted as blueish color):

enter image description here

Then you present the sheet and do the dismissal of the sheet accordingly. After the dismissal, the position of the navigation bar button isn't quite right. See:

enter image description here

The Text of the button stays in the right position, but the bar button is displaced from where it should be.

But this displacement doesn't happen if you use NavigationLink instead of .sheet presentation.

Well, this issue is prevalent for the large option of TitleDisplayMode of navigation bar title, and it's the default for a quite long. But if the inline option is used, the issue doesn't exist. See, with the inline option, before and after the sheet dismissal the navigation bar button stays at the same place:

enter image description here


So that means, you now have two options to consider:

  1. Use NavigationLink instead of sheet presentation. You can do this by placing the below code inside the outermost View (in your case the VStack):

    NavigationLink(destination: TestView(), isActive: self.$isModalShowing) {
        EmptyView()
        // no need to provide any other view as it will be triggered by the action
        // of navigation bar button item which already provides its own view
    }
    

Note: This NavigationLink option isn't tested with Xcode 11.3 or newer as navigation link seems to misbehave with this version. But works as expected upto Xcode 11.2.1. More on SwiftUI unable to navigate back and forth with navigationLink

  1. Use the inline option of the navigation bar TitleDisplayMode as:

    .navigationBarTitle(Text("Home"), displayMode: .inline)
    // you can get rid of the title "Home" with empty "" string though
    
nayem
  • 7,285
  • 1
  • 33
  • 51
  • Great and detailed answer! I ended up implementing the second option as it's just a toy project and I don't particularly care about the looks. Works consistently! Last question: should I be reporting this bug somewhere? – d00bsm3n Dec 27 '19 at 09:46
  • Yes! You can. There are [discussion forums](https://forums.developer.apple.com/welcome), [feedback assistant](https://feedbackassistant.apple.com) for this purpose. But I never bothered. – nayem Dec 27 '19 at 10:28
  • Thanks @nayem this was driving me insane! – Tom Wicks Jan 05 '20 at 11:53
  • Thanks @nayem now I can avoid these hidden view hacks – kocodude Feb 14 '20 at 05:23
0

A different way to dismiss the modal sheet:

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

    var body: some View {
        VStack {
            Text("Hello SwiftUI!")
            Button("Dismiss") {
                self.presentationMode.wrappedValue.dismiss()
            }
        }
    }
}
Yonat
  • 4,382
  • 2
  • 28
  • 37
  • As I mentioned in my OP: `Different ways of passing the state (@Binding vs @Environment) didn't help.` Or did you modify something else as well that I missed? – d00bsm3n Dec 27 '19 at 09:23
  • The code above has no state passing. Instead it uses the built-in `@Environment(\.presentationMode)`. Try it and see, just don't wrap your `TestView` with an `.environment()` modifier -- the code uses a built-in environment var. – Yonat Dec 27 '19 at 10:46
0

None of the solutions here worked for me, but this one does -- dropping down to UIKit for a different dismiss function: What is different between @Binding and scroll down to dismiss presented view?

Matt Corey
  • 96
  • 3
0

There is a workaround by using a Text view with onTapGesture, however you'll lose the Button color animation when tapped.

workaround

The code would be something like:

.navigationBarItems(leading:
    Text("Text")
        .onTapGesture {
            self.isPresentingSheet = true
        }
        .sheet(isPresented: self.$isPresentingSheet, content: {
            SheetView()
        })
)
JS1010111
  • 341
  • 2
  • 12
0

I found an alternative way in dealing with this problem while still using a button and a large title. Here's the code:

struct ContentView: View {

    @State var isModalShowing = false
    @State var opacity: Double = 1

    var body: some View {
        NavigationView() {

            List() {

                // Some views here

            }
            .navigationBarTitle(Text("Home"), displayMode: .automatic)
            .navigationBarItems(trailing: 

                Button(action: {

                    self.opacity = 0.5
                    self.isModalShowing = true

                }, label: {

                    Image(systemName: "plus")

                })

                    .opacity(self.opacity)
                    .scaleEffect(1.5)
                    .sheet(isPresented: $isModalShowing,
                           onDismiss: {
                               self.opacity = 1
                        },    
                           content: {

                               NewView(onDismiss: self.$isModalShowing)

                        })
                )
           }

      }
}

Basically, we change the opacity when the button is tapped, and change it back when the sheet is dismiss which I think would force the button to update itself.

As for dismissing the sheet, OP's way works in this case

Boom PT
  • 374
  • 4
  • 11