NavigationStack's navigationDestination in Xcode beta unable to navigate - swiftui

I am still new to SwiftUI framework I am learning with HackingWithSwift for NavigationStack. I followed all the steps just one difference is my result of code. I ran without any error just like in video however I could not navigate to my Detail destination.
struct ContentView: View {
var destinations = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
var body: some View {
NavigationStack {
List(destinations, id: \.self) { i in
NavigationLink(value: i) {
Label("Row \(i)", systemImage: "\(i).circle")
}
}
.navigationDestination(for: Int.self) { i in
Text("Detail \(i)")
}
.navigationTitle("Navigation")
}
}
}
My simulator show a List of Rows with Labels but I am unable to navigate.
Could this by chance a bug because this is still new. Thank you in advance.

Your List data is an array of String, but the .navigationDestination value type is Int.
To fix your problem, modify the data type of .navigationDestination like below:
.navigationDestination(for: String.self) { I in //modify here
Text("Detail \(i)")
}

There is special List (and ForEach) syntax for a static array of Int, here it is:
struct NavTestView: View {
//let destinations = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
var body: some View {
NavigationStack {
List(1..<10) { i in
NavigationLink(value: i) {
Label("Row \(i)", systemImage: "\(i).circle")
}
}
.navigationDestination(for: Int.self) { i in
Text("Detail \(i)")
}
.navigationTitle("Navigation")
}
}
}
If you move on to requiring a dynamic array, i.e. you add or remove items, you really shouldn't misuse the API with the id:\.self hack or it'll crash, you need to migrate to an array of struct that contains a property that is a unique identifier, even better conform the struct to Identifiable that provides an id property for List/ForEach to use automatically, e.g.
struct ListItem: Identifiable {
let id = UUID()
var number: Int
}

Related

Deselect Items from SwiftUI List on macOS with single click

