SwiftUI - Navigation bar button not clickable after sheet has been presented - swift

I have just started using SwiftUI a couple of weeks ago and i'm learning. Today I ran into a into an issue.
When I present a sheet with a navigationBarItems-button and then dismiss the ModalView and return to the ContentView I find myself unable to click on the navigationBarItems-button again.
My code is as follows:
struct ContentView: View {
#State var showSheet = false
var body: some View {
NavigationView {
VStack {
Text("Test")
}.sheet(isPresented: self.$showSheet) {
ModalView()
}.navigationBarItems(trailing:
Button(action: {
self.showSheet = true
}) {
Text("SecondView")
}
)
}
}
}
struct ModalView: View {
#Environment(\.presentationMode) var presentation
var body: some View {
VStack {
Button(action: {
self.presentation.wrappedValue.dismiss()
}) {
Text("Dismiss")
}
}
}
}

I think this happens because the presentationMode is not inherited from the presenter view, so the presenter didn't know that the modal is already closed. You can fix this by adding presentationMode to presenter, in this case to ContentView.
struct ContentView: View {
#Environment(\.presentationMode) var presentation
#State var showSheet = false
var body: some View {
NavigationView {
VStack {
Text("Test")
}.sheet(isPresented: self.$showSheet) {
ModalView()
}.navigationBarItems(trailing:
Button(action: {
self.showSheet = true
}) {
Text("SecondView")
}
)
}
}
}
Tested on Xcode 12.5.
Here is the full working
example.

This seems to be a bug in SwiftUI. I am also still seeing this issue with Xcode 11.5 / iOS 13.5.1. The navigationBarMode didn't make a difference.
I filed an issue with Apple:
FB7641003 - Taps on a navigationBarItem Button presenting a sheet sometimes not recognized
You can use the attached example project SwiftUISheet (also available via https://github.com/ralfebert/SwiftUISheet) to reproduce the issue. It just presents a sheet from a navigation bar button.
Run the app and tap repeatedly on the 'plus' button in the nav bar. When the sheet pops up, dismiss it by sliding it down. Only some taps to the button will be handled, often a tap is ignored.
Tested on Xcode 11.4 (11E146) with iOS 13.4 (17E255).

Very hacky but this worked for me:
Button(action: {
self.showSheet = true
}) {
Text("SecondView")
.frame(height: 96, alignment: .trailing)
}

I'm still seeing this issue with Xcode 13 RC and iOS 15. Unfortunately the solutions above didn't work for me. What I ended up doing is adding a small Text view to the toolbar whose content changes depending on the value of the .showingSheet property.
struct ContentView: View {
#State private var showingSheet = false
var body: some View {
NavigationView {
VStack {
Text("Content view")
Text("Swift UI")
}
.sheet(isPresented: $showingSheet) {
Text("This is a sheet")
}
.navigationTitle("Example")
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
// Text view workaround for SwiftUI bug
// Keep toolbar items tappable after dismissing sheet
Text(showingSheet ? " " : "").hidden()
Button(action: {
self.showingSheet = true
}) {
Label("Show Sheet", systemImage: "plus.square")
}
}
}
}
}
}
I realize it's not ideal but it's the first thing that worked for me. My guess is that having the Text view's content change depending on the .showingSheet property forces SwiftUI to fully refresh the toolbar group.

So far, I can still observe the disorder of navi buttons right after dismissing its presented sheet.
FYI, I am using a UINavigationController wrapper instead as workaround. It works well.
Unfortunately, I sure that the more that kind of bugs, the farther away the time of using SwiftUI widely by the ios dev guys. Because those are too basic to ignore.

Very hacky but this worked for me:
I had the same problem. this solution worked for me.
struct ContentView: View {
#State var showSheet = false
var body: some View {
NavigationView {
VStack {
Text("Test")
}.sheet(isPresented: self.$showSheet) {
ModalView()
}.navigationBarItems(trailing:
Button(action: {
self.showSheet = true
}) {
Text("SecondView")
// this is a workaround
.frame(height: 96, alignment: .trailing)
}
)
}
}
}

