SwiftUI bug with navigation view rotation? - ios

While testing my app to make sure it behaves properly under screen rotations, I discovered that navigation links do not work after a certain sequence of rotations.
The following is a MWE:
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink("link") {
Text("hi")
}
}
}
// rotate -> back -> link -> rotate -> back -> link
}
On a iPhone 13 Pro Max (iOS 15.2 (19C51)) simulator, the following leads to an error:
Run the app on the portrait mode
Rotate the app to the landscape mode
Touch the back button (in the navigation bar)
Touch the navigation link link
Rotate the app to the portrait mode
Touch the back button
Now touching the navigation link link does not work!
Also, the debug console prints:
Unbalanced calls to begin/end appearance transitions for <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentGS1_VVS_22_VariadicView_Children7ElementVS_24NavigationColumnModifier_GVS_18StyleContextWriterVS_19SidebarStyleContext___: 0x152f24860>.
Is this a bug in SwiftUI?
And is there a way to work around this issue?
I'm on macOS Monterey (12.2 (21D49)) + Xcode 13.2.1 (13C100).

Changing ColumnNavigationViewStyle to StackNavigationViewStyle will solve your problem, the sequence you mentioned is most probably a bug, hopefully apple will solve it soon.
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink("link") {
Text("hi")
}
}
.navigationViewStyle(.stack) //Style
}
}

Without having to give up double column layout in iPadOS, I used the following conditional view builder to use different navigation style based on the horizontal size class.
(Although conditional view builders often do not play well with animations, it seems ok to use it here.)
extension View {
#ViewBuilder func `if`<TrueContent: View>(
_ condition: Bool,
then trueContent: (Self) -> TrueContent
) -> some View {
if condition {
trueContent(self)
} else {
self
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink("link") {
Text("hi")
}
}
.if(UIDevice.current.userInterfaceIdiom == .phone) {
$0.navigationViewStyle(.stack)
}
}
}
P.S. The issue can be reproduced in iPadOS as well when the scene is resized appropriately after each rotation, but I think this is less likely to happen without intention to reproduce.

The max uses columns in landscape and you are missing the second column View. Try this
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink("link") {
Text("hi")
}
Text("Select a link")
}
}
}

Related

SwiftUI NavigationSplitView link is disabled in compact mode

When implementing my app, I want to embed a NavigationStack inside NavigationSplitView, as the documentation says. However, under compact mode like on iPhone, I encountered a bug.
To be specific, here's the example code:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationSplitView {
List {
NavigationLink("Link") {
NavigationStack {
Text("Destination")
}
}
}
} detail: {
}
}
}
When running the code above, as I tap the "Link", it navigate to "Destination" as expected. However, if I navigate back and tap again, this link won't navigate.
Is there any workaround? Thanks for your solution!

How to display a detailed view on startup for a NavigationView (SwiftUI)

This tutorial uses a NavigationView to display a List of elements which can be clicked, leading to a detailed view, LandmarkDetail. On an iPhone, the UI uses the StackNavigationViewStyle() which looks and works fine, but on an iPad the NavigationView is displayed on the side. I want to be able to fill up the remaining space with a detailed view (see below).
I have tried to display the detailed view, LandmarkDetail, beside the NavigationView like so:
var body: some View {
NavigationView {
List {
Toggle(isOn: $showFavoritesOnly) {
Text("Favorites only")
}
ForEach(filteredLandmarks) { landmark in
NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
LandmarkRow(landmark: landmark)
}
}
}
.navigationTitle("Landmarks")
LandmarkDetail(landmark: ModelData().landmarks[0])
}
}
This approach works fine for the iPad, but produces unexpected results (overlapping tiles, etc.) on the iPhone (see below). Is there a better way for achieving my desired results?
Thanks for any help and I apologise about potential obvious mistakes, etc. This is my first time using SwiftUI and there are noticeably less resources available than for Android development.
Creating a modifier which programmatically assigned the NavigationView style depending on the target device fixed this issue.
extension View
{
func navigationStyleModification() -> some View
{
if UIDevice.current.userInterfaceIdiom == .phone
{
return AnyView(self.navigationViewStyle(StackNavigationViewStyle()))
} else
{
return AnyView(self.navigationViewStyle(DefaultNavigationViewStyle()))
}
}
}
The modifier can be called via:
NavigationView
{
...
}.navigationStyleModification()
I hope this helps someone who has a similar issue.

SwiftUI Navigation Bar Title

I'm making my very first steps into SwiftUI following HackingWithSwift course. I have a problem trying to implement navigation bar in my app. For some reason the title does not show up.
Here's the code I'm using:
struct ContentView: View {
var body: some View {
NavigationView {
Form {
...
}
}
.navigationBarTitle(Text("WeSplit"))
}
}
Once running it in simulator or on my target device I see free space for the navigation bar title but no text there.
I've also tried typing .navigationBarTitle("WeSplit") with no Text() in there. Result's still the same.
Do you have any ideas on how to fix it? Thank you in advance!
I'm running Xcode Version 12.3.
It should be inside NavigationView, like
struct ContentView: View {
var body: some View {
NavigationView {
Form {
...
}
.navigationBarTitle(Text("WeSplit")) // << here !!
}
}
}

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

Resources