Search code examples
swiftui

Cannot append to array inside bound variable


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!


Solution

  • 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
    }