Only #adamwjohnson5's answer worked for me. I don't like doing it but it's the only solution that works as of Xcode 13.1 and iOS 15.0. Here is my code for anyone interested in seeing iOS 15.0 targeted code:
var body: some View {
NavigationView {
mainContentView
.navigationTitle(viewModel.navigationTitle)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
PlusButton {
viewModel.showAddDialog.toggle()
}
.frame(height: 96, alignment: .trailing) // Workaroud, credit: https://stackoverflow.com/a/62209223/5421557
.confirmationDialog("CatalogView.Add.DialogTitle", isPresented: $viewModel.showAddDialog, titleVisibility: .visible) {
Button("Program") {
viewModel.navigateToAddProgramView.toggle()
}
Button("Exercise") {
viewModel.navigateToAddExerciseView.toggle()
}
}
}
}
.sheet(isPresented: $viewModel.navigateToAddProgramView, onDismiss: nil) {
Text("Add Program View")
}
.sheet(isPresented: $viewModel.navigateToAddExerciseView, onDismiss: nil) {
AddEditExerciseView(viewModel: AddEditExerciseViewModel())
}
}
.navigationViewStyle(.stack)
}

Related

Why doesn't the button's image background show up in toolbar using SwiftUI?

In Apple's calendar app, they provide a toolbar item that toggles its style based on some state. It essentially acts as a Toggle. I'm trying to re-create this same thing in SwiftUI and make it work well in both light and dark mode. I was able to make a view that works as intended, until I put it into the toolbar and it no longer shows the selected state. Here is my attempt:
struct ToggleButtonView: View {
#State private var isOn = false
var body: some View {
Button(action: {
isOn.toggle()
}, label: {
if isOn {
Image(systemName: "list.bullet.below.rectangle")
.accentColor(Color(.systemBackground))
.background(RoundedRectangle(cornerRadius: 5.0)
.fill(Color.accentColor)
.frame(width: 26, height: 26))
} else {
Image(systemName: "list.bullet.below.rectangle")
}
})
.accentColor(.red)
}
}
And here is how I am actually placing the button into the toolbar:
struct TestView: View {
var body: some View {
NavigationView {
ScrollView {
ForEach(0..<5) { number in
Text("Number \(number)")
}
}
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
ToggleButtonView()
Button(action: {}, label: {
Image(systemName: "plus")
})
}
}
.navigationTitle("Plz halp")
}
.accentColor(.red)
}
}
Here are screenshots from the calendar app. Notice the toolbar item to the left of the search icon.
you could try this:
.toolbar {
// placement as you see fit
ToolbarItem(placement: .navigationBarTrailing) {
ToggleButtonView()
}
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {}, label: {
Image(systemName: "plus")
})
}
}
This looks like an issue with how SwiftUI handles ToolbarItems before iOS 15. According to Asperi's answer to a similar question, "...all standards types (button, image, text, etc) are intercepted by ToolbarItem and converted into an appropriate internal representation."
Toggle buttons in SwiftUI iOS 15
Interestingly enough, iOS 15 now provides a standard solution to the use-case above using the .button toggle style, as shown in the following code:
struct ContentView: View {
#State private var isOn = false
var body: some View {
Toggle(isOn: $isOn) {
Image(systemName: "list.bullet.below.rectangle")
}
}
}

SwiftUI: Sheet gets dismissed immediately after being presented

I want to have a fullscreen SwiftUI View with a button in the Navigation Bar, which presents a SwiftUI Sheet above.
Unfortunately, the Compiler says: "Currently, only presenting a single sheet is supported.
The next sheet will be presented when the currently presented sheet gets dismissed."
This is my Code:
struct ContentView: View {
var body: some View {
EmptyView().fullScreenCover(isPresented: .constant(true), content: {
FullScreenView.init()
})
}
}
struct FullScreenView: View{
var body: some View {
NavigationView{
MasterView()
}.navigationViewStyle(DoubleColumnNavigationViewStyle())
}
}
struct MasterView: View {
#State private var showingSheet = false
var body: some View {
Form {
Section(header: Text("Header")) {
NavigationLink(destination: UIKitView()) { Text("Hey") }
}
}
.navigationBarItems(trailing:
HStack {
// First Try: Use a Button
Button("Plus"){
showingSheet = true
}.sheet(isPresented: $showingSheet){
AddContentView()
}
// Second Try: Use NavigationLink
NavigationLink(
destination: AddContentView(),
label: {
Image(systemName: "plus.square.fill")
})
})
}
}
The Problem
I want to show the SwiftUI View in Fullscreen, so I use fullScreenCover(...). With this first "Sheet", I cannot present a second sheet, my AddContentView() View. Is there any way how I can fix this? I really want to have this sheet above :(
Thanks for any help!!
Feel free to ask for other code or if there are ambiguities. :)
The error message says that the sheet cannot be displayed at the same time(Do not overlap sheets), so if you want to go to view and again to another view, you have to use a NavigationLink and only at the end .sheet()
.sheet(isPresented: $showingSheet){
AddContentView()
}
or fullScreenCover()
.fullScreenCover(isPresented: $showingSheet){
AddContentView()
}
Edited: Sheet is not overlapped twice in this code.
import SwiftUI
struct ContentView: View {
#State private var showingSheet = false
var body: some View {
NavigationView{
Form {
Section(header: Text("Header")) {
NavigationLink(destination: EmptyView()) { Text("Hey") }
}
}
.navigationBarItems(trailing:
HStack {
// First Try: Use a Button
Button("Plus"){
showingSheet = true
}.sheet(isPresented: $showingSheet){
EmptyView()
}
// Second Try: Use NavigationLink
NavigationLink(
destination: EmptyView(),
label: {
Image(systemName: "plus.square.fill")
})
})
}.navigationViewStyle(DoubleColumnNavigationViewStyle())
}
}
p.s. fullScreenCover() belongs to the sheet

