SwiftUI Changing State does not Dismiss Modal View - ios

SwiftUI Changing State does not Dismiss Modal View
I have a basic master/detail app with SwiftUI life cycle. On the detail page, I
have a button to toggle an #State to present a modal for editing the detail item. I am
using the fullScreenCover modal. The #State variable in the Detail view is passed as
an #Binding to the Edit view.
The Edit view has a "Done" button to dismiss itself. I have tried coding with both the binding
and presentationMode methods.
This all works except that on RARE occasions tapping the "Done" button does not dismiss
the Edit view. I then have to hard close the app and restart. The edits which were made
are still saved as expected. There is simply no way to move from that screen.
The Detail view calls the Edit view like this:
.fullScreenCover(isPresented: $showEditModalView) {//showEditView
InvItemEditView(showEditModalView: $showEditModalView,
invItem: self.invItem,
inputCategory1: self.inputCategory1).environment(\.managedObjectContext, managedObjectContext)
}//full screen
The Done button is coded as this:
Button(action: {
self.saveEditedRecord()
print("Before change - Done button showEditModalView is \(self.showEditModalView)")
self.showEditModalView = false
print("I got passed showEditModalView = false")
self.presentationMode.wrappedValue.dismiss()
print("I got passed presentationMode dismiss")
print("After change - Done button showEditModalView is \(self.showEditModalView)")
}) {
Text("Done")
.font(.system(size: 20))
}//trailing button
.disabled(self.localDisableSaveButton)
.disabled(self.dataStore.pubDisableEditButton)
The saveEditedRecord does just that - it issues the Core Data saveContext.
I can't replicate the error on demand. It just happens occasionally. I was curious to
see that the Button action always executes all lines of code - I had expected it to
terminate once the variable controlling the view presentation changed. I searched for
others who may have had issues with fullScreenCover but found nothing relavant. I added the
print statements to see if there was an issue setting the #State variable. Here is an
example console output:
CoreData: debug: CoreData+CloudKit: -[NSCloudKitMirroringDelegate managedObjectContextSaved:](2092): <NSCloudKitMirroringDelegate: 0x2837a8680>: Observed context save: <NSPersistentStoreCoordinator: 0x2827ab4f0> - <NSManagedObjectContext: 0x2837a9790>
in invItemEditView saveEditedRecord, in do after context.save
Before change - Done button showEditModalView is true
I got passed showEditModalView = false
I got passed presentationMode dismiss
After change - Done button showEditModalView is false
When it fails, the first console statement after the Core Data item is:
Before change - Done button showEditModalView is false
Any guidance would be appreciated. Xcode 12.4 iOS 14.4

I guess the problem is that you missed adding the presentation environment in the SwiftUI Modal View.
Here is a complete example in SwiftUI:
struct ContentView: View {
#State private var showEditModalView = false
#Environment(\.presentationMode) var presentationMode
var body: some View {
Button(action: {
self.showEditModalView = true
}) {
Text("Show modal")
}.fullScreenCover(isPresented: $showEditModalView) {
InvItemEditView()
}
}
}
struct InvItemEditView: View {
#Environment(\.presentationMode) private var presentationMode
var body: some View {
Group {
Text("Modal view")
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Done")
.font(.system(size: 20))
}
}
}
}
I would also suggest reading this question: SwiftUI dismiss modal

Related

Bug with NavigationLink and #Binding properties causing unintended Interactions in the app

