Why is SwiftUI picker in form repositioning after navigation? - ios

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

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.

WatchOS and iOS new .accessoryInline Widget does not display everything

I'm trying to made an WatchOS extension with the new type .accessoryInlineBut I don"t understand why I cannot have more than one stack in the main horizontal stack.
See the invisible stack in the following commented code and in the WatchOS simulator
struct WidgetInlineView : View {
var entry: BurnoutTimelineEntry
var body: some View {
HStack(spacing: 5) {
HStack {
Image("widgetWork")
Text(entry.exchange.todayWork.durationString)
}
HStack {
Image("widgetPause")
Text(entry.exchange.todayPause.durationString)
}
}
}
}
struct WidgetInline: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(kind: "MyKind", provider: BurnoutTimelineProvider()) { entry in
WidgetInlineView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
.supportedFamilies([.accessoryInline])
}
}
struct WidgetInline_Previews: PreviewProvider {
static var previews: some View {
WidgetInlineView(entry: BurnoutTimelineEntry(date: Date()))
.previewContext(WidgetPreviewContext(family: .accessoryInline))
}
}
The inline widgets only display the first text item and the first image. Everything else is ignored and they ignore any styling, too. And to top it off, it's very picky about the image size (I have not found the exact limits yet): if your image is too large it won't get rendered, even if you marked it as resizable.
I come to the same conclusion as answered by #DarkDust
Regarding text, my work around is to use text concatenation. something like this...
Text("Hello") + Text("\(name) ") + Text(Image(systemName: "exclamationmark.circle.fill"))

SwiftUI PageTabView in iOS 14.2 performance issues lagging

I'm using TabView with PageTabViewStyle, and each child view comprises a list view with a large data set.
Only on iOS 14.2, the page transitions seem to be very laggy.
However, page transitions are not delayed in list views with a small amount of data.
It's my guess that the performance of TabView comprises list would be independent of the amount of data, because of the list row display is lazy.
So, I believe it is bugs or default view style changes.
I look forward to your help to solve this problem. Thank you
#available(iOS 14.0, *)
struct ContentView: View {
#State var showHeart: Bool = false
var body: some View {
TabView{
self.contents
self.contents
}
.tabViewStyle(PageTabViewStyle())
}
var contents: some View{
List(0..<1000){_ in
Text("HELLO WORLD HELLOWORLD")
}
}
}
I have been playing with this and just a discovery - when you use TabView() it is laggy, but if you add a binding passed as TabView(selection: $selection) and just don't do anything with the selection binding it somehow doesn't lag anymore? Hacky, but a solution.
Try using lazy loading. Something like this: https://www.hackingwithswift.com/quick-start/swiftui/how-to-lazy-load-views-using-lazyvstack-and-lazyhstack
As you can see in the video: https://streamable.com/7sls0w
the List is not properly optimized. Create your own list, using LazyVStack. Much better performance, much smoother transition to it.
I don't think you understood the idea. Code to solve the issue:
#State var showHeart: Bool = false
var body: some View {
TabView {
contents
contentsSecond
}
.tabViewStyle(PageTabViewStyle())
}
var contents: some View {
List(0..<10000) { _ in
Text("HELLO WORLD HELLOWORLD")
}
}
var contentsSecond: some View {
return ScrollView {
Divider()
LazyVStack {
ForEach(1...1000, id: \.self) { value in
Text("Luke, I am your father \(value)")
.padding(.all, 5)
Divider()
}
}
}
}
I updated to iOS 14.2 yesterday and have the same issue (using Scrollview instead of List btw). I believe this is a bug.
One possible Workaround is to fallback to UIKits PageViewController by using UIViewControllerRepresentable as shown in the accepted answer here:
How can I implement PageView in SwiftUI?
This has solved the lagginess problem.

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.

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