SwiftUI artefact after detail with list is presented - swiftui

With the simple navigation-link structure below, I get a strange artefact after the detail view with a list is pushed on screen, as shown here https://youtu.be/LU9uluD5hEw.
If I include a section with a header, the view does not jerk up after the screen is loaded, but remains in its originally presented position. Anybody else experiencing this problem, or know how to fix it?
struct ContentView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination: DetailView()) {
Text("Link")
}
}
.navigationBarTitle("Master")
.listStyle(GroupedListStyle())
}
}
}
struct DetailView: View {
var body: some View {
List {
Text("Detail")
}
.navigationBarTitle("Detail")
.listStyle(GroupedListStyle())
}
}
This is especially annoying for picker details where I cannot add an empty section header as a workaround.

Workaround: It looks like a bug in TitleDisplayMode.large mode, because in the .inline mode such effect is not observed. So, the following might be considered as workaround if it is allowed by app design:
struct DetailView: View {
var body: some View {
List {
Text("Detail")
}
.navigationBarTitle("Detail", displayMode: .inline)
.listStyle(GroupedListStyle())
}
}

Related

Does a LazyVStack remain "lazy" if it's inside a VStack?

Let's consider a list of 100 posts. According to Apple, if I layout them inside a LazyVStack:
the stack view doesn’t create items until it needs to render them
onscreen.
What if I embed that LazyVStack inside a VStack? Does it still load the views "as needed"?
struct MyView: View {
init() {
print("init...")
}
var body: some View {
Text("test")
}
}
struct ContentView: View {
var body: some View {
ScrollView {
VStack {
LazyVStack {
ForEach.init(0..<100) { int in
MyView()
}
}
}
}
}
}
Running the above code, as we scroll we can see more MyViews are init'd (by viewing the print statements in the console), so it seems like a LazyVStack in a VStack does indeed create it's content lazily. We can see the same is true when removing the VStack as well.

SwiftUI extra space at top of list above section header. On testing device only

I have been trying to figure out what is causing the space at the top of the screen in my production app, so I made this test app to see if it is a bug or not. The code works as intended on a simulator but when a testing device runs the code it adds extra space. The space goes away after you start scrolling, and does not comeback until the view reloads. I have tried restarting the device and other devices. I took out .navigationTitle and .navigationBarTitleDisplayMode and it did not fix the problem. So far my best guess is that there is some problem with changing the section header in .onAppear(). Changing it to .task() seems to be a workaround for now.
struct DetailView: View {
#State var item: Item
#State private var headerText = "Header"
var body: some View {
List {
Section(header: Text("\(headerText)")) {
Text("Text")
}
HStack {
Text("Red Text")
}.listRowBackground(Color.red)
// Change to .task instead
}.onAppear {
headerText = "Change Header"
}
}
}
Edit: Here is the code for the list view, it is the default new project setup.
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink(destination: DetailView(item: item), label: {
Text(item.timestamp!, formatter: itemFormatter)
})
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
Text("Select an item")
}
}
In my case this behaviour was caused by .navigationViewStyle(StackNavigationViewStyle()).
Looks like it's a bug with NavigationView in SwiftUI 4 since it wasn't like this before.
If you are on iOS 16 use NavigationStack instead NavigationView. This fixed all my problems and I didn't even need to use the navigationViewStyle anymore.
NavigationView is deprecated in favor of NavigationStack.

Navigation + Tabview + Sheet broken in iOS 15

