NavigationLink in List using isActive pushes the wrong row - swift

I'm trying to use NavigationLink's isActive variable to pop back to the root view controller.
The problem I'm experiencing is that using isActive pushes the wrong row when clicking on a list item. Remove the isActive variable and everything works as expected.
Here's some example code for demonstration purposes:
struct ContentView: View {
#State private var activateNavigationLink: Bool = false
var exampleData = ["a", "b", "c"]
var body: some View {
NavigationView {
List(exampleData, id: \.self) { item in
NavigationLink(
destination: SecondView(item: item), isActive: $activateNavigationLink) {
Text(item)
}
}
}
}
}
SecondView
struct SecondView: View {
var item: String
var body: some View {
Text(item)
}
}
This is driving me nuts. Any help would be greatly appreciated.

Because activateNavigationLink is just a Bool in your code, if it is true, every NavigationLink will register as active in your List. Right now, this is manifesting as the last item (C) getting pushed each time.
Instead, you'd need some system to store which item is active and then translate that to a boolean binding for the NavigationLink to use.
Here's one possible solution:
struct ContentView: View {
#State private var activeNavigationLink: String? = nil
var exampleData = ["a", "b", "c"]
func bindingForItem(item: String) -> Binding<Bool> {
.init {
activeNavigationLink == item
} set: { newValue in
activeNavigationLink = newValue ? item : nil
}
}
var body: some View {
NavigationView {
List(exampleData, id: \.self) { item in
NavigationLink(
destination: SecondView(item: item), isActive: bindingForItem(item: item)) {
Text(item)
}
}
}
}
}

You should not use activeNavigationLink on main view it should be used with cellView
struct ContentView: View {
var exampleData = ["a", "b", "c"]
var body: some View {
NavigationView {
List(exampleData, id: \.self) { item in
CellView(item: item)
}
}
}
}
CellView
struct CellView: View {
#State private var activateNavigationLink: Bool = false
var item: String
var body: some View {
NavigationLink(
destination: SecondView(item: item), isActive: $activateNavigationLink) {
Text(item)
}
}
}
SecondView
struct SecondView: View {
var item: String
var body: some View {
Text(item)
}
}

Related

Force deselection of row in SwiftUI List view

I am using a List with a selection Binding. My expectation is that clearing the selection should update the List to not have any selected rows but the row UI remains selected.
Here is the example code:
struct ContentView: View {
#State private var selection: String?
let names = [
"a",
"b",
"c",
"d"
]
var body: some View {
NavigationView {
VStack {
List(names, id: \.self, selection: $selection) { name in
Text(name)
}
Button("Deselect") {
self.selection = nil
}
}
}
}
}
I expect that when clearing the selection on button press, the list should update to not have any selection but it remains selected.
Working on iOS 16
import SwiftUI
struct listSelection: View {
#State private var selection: String?
let names = [
"a",
"b",
"c",
"d"
]
var body: some View {
NavigationView {
VStack {
List(names, id: \.self, selection: $selection) { name in
Text(name)
}
.id(UUID())
Button("Deselect") {
selection = nil
}
}
}
}
}
You can use listRowBackground modifier to set background based on selection.
struct ContentView: View {
#State private var selection: String?
let names = ["Munny", "Badnam", "Hui", "Tereliye"]
var body: some View {
NavigationView {
VStack {
List(names, id: \.self, selection: $selection) { name in
Text(name)
.listRowBackground(selection == name ? Color.black.opacity(0.2) : Color.white)
}
Button("Deselect") {
self.selection = .none
}
}
}
}
}
Single row selection is designed for when in edit mode, which you can turn on with the code below and it makes deselection work:
struct ListTestView2: View {
#State var editMode:EditMode = EditMode.active
#State private var selection: String?
let names = [
"a",
"b",
"c",
"d"
]
var body: some View {
NavigationView {
VStack {
List(names, id: \.self, selection: $selection) { name in
Text(name)
}
.environment(\.editMode, $editMode)
Button("Deselect") {
self.selection = nil
}
}
}
}
}