I'm developing a macOS App with a List View with selectable rows.
As there is unfortunately no editMode
on macOS, Single Click Selection of Cells is possible, but deselecting an already selected Cell doing the same does nothing.
The only option to deselect the Cell is to CMD + Click which is not very intuitive.
Minimum Example:
struct RowsView: View {
#State var selectKeeper: String?
let rows = ["1", "2", "3", "4", "5", "6", "7", "8"]
var body: some View {
List(rows, id: \.self, selection: $selectKeeper) { row in
Text(row)
}
}
}
struct RowsView_Previews: PreviewProvider {
static var previews: some View {
RowsView()
}
}
Clicking Row Nr 3 with a Single Click or even double Click does nothing and the row stays selected.
Attaching Binding directly
I have tried to attach the Binding directly as described in the excelent answer for a Picker here, but this does not seem to work for List on macOS:
...
var body: some View {
List(rows, id: \.self, selection: Binding($selectKeeper, deselectTo: nil)) { row in
Text(row)
}
}
...
public extension Binding where Value: Equatable {
init(_ source: Binding<Value>, deselectTo value: Value) {
self.init(get: { source.wrappedValue },
set: { source.wrappedValue = $0 == source.wrappedValue ? value : $0 }
)
}
}
Any ideas on how single click deselect can be made possible without rebuilding the selection mechanism?
For the record: XCode 13.2.1, macOS BigSur 11.6.2
We can block default click handling by using own gesture and manage selection manually.
Here is demo of possible approach. Tested with Xcode 13.2 / macOS 12.1
struct RowsView: View {
#State var selectKeeper: String?
let rows = ["1", "2", "3", "4", "5", "6", "7", "8"]
var body: some View {
List(rows, id: \.self, selection: $selectKeeper) { row in
Text(row)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
.contentShape(Rectangle()) // handle click row-wide
.listRowInsets(EdgeInsets()) // remove default edges
.onTapGesture {
selectKeeper = selectKeeper == row ? nil : row // << here !!
}
.padding(.vertical, 4) // look&feel like default
}
}
}
I refer to this answer to a SO post titled "Select Multiple Items in a SwiftUI List".
From this answer I generated code to select multiple items in a list.
In my sample code I use a checkmark to illustrate whether a row is selected, but you could change this to suit your needs.
You'll need to change your #State wrapper to a Set, because you confirmed that you'll need to select a group of items. (An aside, Apple recommends that you mark #State property wrappers as private so as to reinforce their intended use locally.)
#State private var selectedItems: Set<String>?
let rows = ["1", "2", "3", "4", "5", "6", "7", "8"]
var body: some View {
List(rows, id: \.self) { item in
RowSelectable(selectedItems: $selectedItems, rowItem: item)
}
}
where RowSelectable is...
struct RowSelectable: View {
#Binding var selectedItems: Set<String>?
var rowItem: String
var isSelected: Bool {
return selectedItems?.contains(rowItem) == true
}
var body: some View {
HStack {
Text(rowItem)
if selectedItems?.contains(rowItem) == true {
Spacer()
Image(systemName: "checkmark")
}
}
.onTapGesture(count: 1) {
if self.isSelected {
selectedItems!.remove(rowItem)
}
else {
selectedItems!.insert(rowItem)
}
}
}
}
I haven't tested this yet, so let me know if it doesn't work and I'll check in Xcode.

SwiftUI: Toggling a JSON property in a List

I have a local JSON file I am importing and decoding. I am them iterating through that data to create a list. I have a Button and I want to toggle the value of the favorite property when the button is tapped. I realize that would be mutating a JSON value which wouldnt work so I am trying to figure out how to accomplish this.
Towns.json
[
{
"display_name": "California",
"favorite": false,
},
{
"display_name": "Colorado",
"favorite": false,
}
]
Town.swift
struct Town: Codable, Identifiable {
var id: String {image}
let display_name: String
let favorite: Bool
}
MainView.swift
ForEach(towns) { town in
LazyVStack(spacing: 20) {
HStack {
Text(town.display_name)
Spacer()
Button {
town.favorite.toggle()
} label: {
if town.favorite {
Image(systemName: "flame").foregroundColor(.red)
} else {
Image(systemName: "flame.fill").foregroundColor(.red)
}
}
}
}
}
You'll need a way to access the original element from the array. In SwiftUI 3 (just announced), this has become much easier, but until that's out, generally people use indices or enumerated to get an index of the original item (there's also a .indexed() from Swift Collections, but it requires importing an SPM package to use it):
struct ContentView : View {
#State var towns : [Town] = []
var body: some View {
ForEach(Array(towns.enumerated()), id: \.1.id) { (index,town) in
LazyVStack(spacing: 20) {
HStack {
Text(town.display_name)
Spacer()
Button {
towns[index].favorite.toggle()
} label: {
if town.favorite {
Image(systemName: "flame").foregroundColor(.red)
} else {
Image(systemName: "flame.fill").foregroundColor(.red)
}
}
}
}
}
}
}
You'll also need to change let favorite to var favorite in your model, since now it's a mutable property.

How can I stop a SwiftUI TextField from losing focus when binding within a ForEach loop?

In a form, I'd like a user to be able to dynamically maintain a list of phone numbers, including adding/removing numbers as they wish.
I'm currently maintaining the list of numbers in a published array property of an ObservableObject class, such that when a new number is added to the array, the SwiftUI form will rebuild the list through its ForEach loop. (Each phone number is represented as a PhoneDetails struct, with properties for the number itself and the type of phone [work, cell, etc].)
Adding/removing works perfectly fine, but when I attempt to edit a phone number within a TextField, as soon as I type a character, the TextField loses focus.
My instinct is that, since the TextField is bound to the phoneNumber property of one of the array items, as soon as I modify it, the entire array within the class publishes the fact that it's been changed, hence SwiftUI dutifully rebuilds the ForEach loop, thus losing focus. This behavior is not ideal when trying to enter a new phone number!
I've also tried looping over an array of the PhoneDetails objects directly, without using an ObservedObject class as an in-between repository, and the same behavior persists.
Below is the minimum reproducible example code; as mentioned, adding/removing items works great, but attempting to type into any TextField immediately loses focus.
Can someone please help point me in the right direction as to what I'm doing wrong?
class PhoneDetailsStore: ObservableObject {
#Published var allPhones: [PhoneDetails]
init(phones: [PhoneDetails]) {
allPhones = phones
}
func addNewPhoneNumber() {
allPhones.append(PhoneDetails(phoneNumber: "", phoneType: "cell"))
}
func deletePhoneNumber(at index: Int) {
if allPhones.indices.contains(index) {
allPhones.remove(at: index)
}
}
}
struct PhoneDetails: Equatable, Hashable {
var phoneNumber: String
var phoneType: String
}
struct ContentView: View {
#ObservedObject var userPhonesManager: PhoneDetailsStore = PhoneDetailsStore(
phones: [
PhoneDetails(phoneNumber: "800–692–7753", phoneType: "cell"),
PhoneDetails(phoneNumber: "867-5309", phoneType: "home"),
PhoneDetails(phoneNumber: "1-900-649-2568", phoneType: "office")
]
)
var body: some View {
List {
ForEach(userPhonesManager.allPhones, id: \.self) { phoneDetails in
let index = userPhonesManager.allPhones.firstIndex(of: phoneDetails)!
HStack {
Button(action: { userPhonesManager.deletePhoneNumber(at: index) }) {
Image(systemName: "minus.circle.fill")
}.buttonStyle(BorderlessButtonStyle())
TextField("Phone", text: $userPhonesManager.allPhones[index].phoneNumber)
}
}
Button(action: { userPhonesManager.addNewPhoneNumber() }) {
Label {
Text("Add Phone Number")
} icon: {
Image(systemName: "plus.circle.fill")
}
}.buttonStyle(BorderlessButtonStyle())
}
}
}
try this:
ForEach(userPhonesManager.allPhones.indices, id: \.self) { index in
HStack {
Button(action: {
userPhonesManager.deletePhoneNumber(at: index)
}) {
Image(systemName: "minus.circle.fill")
}.buttonStyle(BorderlessButtonStyle())
TextField("Phone", text: $userPhonesManager.allPhones[index].phoneNumber)
}
}
EDIT-1:
Reviewing my comment and in light of renewed interest, here is a version without using indices.
It uses the ForEach with binding feature of SwiftUI 3 for ios 15+:
class PhoneDetailsStore: ObservableObject {
#Published var allPhones: [PhoneDetails]
init(phones: [PhoneDetails]) {
allPhones = phones
}
func addNewPhoneNumber() {
allPhones.append(PhoneDetails(phoneNumber: "", phoneType: "cell"))
}
// -- here --
func deletePhoneNumber(of phone: PhoneDetails) {
allPhones.removeAll(where: { $0.id == phone.id })
}
}
struct PhoneDetails: Identifiable, Equatable, Hashable {
let id = UUID() // <--- here
var phoneNumber: String
var phoneType: String
}
struct ContentView: View {
#ObservedObject var userPhonesManager: PhoneDetailsStore = PhoneDetailsStore(
phones: [
PhoneDetails(phoneNumber: "800–692–7753", phoneType: "cell"),
PhoneDetails(phoneNumber: "867-5309", phoneType: "home"),
PhoneDetails(phoneNumber: "1-900-649-2568", phoneType: "office")
]
)
var body: some View {
List {
ForEach($userPhonesManager.allPhones) { $phone in // <--- here
HStack {
Button(action: {
userPhonesManager.deletePhoneNumber(of: phone) // <--- here
}) {
Image(systemName: "minus.circle.fill")
}.buttonStyle(BorderlessButtonStyle())
TextField("Phone", text: $phone.phoneNumber) // <--- here
}
}
Button(action: { userPhonesManager.addNewPhoneNumber() }) {
Label {
Text("Add Phone Number")
} icon: {
Image(systemName: "plus.circle.fill")
}
}.buttonStyle(BorderlessButtonStyle())
}
}
}

