listswiftuiforeachstatebindable

How do I add sections to a binding array of structs that I can update?


I've been looking for a few days and I'm still can't find a solution.

I'd like to group a list of structs that are binding in a list, and still be able to update them.

In the example below I'd like to be able to toggle isOn and the bindable array be updated. This is as close as I've got...

enum UserType: Int, CaseIterable {
    case one = 1
    case two = 2
}

struct User: Identifiable {
    var id = UUID()
    var name: String
    var type: UserType
    var isOn: Bool
}

struct UserView: View {


@State var users: [User] = [
    User(name: "James", type: .one, isOn: false),
    User(name: "Josh", type: .two, isOn: false),
    User(name: "Jim", type: .one, isOn: false),
    User(name: "Jake", type: .two, isOn: false)
]

var body: some View {
    
    List(){
        ForEach(UserType.allCases) { type in
            Section(header: Text("\(type.rawValue)")) {
                ForEach(users) { user in
                    Text("user.name")
                    Toggle(isOn: user, label: Text("isOn"))
                }

            }
        }
        
    }
}

}


Solution

  • You should change your Data models to a class instead of a struct.

    It is how Apple recommends managing model data

    https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app

    It requires changing the model a little

    class User: Identifiable, ObservableObject {
        @Published var id: UUID
        @Published var name: String
        @Published var type: UserType
        @Published var isOn: Bool
        
        init(id: UUID = UUID(), name: String, type: UserType, isOn: Bool) {
            self.id = id
            self.name = name
            self.type = type
            self.isOn = isOn
        }
    }
    

    And using @ObservedObject to see changes and use @Binding for the value types.

    struct RowView: View {
        @ObservedObject var user: User
        var body: some View {
            Text(user.name)
            Toggle(isOn: $user.isOn, label: {
                Text("isOn")
            })
        }
    }
    

    Then your UserView can look something like.

    struct UserView: View {
        @State var users: [User] = [
            User(name: "James", type: .one, isOn: false),
            User(name: "Josh", type: .two, isOn: false),
            User(name: "Jim", type: .one, isOn: false),
            User(name: "Jake", type: .two, isOn: false)
        ]
        var grouped: [UserType: [User]] {
            Dictionary(grouping: users) { element in
                element.type
            }
        }
        var body: some View {
            
            List{
                ForEach(Array(grouped), id:\.key) { (type, users) in
                    Section(header: Text("\(type.rawValue)")) {
                        ForEach(users) { user in
                            RowView(user: user)
                        }
                    }
                }
            }
        }
    }
    

    For iOS 17 @Published and ObservableObject are no longer used you use use @Observable but you can adapt your code with a few changes.

    @Observable
    class User: Identifiable {
        var id: UUID = .init()
        var name: String = ""
        var type: UserType = UserType.allCases.randomElement()!
        var isOn: Bool = Bool.random()
        
        init(id: UUID = UUID(), name: String, type: UserType, isOn: Bool) {
            self.id = id
            self.name = name
            self.type = type
            self.isOn = isOn
        }
    }
    

    And use @Bindable for the row.

    struct RowView: View {
        @Bindable var user: User
        var body: some View {
            Text(user.name)
            Toggle(isOn: $user.isOn, label: {
                Text("isOn")
            })
        }
    }