How to pop NavigationLink back to ContentView when inside a loop

The answer here works tremendously but, as Im sure, is not ideal for everyone. Say ContentView2, in the link, is looping a list of NavigationLinks, when self.$isActive is true, it triggers all the NavigationLinks to open so not ideal. I've found out about using tag and selection.
// Inside a ForEach loop
// Omitted is the use of EnvironmentObject
NavigationLink(
destination: DestinationView(id: loop.id),
tag: loop.id,
selection: self.$routerState.selection,
label: {
NavCell(loopData: loop)
}
)
.isDetailLink(false)
// State:
class RouterState: ObservableObject {
//#Published var rootActive: Bool = false
#Published var tag: Int = 0
#Published var selection: Int? = nil
}
How to pop the NavigationLink when inside of a loop? The answer in the link works but not inside a loop. Is there a way to amend the answer to use both tag and selection?
Using example from link above:
import SwiftUI
struct ContentView: View {
#State var isActive : Bool = false
var body: some View {
NavigationView {
List {
/// Data from the loop will be passed to ContentView2
ForEach(0..<10, id: \.self) { num in
NavigationLink(
destination: ContentView2(rootIsActive: self.$isActive),
isActive: self.$isActive
) {
Text("Going to destination \(num)")
}
.isDetailLink(false)
}
}
}
}
}
struct ContentView2: View {
#Binding var rootIsActive : Bool
var body: some View {
NavigationLink(destination: ContentView3(shouldPopToRootView: self.$rootIsActive)) {
Text("Hello, World #2!")
}
.isDetailLink(false)
.navigationBarTitle("Two")
}
}
struct ContentView3: View {
#Binding var shouldPopToRootView : Bool
var body: some View {
VStack {
Text("Hello, World #3!")
Button (action: { self.shouldPopToRootView = false } ){
Text("Pop to root")
}
}.navigationBarTitle("Three")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

SwiftUI Navigation: How to switch detail view to a different item?

I'm struggling implementing the following navigation behavior:
From a list the user can select an item which triggers a detail view for this item. On this detail view there is an "Add" button in the navigation bar which opens a modal sheet for adding an other item.
Up to this point, everything works as expected.
But after adding the item, I want the detail view to show the new item. I tried to set the list selection to the id of the new item. This triggers the detail view to disappear, the list selects the new item and show the details for a very short time, then the detail view disappears again and the list is shown.
I've tried adding a bridged binding and let the list view not set the selection to nil, this solves the issue at first, but then the "Back" button isn't working anymore.
Please note: I want the "Add" button on the detail view and not on the list view as you would expect it.
Here's the full code to test:
import Combine
import SwiftUI
struct ContentView: View {
#ObservedObject private var state = AppState.shared
var body: some View {
NavigationView {
List(state.items) {item in
NavigationLink(destination: DetailView(item: item), tag: item.id, selection: self.$state.selectedId) {
Text(item.title)
}
}
.navigationBarTitle("Items")
}
}
}
struct DetailView: View {
var item: Item
#State private var showForm = false
var body: some View {
Text(item.title)
.navigationBarItems(trailing: Button("Add") {
self.showForm = true
})
.sheet(isPresented: $showForm, content: { FormView() })
}
}
struct FormView: View {
#Environment(\.presentationMode) private var presentationMode
private var state = AppState.shared
var body: some View {
Button("Add") {
let id = self.state.items.count + 1
self.state.items.append(Item(id: id, title: "Item \(id)"))
self.presentationMode.wrappedValue.dismiss()
self.state.selectedId = id
}
}
}
class AppState: ObservableObject {
static var shared = AppState()
#Published var items: [Item] = [Item(id: 1, title: "Item 1")]
#Published var selectedId: Int?
}
struct Item: Identifiable {
var id: Int
var title: String
}
In your scenario it is needed to make navigation link destination independent, so it want be reactivated/invalidated when destination changed.
Here is possible approach. Tested with Xcode 11.7 / iOS 13.7
Updated code only:
struct ContentView: View {
#ObservedObject private var state = AppState.shared
#State private var isActive = false
var body: some View {
NavigationView {
List(state.items) {item in
HStack {
Button(item.title) {
self.state.selectedId = item.id
self.isActive = true
}
Spacer()
Image(systemName: "chevron.right").opacity(0.5)
}
}
.navigationBarTitle("Items")
.background(NavigationLink(destination: DetailView(), isActive: $isActive) { EmptyView() })
}
}
}
struct DetailView: View {
#ObservedObject private var state = AppState.shared
#State private var showForm = false
#State private var fix = UUID() // << fix for known issue with bar button misaligned after sheet
var body: some View {
Text(state.selectedId != nil ? state.items[state.selectedId! - 1].title : "")
.navigationBarItems(trailing: Button("Add") {
self.showForm = true
}.id(fix))
.sheet(isPresented: $showForm, onDismiss: { self.fix = UUID() }, content: { FormView() })
}
}

SwiftUI updating array state doesn't update view

Here is my test code:
struct ArrView: View {
#State var arr: [String] = ["first", "second"]
var body: some View {
VStack {
Button("append") {self.arr.append("item\(self.arr.count)")}
List(arr.indices) {row in
Text(self.arr[row])
}
}
}
}
When the 'append' Button was pressed, the List view didn't update.
Am I doing wrong?
You can use ForEach<Range>() only if the provided data is a constant, otherwise you need to use ForEach(_:id:content:) and provide an id explicitly, like this:
struct ArrView: View {
#State var arr: [String] = ["first", "second"]
var body: some View {
VStack {
Button("append") {self.arr.append("item\(self.arr.count)")}
List(arr, id: \.self) { item in
Text(item)
}
}
}
}
I don't think it is the proper way to create List view. Here is a sample
struct ContentView: View {
#State var arr: [String] = ["first", "second"]
var body: some View {
VStack {
Button("append") {self.arr.append("item\(self.arr.count)")}
List(self.arr, id: \.self) { str in
Text(str)
}
}
}
}

How to pass a string to a child view?

I want to pass the text in the textBox to the child view and create a scrollable Button there. As for the output status, we hope that 'a ~ c' are arranged vertically and that each is a button.
struct ContentView: View {
var textBox = ["a","b","c"]
var body: some View {
VStack {
ScrollView(.vertical, showsIndicators: false) {
ForEach(0..<textBox.count) { number in
ScrollText(text: self.textBox[number].lowercased())
}
}
}
}
}
struct ScrollText: View {
#Binding var text: String
#State private var flag: Bool = false
var body: some View {
Button(action: {
self.flag.toggle()
}) {
Text(text)
}
}
}
I'm not totally clear what the problem is, or what you want, but I solved some compiler errors in your code, and it's showing three buttons as expected:
struct ContentView : View {
var textBox = ["a","b","c"]
var body: some View {
VStack {
ScrollView(.vertical, showsIndicators: false){
ForEach(textBox, id: \.self) { letter in
ScrollText(text: letter)
}
}
}
}
}
struct ScrollText: View {
var text: String
#State private var flag: Bool = false
var body: some View {
Button(action: {
self.flag.toggle()
}, label: {
Text(text)
})
}
}
Your question was how to pass a string, so you don't need #Binding for that. Just pass a string :)
If you're going to keep ScrollText untouched the here is possible modifications in ContentView which uses it
struct ContentView: View {
#State private var textBox = ["a","b","c"] // < make State, so modifiable
var body: some View {
VStack {
ScrollView(.vertical, showsIndicators: false) {
ForEach(0..<textBox.count) { number in
ScrollText(text: self.$textBox[number]) // < pass Binding as intended
}
}
}
}
}