Using TextField with ForEach in SwiftUI

I am trying to display a dynamic list of text fields using a ForEach. The following code is working as expected: I can add/remove text fields, and the binding is correct. However, when I move the items in a ObservableObject view model, it does not work anymore and it crashes with an index out of bounds error. Why is that? How can I make it work?
struct ContentView: View {
#State var items = ["A", "B", "C"]
var body: some View {
VStack {
ForEach(items.indices, id: \.self) { index in
FieldView(value: Binding<String>(get: {
items[index]
}, set: { newValue in
items[index] = newValue
})) {
items.remove(at: index)
}
}
Button("Add") {
items.append("")
}
}
}
}
struct FieldView: View {
#Binding var value: String
let onDelete: () -> Void
var body: some View {
HStack {
TextField("item", text: $value)
Button(action: {
onDelete()
}, label: {
Image(systemName: "multiply")
})
}
}
}
The view model I am trying to use:
class ViewModel: Observable {
#Published var items: [String]
}
#ObservedObject var viewModel: ViewModel
I found many questions dealing with the same problem but I could not make one work with my case. Some of them do not mention the TextField, some other are not working (anymore?).
Thanks a lot
By checking the bounds inside the Binding, you can solve the issue:
struct ContentView: View {
#ObservedObject var viewModel: ViewModel = ViewModel(items: ["A", "B", "C"])
var body: some View {
VStack {
ForEach(viewModel.items.indices, id: \.self) { index in
FieldView(value: Binding<String>(get: {
guard index < viewModel.items.count else { return "" } // <- HERE
return viewModel.items[index]
}, set: { newValue in
viewModel.items[index] = newValue
})) {
viewModel.items.remove(at: index)
}
}
Button("Add") {
viewModel.items.append("")
}
}
}
}
It is a SwiftUI bug, similar question to this for example.
I can not perfectly explain what is causing that crash, but I've been able to reproduce the error and it looks like after deleting a field,SwiftUI is still looking for all indices and when it is trying to access the element at a deleted index, it's unable to find it which causes the index out of bounds error.
To fix that, we can write a conditional statement to make sure an element is searched only if its index is included in the collection of indices.
FieldView(value: Binding<String>(get: {
if viewModel.items.indices.contains(index) {
return viewModel.items[index]
} else {
return ""
}
}, set: { newValue in
viewModel.items[index] = newValue
})) {
viewModel.items.remove(at: index)
}
The above solution solves the problem since it makes sure that the element will not be searched when the number of elements (items.count) is not greater than the index.
This is just what I've been able to understand, but something else might be happening under the hood.

Incomplete separator with Picker SwiftUI

Is there a way to fix the incomplete separator in the selected row? the issue is in simulator and real device.
Xcode 12.4 and iOS 14.4
struct ContentView: View {
var numbers = ["1", "2", "3", "4"]
#State private var selectedIndex = 0
var body: some View {
NavigationView {
Form {
Section {
Picker(selection: $selectedIndex, label: Text("Test")) {
ForEach(0 ..< numbers.count) {
Text(self.numbers[$0])
}
}
}
}
}
}
}