It looks like Navigation + TabView + Sheet is broken in iOS 15.
When I do this:
ContentView -> DetailView -> Bottom Sheet
When the bottom sheet comes up, the Detail view is automatically popped off the stack:
https://www.youtube.com/watch?v=gguLptAx0l4
I expect the Detail view to stay there even when the bottom sheet appears. Does anyone have any idea on why this happens and how to fix it?
Here is my sample code:
import Combine
import SwiftUI
import RealmSwift
struct ContentView: View {
var body: some View {
NavigationView {
TabView {
TabItemView(num: 1)
.tabItem {
Text("One")
}
TabItemView(num: 2)
.tabItem {
Text("Two")
}
}
}
}
}
struct TabItemView: View {
private let num: Int
init(num: Int) {
self.num = num
}
var body: some View {
NavigationLink(destination: DetailView(text: "Detail View \(num)")) {
Text("Go to Detail View")
}
}
}
struct DetailView: View {
#State private var showingSheet = false
private let text: String
init(text: String) {
self.text = text
}
var body: some View {
Button("Open Sheet") {
showingSheet.toggle()
}.sheet(isPresented: $showingSheet) {
Text("Sheet Text")
}
}
}
This works on iOS 14 btw
UPDATE 1:
Tried #Sebastian's suggestion of putting NavigationView inside of TabView. While this fixed the nav bug, it fundamentally changed the behavior (I don't want to show the tabs in DetailView).
Also tried his suggestion of using Introspect to set navigationController.hidesBottomBarWhenPushed = true on the NavigationLink destination, but that didn't do anything:
struct ContentView: View {
var body: some View {
TabView {
NavigationView {
TabItemView(num: 1)
}.tabItem {
Text("One")
}
NavigationView {
TabItemView(num: 2)
}.tabItem {
Text("Two")
}
}
}
}
struct TabItemView: View {
private let num: Int
init(num: Int) {
self.num = num
}
var body: some View {
NavigationLink(destination: DetailView(text: "Detail View \(num)").introspectNavigationController { navigationController in
navigationController.hidesBottomBarWhenPushed = true
}) {
Text("Go to Detail View")
}
}
}
struct DetailView: View {
#State private var showingSheet = false
private let text: String
init(text: String) {
self.text = text
}
var body: some View {
Button("Open Sheet") {
showingSheet.toggle()
}.sheet(isPresented: $showingSheet) {
Text("Sheet Text")
}
}
}
You need to flip how you nest TabView & NavigationView. Instead of nesting several TabView views inside a NavigationView, use the TabView as the parent component, with a NavigationView for each tab.
This is how the updated ContentView would look like:
struct ContentView: View {
var body: some View {
TabView {
NavigationView {
TabItemView(num: 1)
}
.tabItem {
Text("One")
}
NavigationView {
TabItemView(num: 2)
}
.tabItem {
Text("Two")
}
}
}
}
This makes sense and is more correct: The tabs should always be visible, but you want to show a different navigation stack with different content in each tab.
That it worked previously doesn't make it more correct - SwiftUI probably just changed its mind on dealing with unexpected situations. That, and the lack of error messages in these situations, is the downside of using a framework that tries to render anything you throw at it!
If the goal is specifically to hide the tabs when pushing a new view on a NavigationView (e.g., when tapping on a conversation in a messaging app), you have to use a different solution. Apple added the UIViewController.hidesBottomBarWhenPushed property to UIKit to support this specific use case.
This property is set on the UIViewController that, when presented, should not show a toolbar. In other words: Not the UINavigationController or the UITabBarController, but the child UIViewController that you push onto the UINavigationController.
This property is not supported in SwiftUI natively. You could set it using SwiftUI-Introspect, or simply write the navigation structure of your application using UIKit and write the views inside in SwiftUI, linking them using UIHostingViewController.

navigationBarHidden combined with inline display mode causes jump

I have a parent view, in which I don't want any navigation bar, and a child view, where I want an inline navigation bar.
If I navigate to the child view, then back again. The top of the list will have a weird jump effect when scrolling upwards.
I'm sure this is a bug, but does anyone have a workaround? If it helps, I can get access to the underlying UIScrollView/UINavigationController components - but I'm not sure if any of the properties would help.
struct ContentView: View {
var body: some View {
NavigationView {
List( 0...50, id: \.self ) { i in
NavigationLink(destination: HelloView()) {
Text("\(i)")
}
}
.navigationBarHidden( true )
}
}
}
struct HelloView: View {
var body: some View {
Text("Hello")
.navigationBarTitle("Hello", displayMode: .inline)
}
}
I realize this is odd, but this can be alleviated by setting the navigationBarTitle property. In your desired case I would recommend the following:
struct ContentView: View {
var body: some View {
NavigationView {
List( 0...50, id: \.self ) { i in
NavigationLink(destination: HelloView()) {
Text("\(i)")
}
}
.navigationBarTitle("", displayMode: .inline) /// <<--- Insert this line
.navigationBarHidden( true )
}
}
}
By setting the title attribute to blank and using the inline display mode, it rids the view of the large title and actually hides the view correctly.

Multiple NavigationLink in SwiftUI

Can you have multiple NavigationLinks in SwiftUI? The following only displays the first Link:
struct Test : View {
var body: some View {
NavigationView {
NavigationLink(destination: Text("First")) {
Text("Visible")
}
NavigationLink(destination: Text("Second")) {
Text("Invisible")
}
//EDIT: Also Invisible
Text("Not rendered")
}
}
}
EDIT: Turns out everything under the first NavigationLink is not displayed
Put your views inside a VStack:
struct Test : View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: Text("First")) {
Text("Visible")
}
NavigationLink(destination: Text("Second")) {
Text("Invisible")
}
//EDIT: Also Invisible
Text("Not rendered")
}
}
}
}
Look you can definitely have multiple NavigationLinks but here you are doing one things wrong.
The body property returns a single View but here you are trying to return more than one views, which causes the error.
To Solve this issue we put them inside another View like a VStack or a HStack, like shown above in the answer given by kontiki.