How to make SwiftUI NavigationLink work in edit mode? - swiftui

I have a list of items. Clicking on one should push a new view to the navigation stack. I notice the NavigationLink doesn't work if the list is in edit mode. Is there a way to control that? I need it to work in edit mode.
List {
ForEach(segments) { segment in
NavigationLink(destination: EditSegmentView(segment: segment)) {
Text(segment.title)
}
}.onDelete(perform: onDelete)
.onMove(perform: onMove)
}.environment(\.editMode, $alwaysTrue)

I now have this working the way I wanted. I used a different NavigationLink initializer, with the tag and selection arguments. It seems to work well, but I don't know if this is the intended use of that initializer, because the documentation is painfully sparse.
#State var segmentSelection: Segment.ID? = nil
var body: some View {
NavigationView {
...
List {
ForEach(workout.segments) { segment in
NavigationLink(destination: EditSegmentView(segment: segment),
tag: segment.id,
selection: self.$segmentSelection) {
Text(segment.title)
}
.onTapGesture(perform: { self.segmentSelection = segment.id })
}.onDelete(perform: onDelete)
.onMove(perform: onMove)
}.environment(\.editMode, Binding.constant(.active))
...
}
}

Related

Non-deprecated way to call NavigationLink on Buttons

This is the old way of calling NavigationLink on Buttons
struct ContentView: View {
#State private var selection: String? = nil
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: View1(), tag: "tag1", selection: $selection) {
EmptyView()
}
NavigationLink(destination: NotView1(), tag: "tag2", selection: $selection) {
EmptyView()
}
Button("Do work then go to View1") {
// do some work that takes about 1 second
mySleepFunctionToSleepOneSecond()
selection = "tag1"
}
Button("Instantly go to NotView1") {
selection = "tag2"
}
}
.navigationTitle("Navigation")
}
}
}
This code works perfectly. It can go to different View targets depending on which button is clicked. Not only that, it guarantees all work is done BEFORE navigating to the target view. However, the only issue is that 'init(destination:tag:selection:label:)' was deprecated in iOS 16.0: use NavigationLink(value:label:) inside a List within a NavigationStack or NavigationSplitView
I get NavigationStack is awesome and such. But how can I translate the code to use the new NavigationStack + NavigationLink. Especially, how can I make sure work is done Before navigation?
Using new NavigationStack and its path property you can do much more. Your example will be transformed to
struct ContentView: View {
#State private var path = [String]()
var body: some View {
NavigationStack(path: $path) {
VStack {
Button("Do work then go to View1") {
// do some work that takes about 1 second
mySleepFunctionToSleepOneSecond()
path.append("tag1")
}
Button("Instantly go to NotView1") {
path.append("tag2")
}
}
.navigationTitle("Navigation")
.navigationDestination(for: String.self) { route in
switch route {
case "tag1":
EmptyView()
case "tag2":
EmptyView()
default:
EmptyView()
}
}
}
}
}
Check this video. There you can find more use cases.
For using non deprecated and after doing some work if we want to go to next view or in anyview there is something called ".navigationDestination". Let's see that using simple example.
#State var bool : Bool = false
var body: some View {
NavigationStack {
VStack {
Text("Hello, world!")
Button {
//Code here before changing the bool value
bool = true
} label: {
Text("Navigate Button")
}
}.navigationDestination(isPresented: $bool) {
SwiftUIView()
}
}
}
In this code we change take bool value as false and change it to true when our work is done using button.
.navigationDestination(isPresented: Binding<Bool>, destination: () -> View)
In .navigationDestination pass the Binding bool and provide the view you want to navigate.
You can use .navigationDestination multiple times.
Hope you found this useful.

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.

Strange issue of "Modifying state during view update, this will cause undefined behavior"

Goal: have a SwiftUI architecture where the "add new item" and "edit existing item" are solved by the same view (EditItemView). However, for some reason, when I do this, the runtime agent complains of "Modifying state during view update, this will cause undefined behavior".
This is the code I want to use, which ensures that the EDITING of the item and ADDING a new item are handled by the same EditItemView:
var body: some View {
NavigationView
{
ScrollView
{
LazyVGrid(columns: my_columns)
{
ForEach(items, id: \.id)
{
let item = $0
// THIS LINE TO EDIT AN EXISTING ITEM
NavigationLink(destination: EditItemView(item: item))
{
ItemView(item: item)
}
}
}
}
.navigationBarItems(trailing:
// THIS LINE TO ADD A NEW ITEM:
NavigationLink(destination: EditItemView(item: Data.singleton.createItem(name: "New item", value: 5.0))
{
Image(systemName: "plus")
}
)
}
}
It doesn't work, leading to the issue highlighted above. I am forced to separate the functionality for Edit and Add into two distinct Views, which then works:
var body: some View {
NavigationView
{
ScrollView
{
LazyVGrid(columns: my_columns)
{
ForEach(items, id: \.id)
{
let item = $0
// THIS LINE TO EDIT AN EXISTING ITEM
NavigationLink(destination: EditItemView(item: item))
{
ItemView(item: item)
}
}
}
}
.sheet(isPresented: $isPresented)
{
// FORCED TO USE SEPARATE VIEW
AddItemView { name, value in
_ = Data.singleton.createItem(name: name, value: value)
self.isPresented = false
}
}
.navigationBarItems(trailing: Button(action: { self.isPresented.toggle()}) { Image(systemName: "plus")})
}
}
I don't understand why the code in the first version is considered to modify the state while updating view, because to me, it's sequential: new Item is created and THEN a view is shown for that Item.
Any ideas?
The destination of NavigationLink is not rendered lazily, meaning it'll get rendered when the NavigationLink itself is rendered -- not when clicked through.
The sheet code, depending on platform and SwiftUI version, may have the same issue, but apparently does not in the version you're using. Or, the closure you provide to AddItemView isn't run immediately -- since you didn't include the code, it's not clear.
To solve the issue in the first method, you can use the following SO answer which provides a lazy NavigationLink: https://stackoverflow.com/a/61234030/560942

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.

SwiftUI Navigation Bar Title doesn't appear

I'm not sure if anything changed in Beta 3, however, when trying to add the NavigationBarTitle modifier to NavigationView, it does not show the text for the title? Any ideas?
NavigationView {
List(0 ..< 20) { item in
NavigationLink(destination: Text("1")) {
Text("Navigate 1")
}
}
}.navigationBarTitle(Text("Update")).navigationBarHidden(false)
}
The list shows but no title for the list in the NavigationView
You're setting .navigationBarTitle and .navigationBarHidden on NavigationView when they should be modifiers on List instead:
NavigationView {
List(0..<20) { item in
NavigationLink(destination: Text("1")) {
Text("Navigate 1")
}
}
.navigationBarTitle("Update")
.navigationBarHidden(false)
}
You can also just remove .navigationBarHidden(false) (unless you're setting it to true in a previous view or something).
Your code works fine and the navigationBarTitle is not outdated. It must be placed above (inside the Navigation View). Yes, it is sometimes confusing, it is necessary to remember this.
To the place where you currently have it .navigationBarTitle(Text ("Update")).navigationBarHidden(false) you need to set the modifier .navigationViewStyle(StackNavigationViewStyle ()), which means that you should always show the first screen regardless of the screen size.
var body: some View {
NavigationView {
List(0 ..< 20) { item in
NavigationLink(destination: Text("1")) {
Text("Navigate 1")
}
}
.navigationBarTitle(Text("Update"), displayMode: .automatic).navigationBarHidden(false)
}
// that means only show one view at a time no matter what device I'm working
.navigationViewStyle(StackNavigationViewStyle())
}