I have a person object with a list of strings
struct Person: Identifiable, Hashable {
var id: UUID = UUID()
var name: String
var strings: [String] = []
static func == (lhs: Person, rhs: Person) -> Bool {
return lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
hasher.combine(name)
}
}
which is stored in a @State
array variable in the app struct and from my PersonView
I'd like to add elements to the strings
array.
struct PersonView: View {
@Binding var person: Person
var body: some View {
List {
Section(header: HStack {
Text("Strings")
Spacer()
Button(action: {
print("BEFORE", person.strings)
person.strings.append("foo")
print("AFTER", person.strings)
}) {
Image(systemName: "plus")
}
}) {
ForEach($person.strings, id: \.self) { $str in
NavigationLink(destination: Text(str)) {
Text(str)
}
}
}
}
.navigationTitle(person.name)
}
}
I'm not reading the string or anything to make the value dynamic because I wanted to simplify the problem as much as I could. This would eventually be replaced by a struct or class, whichever is appropriate.
When I click the button in the PersonView
nothing is appended to the array and no errors are shown in the debug output. What am I doing wrong? What is the idiomatic way of managing collections of objects within another object?
Basically, I want to have a list of top level entities (Person) that have activities they want to do (specific to each Person) and characteristic that affect those activities (also specific to each Person). For instance, Joe is smart but weak and likes to play chess and lift weights. His chess attempts have 20% better chance of success but his bench presses are -15% likely to succeed. Sally just likes to run which takes advantage of her 30% leg speed.
Here is the rest of the application's code: (app)
import SwiftUI
@main
struct bindingsApp: App {
@State private var people:[Person] = []
var body: some Scene {
WindowGroup {
ContentView(people: $people)
}
}
}
(content view)
struct ContentView: View {
@Binding var people: [Person]
@State private var isPresentingTheEditView: Bool = false
@State private var tmpPerson: Person = Person(name: "")
var body: some View {
NavigationStack {
List {
Section(header: HStack {
Text("People")
Spacer()
Button(action: {
isPresentingTheEditView = true
}) {
Image(systemName: "plus")
}
}) {
ForEach($people) { $person in
NavigationLink(destination: PersonView(person: $person)) {
Text(person.name)
}
}
}
}
.sheet(isPresented: $isPresentingTheEditView) {
PersonNewSheet(targetPeople: $people, isPresentingTheEditView: $isPresentingTheEditView)
}
}
}
}
(new person sheet)
struct PersonNewSheet: View {
@State private var name: String = ""
@Binding var targetPeople: [Person]
@Binding var isPresentingTheEditView: Bool
var body: some View {
NavigationStack {
PersonEditView(name: $name)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Dismiss") {
isPresentingTheEditView = false
}
}
ToolbarItem(placement: .confirmationAction) {
Button("Add") {
isPresentingTheEditView = false
targetPeople.append(Person(name: name, strings: ["bar"]))
}
}
}
}
}
}
(person form)
struct PersonEditView: View {
@Binding var name: String
var body: some View {
NavigationView {
Form {
Section {
TextField("Name", text: $name)
}
}
}
.navigationTitle("Person Edit")
}
}
Thanks!
ForEach
expect a collection of unique (preferably Identifiable) items, see ForEach.
Using ForEach($person.strings, id: \.self)
is not a good idea, since
the array [strings]
could contain multiple same string.
Note, with your struct Person
don't try to cook up Hashable
(or Equatable) yourself,
let the system do it for you automatically. This is the source of your original problem (static func == ...
), remove those funcs and it will work.
Note also, NavigationView
is deprecated, use NavigationStack
.
Try this updated code where Person
has a var items: [Item]
and
each Item
is unique and Identifiable
to hold your string, ready for any ForEach
.
If you are targeting iOS17+, have a look at this option for Managing model data in your app
struct PersonView: View {
@Binding var person: Person
var body: some View {
List {
Section(header: HStack {
Text("Strings")
Spacer()
Button(action: {
print("BEFORE", person.items)
person.items.append(Item(string: "bar")) // <--- here
print("AFTER", person.items)
}) {
Image(systemName: "plus")
}
}) {
ForEach($person.items) { $item in // <--- here, $ not really needed in this example
NavigationLink(destination: Text(item.string)) { // <--- here
Text(item.string) // <--- here
}
}
}
}
.navigationTitle(person.name)
}
}
struct PersonNewSheet: View {
@State private var name: String = ""
@Binding var targetPeople: [Person]
@Binding var isPresentingTheEditView: Bool
var body: some View {
NavigationStack {
PersonEditView(name: $name)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Dismiss") {
isPresentingTheEditView = false
}
}
ToolbarItem(placement: .confirmationAction) {
Button("Add") {
isPresentingTheEditView = false
targetPeople.append(Person(name: name, items: [Item(string: "bar")])) // <--- here
}
}
}
}
}
}
struct PersonEditView: View {
@Binding var name: String
var body: some View {
NavigationStack { // <--- here
Form {
Section {
TextField("Name", text: $name)
}
}
}
.navigationTitle("Person Edit")
}
}
struct Person: Identifiable { // <--- here
let id: UUID = UUID()
var name: String
var items: [Item] = [] // <--- here
}
struct Item: Identifiable { // <--- here
let id: UUID = UUID()
var string: String
}