SwiftUI sheet not animating dismissal on macOS Big Sur

I would like the sheet dismissal animated just like the appearance but reversed. I think this is also the standard behavior. You can see it in Xcode for example when you create a new file.
But as you can see it just disappears without animation
Here's my code:
struct ContentView: View {
#State var isAnotherViewPresented: Bool = false
var body: some View {
HStack {
Button(action: {
isAnotherViewPresented.toggle()
}, label: {
Text("Button")
}).sheet(isPresented: $isAnotherViewPresented, content: {
AnotherView()
})
}
.frame(width: 500, height: 300, alignment: .center)
}
}
struct AnotherView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Close")
})
}.padding()
}
}
I'm on
Mac mini (M1, 2020)
macOS Big Sur 11.1 (20C69)
Xcode 12.3 (12C33)
But I can reproduce this on a
Mac mini (2018)
macOS Big Sur 11.0.1 (20B29)
Xcode 12.2 (12B45b)
I finally figured out how to do it, in my SwiftUI app it works if I do this while closing the sheet:
isSheetVisible = false
NSApp.mainWindow?.endSheet(NSApp.keyWindow!)
Example:
struct SheetView: View {
#Binding var isSheetVisible: Bool
var body: some View {
Button("Close") {
isSheetVisible = false
NSApp.mainWindow?.endSheet(NSApp.keyWindow!)
}
}
}
Disclaimer: I had/have the same problem where if I try to dismiss a sheet through a binding, it just disappears instead of having an animation. The below solution worked for me but I am unclear as to why its working.
Solution
Apparently the view you "attach" a modal to has an impact on how it transitions from being presented to not. For instance, in your code the sheet is attached to the button view:
Button(action: {
isAnotherViewPresented.toggle()
}, label: {
Text("Button")
// sheet is attached here
}).sheet(isPresented: $isAnotherViewPresented, content: {
AnotherView()
})
When you call presentationMode.wrappedValue.dismiss() in the second view the modal jolts and disappears instead of sliding away. However, if you attach the sheet to the outer HStack view, then it works and it slides away as expected:
var body: some View {
HStack {
Button(action: {
isAnotherViewPresented.toggle()
}, label: {
Text("Button")
})
}
.frame(width: 500, height: 300, alignment: .center)
.sheet(isPresented: $isAnotherViewPresented, content: {
AnotherView()
})
// sheet is now here
}
For me as long as the sheet wasn't attached to the button the animation worked. I don't know why this works but it did for me and hopefully it will for you as well.
I like this mix:
assuming you have:
#Environment(\.presentationMode) var presentationMode
then:
presentationMode.wrappedValue.dismiss() // this updates the binding from .sheet(isPresented: ...) to false
NSApp.mainWindow?.endSheet(NSApp.keyWindow!) // this runs the animation

SwiftUI, navigationBarItems not working after programatically dismissing modal

