Unable to reshow a sheet if triggered from navigationBar - ios

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

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.

SwiftUI NavigationLink Text click not working [duplicate]

I was working on an application with login and after login there are categories listed. And under each category there are some items listed horizontally. The thing is after login, main page appears and everything is listed great. When you click on an item it goes to detailed screen but when you try to go back it just crashes. I found this flow Why does my SwiftUI app crash when navigating backwards after placing a `NavigationLink` inside of a `navigationBarItems` in a `NavigationView`? but i could not solve my problem. Since my project become complicated, I just wanted to practice navigation in swiftui and I created a new project. By the way I downloaded the latest xcode version 11.3. I wrote a simple code as follows:
NavigationView{
NavigationLink(destination: Test()) {
Text("Show Detail View")
}
.navigationBarTitle("title1")
And Test() view is as follows:
import SwiftUI
struct Test: View {
var body: some View {
Text("Hello, World!")
}
}
struct Test_Previews: PreviewProvider {
static var previews: some View {
Test()
}
}
As you can see it is really simple. I also tried similar examples on the internet but it does not work the way it suppose to work. When I run the project, I click the navigation link and it navigates to Test() view. Then I click back button and it navigates to the main page. However, when I click the navigation link second time, nothing happens. Navigation link works only once and after that nothing happens. It does not navigate, it des not throw any error. I am new to swiftui and everything is great but the navigation. I tried many examples and suggested solutions on the internet, but nothing seems to fix my issues.
[UPDATE] Nov 5, 2020 - pawello2222 says that this issue has been fixed in Xcode 12.1.
[UPDATE] Jun 14, 2020 - Quang Hà says that this issue has come back in Xcode 11.5.
[UPDATE] Feb 12, 2020 - I checked for this issue in Xcode 11.4 beta and found that this issue has been resolved.
I was getting the same issue in my project too, when I was testing it in Xcode's simulator. However, when I launched the app on a real device (iPhone X with iOS 13.3), NavigationLink was working totally fine. So, it really does seem like Xcode's bug.
Simulator 11.4: This issue has been fixed
You need to reset the default isActive value in the second view.
It works on devices and emulators.
struct NavigationViewDemo: View {
#State var isActive = false
var body: some View {
NavigationView {
VStack {
Text("View1")
NavigationLink(
destination: NavigationViewDemo_View2(isActive: $isActive),
isActive: $isActive,
label: { Button(action: { self.isActive = true }, label: { Text("click") }) })
}
}
}
}
struct NavigationViewDemo_View2: View {
#Binding var isActive: Bool
var body: some View {
Text("View2")
.navigationBarItems(leading: Button(action: { self.isActive = false }, label: { Text("Back") }))
}
}
Presumably this will be resolved when Apple fixes the related bug that prevents 13.3 from being selectable as a deployment target.
I'm experiencing the same issue as everyone else. This issue is present in simulator and preview running 13.2, but is fixed when deploying to my own device running 13.3.
As #Александр Грабовский said its seems like a Xcode 11.3 bug, I am encountering the same problem, you must downgrade or use some workaround like custom back button as below
struct ContentView: View {
#State private var pushed: Bool = false
var body: some View {
NavigationView {
VStack {
Button("Show Detail View") {
self.pushed.toggle()
}
NavigationLink(destination: Test(pushed: $pushed), isActive: $pushed) { EmptyView() }
}.navigationBarTitle("title1")
}
}
}
struct Test: View {
#Binding var pushed: Bool
var body: some View {
Text("Hello, World!")
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: BackButton(label: "Back") {
self.pushed = false
})
}
}
struct BackButton: View {
let label: String
let closure: () -> ()
var body: some View {
Button(action: { self.closure() }) {
HStack {
Image(systemName: "chevron.left")
Text(label)
}
}
}
}
For anyone who's having the same symptom with other versions of iOS than the buggy beta identified by other answers, there's another reason you might be seeing this behaviour.
If your NavigationLink is nested inside another NavigationLink, the inner NavigationLink will only work once, unless you add isDetailLink(false) to the outer link.

How do I reset a selection tag when pushing a NavigationLink programmatically?