I've encountered a bug in SwiftUI that could cause unintended interaction with the app without the user's knowledge.
Description
The problem seems to be related to using #Binding properties on the View structs when used in conjunction with NavigationStack and NavigationLink. If you use NavigationView with NavigationLink to display a DetailView that accepts a $Binding parameter, and that parameter is used in some sort of condition in the DetailView, it will result in unexpected behavior.
To clearly show the problem, I'm using a DetailView where the "Blue" or "Red" view is shown depending on the #Binding property. Each of those views has a .onTapGesture() modifier that prints some text when tapped. The problem is that if the Red view is shown, it detects and triggers the action on the Blue view, which could lead to unintended changes in many apps without the user's knowledge.
Replication of the problem
You can easily copy and paste this code into your own file to replicate the bug. To see the unexpected behavior, run the code below and follow these steps on the simulator:
Tap on the DetailView in the NavigationLink.
Tap the blue color area and the console will print "Blue Tapped".
Tap the "RED BUTTON" to switch to the other view.
Tap the red color area and the console will print "Red Tapped".
Now try to tap a blank space below the red area (where the blue area was previously located). The console will print "BLUE tapped" - this is the problem, it seems that the blue view is still active there.
I tested this behavior on: XCode 14.1, iPhone 13 Pro 16.1 iOS Simulator, and on a real iPhone with iOS 16. The result was always the same.
import SwiftUI
struct ContentView: View {
var body: some View {
NavView()
}
}
struct NavView: View {
#State private var colourShowed: Int = 1
var body: some View {
// If the DetailView() was shown directly, (without the NavigationLink and NavigationStack) there would be no such a bug.
// DetailView(colourShowed: $colourShowed)
// The bug is obvious when using the NavigationStack() with the NavigationLink()
NavigationStack {
Form {
NavigationLink(destination: { DetailView(colourShowed: $colourShowed) },
label: { Text("Detail View") })
}
}
}
}
struct DetailView: View {
// It seems like the problem is related to this #Binding property when used in conjunction
// with the NavigationLink in "NavView" View above.
#Binding var colourShowed: Int
var body: some View {
ScrollView {
VStack(spacing: 20){
HStack {
Button("BLUE BUTTON", action: {colourShowed = 1})
Spacer()
Button("RED BUTTON", action: {colourShowed = 2})
}
if colourShowed == 1 {
Color.blue
.frame(height: 500)
// the onTapeGesture() is stillActive here even when the "colourShowed" property is set to '2' so this
// view should therefore be deinitialized.
.onTapGesture {
print("BLUE tapped")
}
// The onAppear() doesn't execute when switching from the Red view to the Blue view.
// It seems like the "Blue" View does not deinitialize itself after being previously shown.
.onAppear(perform: {print("Blue appeared")})
}
else {
Color.red
.frame(height: 100)
.onTapGesture {
print("RED tapped")
}
.onAppear(perform: {print("Red appeared")})
}
}
}
}
}
Is there any solution to prevent this?
This is a common problem encountered by those new to Swift and value semantics, you can fix it by using something called a "capture list" like this:
NavigationLink(destination: { [colourShowed] in
It occurred because DetailView wasn't re-init with the new value of colourShowed when it changed. Nothing in body was using it so SwiftUI's dependency tracking didn't think body had to be recomputed. But since you rely on DetailView being init with a new value you have to add it to the capture list to force body to be recomputed and init a new DetailView.
Here are other questions about the same problem with .sheet and .task.

Unable to reshow a sheet if triggered from navigationBar

please help!
I have a simple test code below, to display a sheet when a button is pressed. The issue is when I place the button inside a .toolbar and for the first press the sheet is shown as expected. However, if I were to dismiss the sheet and press the button again, nothing happens and no error is printed out in the console.
If i were to place the button at .bottomBar or withing VStack everything works as expected, I can show and dismiss the sheet multiple times.
I am using Xcode Version 13.2.1 (13C100) and running on iPhone 13 (iOS 15.2)simulator. Haven't tried running on a real device. Video Example
I've been trying to understand what is happening for 2 days and still have no idea what is the cause.
struct ContentView: View {
#State private var showingSheet = false
var body: some View {
NavigationView {
VStack {
Button("Show Sheet") {
showingSheet.toggle()
}
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Show once?!") {
showingSheet.toggle()
}
}
}
.sheet(isPresented: $showingSheet) {
Text("Drag to dismiss")
}
}
}
}
Solution:
Press and hold button and then drag it down just a little bit (other directions are no no) till the button is registered as clicked and .sheet will be presented as intended. Ridiculous :D

SwiftUI View with StateObject, a Form element and a ForEach breaks bindings when trying to use NavigationLink Inside the form