Using SwiftUI,
I show a modal and then programmatically dismiss it.
The issue is, the second time, that navigationBarItems are not responsive and I can't press on them anymore.
If I dismiss the modal using the swipe gesture then it works fine but not when I call self.presentationMode.wrappedValue.dismiss()
HomeView.swift
struct HomeView: View {
#Environment(\.managedObjectContext) var moc;
#State private var isSureToResetCounter = false;
var body: some View {
func handleLeadingBtnPress() -> Void {
self.isSModalPresented = true;
}
return
NavigationView {
VStack(alignment: .center, spacing: 20.0) {
Spacer(minLength: 10)
// ...
}
.padding(20)
.background(Color.secondary)
.edgesIgnoringSafeArea(.all)
.navigationBarTitle(Text(""), displayMode: .inline)
.navigationBarItems(
leading:
Button(action: handleLeadingBtnPress) {
Text("Save")
.foregroundColor(.textPrimary)
}.sheet(isPresented: self.$isSModalPresented) {
SaveModalView()
.environment(\.managedObjectContext, self.moc)
.modifier(SystemServices())
}, trailing:
NavigationLink(destination: ActivityListView()) {
Text("History")
.foregroundColor(.textPrimary)
})
.gesture(tap)
}
}
}
SaveModalView.swift
struct SaveModalView: View {
#Environment(\.presentationMode) private var presentationMode
#Environment(\.managedObjectContext) var moc;
//...
func handleSaveActivity() -> Void {
let newActivity = Activity(context: self.moc);
do {
try moc.save();
} catch {
print("coulnd save activity")
}
AppStoreReviewManager.requestReviewIfAppropriate();
self.presentationMode.wrappedValue.dismiss()
}
//...
What am I doing wrong?
EDIT: The bug only seems to occur on the simulator.
This is a known bug from SwiftUI on certain Xcode versions. I suggest you try latest version of Xcode and see if things work out.

How to resolve "Use of unresolved identifier 'PresentationLink'" error in swiftUI? [duplicate]

I am attempting to dismiss a modal view presented via a .sheet in SwiftUI - called by a Button which is within a NavigationViews navigationBarItems, as per below:
struct ModalView : View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
Button(action: {
self.presentationMode.value.dismiss()
}, label: { Text("Save")})
}
}
struct ContentView : View {
#State var showModal: Bool = false
var body: some View {
NavigationView {
Text("test")
.navigationBarTitle(Text("Navigation Title Text"))
.navigationBarItems(trailing:
Button(action: {
self.showModal = true
}, label: { Text("Add") })
.sheet(isPresented: $showModal, content: { ModalView() })
)
}
}
}
The modal does not dismiss when the Save button is tapped, it just remains on screen. The only way to get rid of it is swiping down on the modal.
Printing the value of self.presentationMode.value always shows false so it seems to think that it hasn't been presented.
This only happens when it is presented from the NavigationView. Take that out and it works fine.
Am I missing something here, or is this a beta issue?
You need to move the .sheet outside the Button.
NavigationView {
Text("test")
.navigationBarTitle(Text("Navigation Title Text"))
.navigationBarItems(trailing:
Button("Add") {
self.showModal = true
}
)
.sheet(isPresented: $showModal, content: { ModalView() })
}
You can even move it outside the NavigationView closure.
NavigationView {
Text("test")
.navigationBarTitle(Text("Navigation Title Text"))
.navigationBarItems(trailing:
Button("Add") { self.showModal = true }
)
}
.sheet(isPresented: $showModal, content: { ModalView() })
Notice you can also simplify the Button call if you have a simple text button.
The solution is not readily apparent in the documentation and most tutorials opt for simple solutions. But I really wanted a button in the NavigationBar of the sheet that would dismiss the sheet. Here is the solution in six steps:
Set the DetailView to not show.
Add a button to set the DetailView to show.
Call the .sheet(isPresented modifier to display the sheet.
Wrap the view that will appear in the sheet in a NavigationView because we want to display a .navigationBarItem button.
PresentationMode is required to dismiss the sheet view.
Add a button to the NavBar and call the dismiss method.
import SwiftUI
struct ContentView: View {
// 1
#State private var showingDetail = false
var body: some View {
VStack {
Text("Hello, world!")
.padding()
Button("Show Detail") {
showingDetail = true // 2
}
// 3
.sheet(isPresented: $showingDetail) {
// 4
NavigationView {
DetailView()
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct DetailView: View {
// 5
#Environment(\.presentationMode) var presentationMode
var body: some View {
Text("Detail View!")
// 6
.navigationBarItems(leading: Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
Image(systemName: "x.circle")
.font(.headline)
.foregroundColor(.accentColor)
})
}
}