I want to push a view programmatically instead of relying on the interface that NavigationLink provides (e.g. I want to use a button with no chevron). The correct way is to use NavigationLink with tag and selection, and an EmptyView.
When I attempt to use the following code to push a view, it works to push the view the first time:
struct PushExample: View {
#State private var pushedView: Int? = nil
var body: some View {
NavigationView {
VStack {
Form {
Button(action: { self.pushedView = 1 }) { Text("Push view") }
}
NavigationLink(destination: Text("Detail view"), tag: 1, selection: $pushedView) { EmptyView() }
}
}
}
}
However, if I tap the back button on the view, and try hitting the button again, it no longer pushes the view. This is because the value pushedView is being set to 1 again, but it is already at 1. Nothing is resetting it back to nil upon pop of the Detail view.
How do I get subsequent taps of the button to push the view again?
TL;DR: There is no need to reset the state variable, as SwiftUI will automatically handle it for you. If it's not, it may be a bug with the simulator.
This was a simulator bug on Xcode 11.3!
The way to check if it's a simulator bug is to run an even simpler example:
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink("Push", destination: Text("Detail"))
}
}
}
On the Xcode 11.3 iPhone 11 Pro Max, this would only work the first time you tap the link.
This worked fine on both a 13.2 and a 13.3 device.
Therefore, when running into odd SwiftUI issues, test on device rather than the simulator.
Update: Restarting the computer didn't fix it either. Thus while SwiftUI is still new, may be better off to use a real device for testing rather than the simulator.

Why is SwiftUI picker in form repositioning after navigation?