OK something weird is going on and I want to see if anyone else have this issue.
Consider the following ViewModel class with one published property to use from a View:
final class ViewModel: ObservableObject {
#Published var isActive = false
}
When using this view:
struct MainView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
NavigationView {
Form {
NavigationLink (
destination: ChildView(isActive: $viewModel.isActive),
isActive: $viewModel.isActive,
label: { Text("Go to child view") }
)
// Adding this ForEach causes the NavigationLink above to have a broken binding
ForEach(1..<4) {
Text("\($0)")
}
}
.navigationBarTitle("Test")
}
}
}
And this SubView:
struct ChildView: View {
#Binding var isActive: Bool
var body: some View {
Button("Go back", action: { isActive = false })
}
}
The issue
The expected result is when tapping on "Go to child view", navigating to the subview and tapping "Go back" to return to the main view - it should navigate back using the isActive binding.
But actually, the button "Go Back" Doesn't work.
BUT If I remove the ForEach element from the form in the main view, the button works again. And it looks like the ForEach breaks everything.
Additional findings:
Changing Form to VStack fixes the issue
Using a struct and a #State also fixes the issue
Extracting the ForEach to a subview fixes the issue but as soon as I pass the viewmodel or part of it to the subview as a binding or as a ObservedObject - it still broken
Can anything advise if there is a logical issue with the code or is it a SwiftUI bug?
Any suggestions for a workaround?
Video of the expected behavior:
https://i.stack.imgur.com/BaggK.gif
Apple developer forum discussion: https://developer.apple.com/forums/thread/674127
Update
It looks like the issue has been fixed in the latest iOS 14.5 Beta 2 🎉
The issue has been fixed on iOS 14.5

SwiftUI View Code is seemingly getting called twice. What is the issue here?

I have a view that is being called using .sheet in SwiftUI. However, when this sheet is presented, I'm getting the debug "Tapped" print in console followed by this error:
Warning: Attempt to present <_TtGC7SwiftUIP13$7fff2c9bdf5c22SheetHostingControllerVS_7AnyView_: 0x7f8f1d7400f0> on <_TtGC7SwiftUI19UIHostingControllerVVS_22_VariadicView_Children7Element_: 0x7f8f17e0dae0> which is already presenting (null)
I'm not exactly sure what is causing this error, but from what I understand it's due to the view getting called twice. I'm not sure how the view is being called twice, or even if it is, which is why I'm asking here. Below is the main view that actually houses the NavigationView in which my List is being housed view
struct AllTablesView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(fetchRequest: Table.getAllTables()) var tables: FetchedResults<Table>
#State private var tableTapped: Table = Table()
#State private var isModal = false
var body: some View {
NavigationView {
List {
ForEach(self.tables) { table in
tableCellView(tableNumber: table.number, clean: table.clean, inUse: table.inUse)
.onTapGesture {
self.tableTapped = table
self.isModal = true
print("tapped")
}
}
.onDelete { indexSet in
let deletedTable = self.tables[indexSet.first!]
self.managedObjectContext.delete(deletedTable)
do {
try self.managedObjectContext.save()
} catch {
print(error)
}
}
.sheet(isPresented: $isModal){
TableStatusView(table: self.tableTapped)
.environment(\.managedObjectContext, self.managedObjectContext)
}
}
.navigationBarTitle("All Tables")
.navigationBarItems(leading: EditButton(), trailing:
HStack {
DeleteAllTablesButton()
AddTableButton()
})
}
}
}
And, if for whatever reason this is the issue, here is the code for my view titled "TableStatusView"
struct TableStatusView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(fetchRequest: Table.getAllTables()) var tables: FetchedResults<Table>
#State var table: Table
var body: some View {
VStack {
Form {
Section {
Text("Table \(table.number)")
}.padding(.all, 10.0)
Section {
Toggle(isOn: $table.inUse) {
Text("In Use")
}.padding(.all, 10.0)
Toggle (isOn: $table.clean) {
Text("Clean")
}.padding(.all, 10.0)
}
}
}
}
}
Is there a reason I'm getting this error, or why my view is being called twice? Is it actually that it's being called twice or am I doing something wrong here? I know certain parts of my code could probably be shrunk down or written better but this is just my first dive into SwiftUI, I haven't had much experience with it outside of basic stuff. Thanks!
I never did find a solution to the question, but I did solve the NavigationLink issue and swapped back to using that. I was using ".onTapGesture" to get a tap gesture and then generate a NavigationLink, thinking it was an action. NavigationLink, I've learned, is actually more or less a container to house content, so I replaced the .onTapGesture and TableCellView() function call with the following:
NavigationLink(destination: TableStatusView(table: table)) {
tableCellView(tableNumber: table.number, clean: table.clean, inUse: table.inUse)
}
And fixed the issue for me. I'm still seeing some errors but from some google-fu found out that they are current bugs of SwiftUI. SwiftUI 2.0 may fix some of these issues

SwiftUI: NavigationDestinationLink deprecated

After installing Xcode 11 beta 5 this morning, I noticed that NavigationDestinationLink was deprecated in favor of NavigationLink.
Also, that's what Apple says about it in the release notes:
NavigationDestinationLink and DynamicNavigationDestinationLink are deprecated; their functionality is now included in NavigationLink. (50630794)
The way I use NavigationDestinationLink is to programmatically push a new view into the stack via self.link.presented?.value = true. That functionality doesn't seem to be present in NavigationLink.
Any idea anyone?
I would rather not use NavigationDestinationLink anymore as it's deprecated...
Thank you!
UPDATE:
Actually, the NavigationDestinationLink way does not work anymore so I guess we have no way of pushing programmatically anymore?
UPDATE 2:
NavigationLink(destination: CustomView(), isActive: $isActive) {
return Text("")
}
This works but when you pass isActive to true, any state update will trigger this code and push over and over... Also, if you pass it back to false, it will pop the view.
Not only updates, if you set isActive to true, it will push the view (good) and if we press the back button, it will go back then immediately push again since it's still true.
Playing with onAppear was my hope but it's not called when going back to it...
I'm not sure how we're supposed to use this.
After spending some time with NavigationLink(destination:isActive), I am liking it a lot more than the old NavigationDestinationLink. The old view was a little confusing, while the new approach seems much more elegant. And once I figure out how to push without animations, it would make state restoration at application launch very easy.
There is one problem though, a big and ugly bug. :-(
Pushing a view programatically works fine, and popping it programatically does too. The problem starts when we use the BACK button in the pushed view which behaves oddly every other time. The first time the view is popped, the view pops and pushes again immediately. The second time around it works fine. Then the third time it starts all over again.
I have created a bug report (number here). I recommend you do the same and reference my number too, to help Apple group them together and get more attention to the problem.
I designed a workaround, that basically consists of replacing the default Back button, with our own:
class Model: ObservableObject {
#Published var pushed = false
}
struct ContentView: View {
#EnvironmentObject var model: Model
var body: some View {
NavigationView {
VStack {
Button("Push") {
// view pushed programmatically
self.model.pushed = true
}
NavigationLink(destination: DetailView(), isActive: $model.pushed) { EmptyView() }
}
}
}
}
struct DetailView: View {
#EnvironmentObject var model: Model
var body: some View {
Button("Bring me Back (programatically)") {
// view popped programmatically
self.model.pushed = false
}
// workaround
.navigationBarBackButtonHidden(true) // not needed, but just in case
.navigationBarItems(leading: MyBackButton(label: "Back!") {
self.model.pushed = false
})
}
}
struct MyBackButton: View {
let label: String
let closure: () -> ()
var body: some View {
Button(action: { self.closure() }) {
HStack {
Image(systemName: "chevron.left")
Text(label)
}
}
}
}
To improve the workaround without replacing the back button with a custom one, you can use the code above :
NavigationLink(destination: Test().onAppear(perform: {
self.editPushed = nil
}), tag: 1, selection: self.$editPushed) {
Button(action: {
self.editPushed = 1
}) {
Image(systemName: "plus.app.fill")
.font(.title)
}
}
The onAppear block will erase the selection value preventing to display the detail view twice
You can also use NavigationLink(destination:tag:selection)
NavigationLink(destination: MyModal(), tag: 1, selection: $tag) {
EmptyView()
}
So programmatically you can set tag to 1 in order to push MyModal. This approach has the same behaviour as the one with the Bool binding variable, so when you pop the first time it pushes the view immediately, hopefully they'll fix it in next beta.
The only downside I see with this approach, compared to DynamicNavigationDestinationLink is that you need to provide a View to NavigationLink, even if you don't need one. Hopefully they'll find a cleaner way to allow us to push programmatically.
The solution is to create custom back button for your detail view and pop detail view manually.
.navigationBarItems(leading:
Button(action: {
self.showDetail = false
}) {
Image(systemName: "chevron.left").foregroundColor(.red)
.font(.system(size: 24, weight: .semibold))
Text("Back").foregroundColor(.red)
.font(.system(size: 19))
}
)
The method used in the selected answer has been deprecated again. Here's the solution copied from this answer in this post.
#State private var readyToNavigate : Bool = false
var body: some View {
NavigationStack {
VStack {
Button {
//Code here before changing the bool value
readyToNavigate = true
} label: {
Text("Navigate Button")
}
}
.navigationTitle("Navigation")
.navigationDestination(isPresented: $readyToNavigate) {
MyTargetView()
}
}
}

Resources