After clicking the picker it navigates to the select view. The item list is rendered too far from the top, but snaps up after the animation is finished. Why is this happening?
Demo: https://gfycat.com/idioticdizzyazurevase
I already created a minimal example to rule out navigation bar titles and buttons, form sections and other details:
import SwiftUI
struct NewProjectView: View {
#State var name = ""
var body: some View {
NavigationView {
Form {
Picker("Client", selection: $name) {
Text("Client 1")
Text("Client 2")
}
}
}
}
}
struct NewProjectView_Previews: PreviewProvider {
static var previews: some View {
NewProjectView()
}
}
This happens in preview mode, simulator and on device (Xcode 11.2, iOS 13.2 in simulator, 13.3 beta 1 on device).
The obviously buggy behavior can be worked around when forcing the navigation view style to stacked:
NavigationView {
…
}.navigationViewStyle(StackNavigationViewStyle())
This is a solution to my problem, but I won‘t mark this as accepted answer (yet).
It seems to be a bug, even if it may be triggered by special circumstances.
My solution won‘t work if you need another navigation view style.
Additionally, it won‘t fix the horizontal repositioning mentioned by DogCoffee in the comments.
In my opinion, it has something to do with the navigation bar. In default (no mention of .navigationBarTitle extension), the navigation display mode is set to .automatic, this should be amend to .inline. I came across another post similar to this and use their solution to combine with yours, by using .navigationBarTitle("", displayMode: .inline) should help.
import SwiftUI
struct NewProjectView: View {
#State var name = ""
var body: some View {
NavigationView {
Form {
Picker("Client", selection: $name) {
Text("Client 1")
Text("Client 2")
}
}
.navigationBarTitle("", displayMode: .inline)
}
}
}
struct NewProjectView_Previews: PreviewProvider {
static var previews: some View {
NewProjectView()
}
}
Until this bug is resolved another way to work around this issue while retaining the DoubleColumnNavigationViewStyle for iPads would be to conditionally set that style:
let navView = NavigationView {
…
}
if UIDevice.current.userInterfaceIdiom == .pad {
return AnyView(navView.navigationViewStyle(DoubleColumnNavigationViewStyle()))
} else {
return AnyView(navView.navigationViewStyle(StackNavigationViewStyle()))
}
Thanks for this thread everyone! Really helped me understand things more and get a hold of one of my problems. To share with others, I was having this problem to but I was also having this problem when I set a section to appear in a if/else statement set on a section with a toggle. When the toggle was activated it would shift the section header horizontally a few pixels.
The following is how I fixed it
Section(header: Text("Subject Identified").listRowInsets(EdgeInsets()).padding(.leading)) {
Picker(selection: $subIndex, label: Text("Test")) {
ForEach(0 ..< subIdentified.count) {
Text(self.subIdentified[$0]).tag($0)
}
}
.labelsHidden()
.pickerStyle(SegmentedPickerStyle())
I'm still having horizontal shift on my picker selection view and not sure how to fix. I created another thread to received input. Thanks again! SwiftUI Shift Picker Text Horizontal

NavigationLink Works Only for Once

I was working on an application with login and after login there are categories listed. And under each category there are some items listed horizontally. The thing is after login, main page appears and everything is listed great. When you click on an item it goes to detailed screen but when you try to go back it just crashes. I found this flow Why does my SwiftUI app crash when navigating backwards after placing a `NavigationLink` inside of a `navigationBarItems` in a `NavigationView`? but i could not solve my problem. Since my project become complicated, I just wanted to practice navigation in swiftui and I created a new project. By the way I downloaded the latest xcode version 11.3. I wrote a simple code as follows:
NavigationView{
NavigationLink(destination: Test()) {
Text("Show Detail View")
}
.navigationBarTitle("title1")
And Test() view is as follows:
import SwiftUI
struct Test: View {
var body: some View {
Text("Hello, World!")
}
}
struct Test_Previews: PreviewProvider {
static var previews: some View {
Test()
}
}
As you can see it is really simple. I also tried similar examples on the internet but it does not work the way it suppose to work. When I run the project, I click the navigation link and it navigates to Test() view. Then I click back button and it navigates to the main page. However, when I click the navigation link second time, nothing happens. Navigation link works only once and after that nothing happens. It does not navigate, it des not throw any error. I am new to swiftui and everything is great but the navigation. I tried many examples and suggested solutions on the internet, but nothing seems to fix my issues.
[UPDATE] Nov 5, 2020 - pawello2222 says that this issue has been fixed in Xcode 12.1.
[UPDATE] Jun 14, 2020 - Quang Hà says that this issue has come back in Xcode 11.5.
[UPDATE] Feb 12, 2020 - I checked for this issue in Xcode 11.4 beta and found that this issue has been resolved.
I was getting the same issue in my project too, when I was testing it in Xcode's simulator. However, when I launched the app on a real device (iPhone X with iOS 13.3), NavigationLink was working totally fine. So, it really does seem like Xcode's bug.
Simulator 11.4: This issue has been fixed
You need to reset the default isActive value in the second view.
It works on devices and emulators.
struct NavigationViewDemo: View {
#State var isActive = false
var body: some View {
NavigationView {
VStack {
Text("View1")
NavigationLink(
destination: NavigationViewDemo_View2(isActive: $isActive),
isActive: $isActive,
label: { Button(action: { self.isActive = true }, label: { Text("click") }) })
}
}
}
}
struct NavigationViewDemo_View2: View {
#Binding var isActive: Bool
var body: some View {
Text("View2")
.navigationBarItems(leading: Button(action: { self.isActive = false }, label: { Text("Back") }))
}
}
Presumably this will be resolved when Apple fixes the related bug that prevents 13.3 from being selectable as a deployment target.
I'm experiencing the same issue as everyone else. This issue is present in simulator and preview running 13.2, but is fixed when deploying to my own device running 13.3.
As #Александр Грабовский said its seems like a Xcode 11.3 bug, I am encountering the same problem, you must downgrade or use some workaround like custom back button as below
struct ContentView: View {
#State private var pushed: Bool = false
var body: some View {
NavigationView {
VStack {
Button("Show Detail View") {
self.pushed.toggle()
}
NavigationLink(destination: Test(pushed: $pushed), isActive: $pushed) { EmptyView() }
}.navigationBarTitle("title1")
}
}
}
struct Test: View {
#Binding var pushed: Bool
var body: some View {
Text("Hello, World!")
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: BackButton(label: "Back") {
self.pushed = false
})
}
}
struct BackButton: View {
let label: String
let closure: () -> ()
var body: some View {
Button(action: { self.closure() }) {
HStack {
Image(systemName: "chevron.left")
Text(label)
}
}
}
}
For anyone who's having the same symptom with other versions of iOS than the buggy beta identified by other answers, there's another reason you might be seeing this behaviour.
If your NavigationLink is nested inside another NavigationLink, the inner NavigationLink will only work once, unless you add isDetailLink(false) to the outer link.

Resources