Having problems with a SwiftUI checkbox binding I'm implementing...

Hello folks,
I'm attempting to implement some swiftUI UI code to support filtering of a list.
One part of the filtering involves displaying one checkbox for each case/value of an enum (TangleType below)
TangleFilter is a model class that includes an array of TangleTypeFilter objects (each owning a single bool value and a binding)

Expected behaviour: when user taps a checkbox, the checkbox toggles the display and the filter model object toggles its value.
Actual behaviour: the model is updating appropriately, however the UI is not updating. (the single filter below the list does in fact behave correctly

any and all guidance greatly appreciated
Mike

Code Block
struct ContentView: View {
    @State var isChecked: Bool = false
    @ObservedObject var filter = TangleFilter()
    @ObservedObject var singleFilter: TangleTypeFilter
    init() {
        self.singleFilter = TangleTypeFilter(tangleType: .grid)
    }
    var body: some View {
        VStack{
            List(filter.tangleTypes, id: \.self) {tangleTypeFilter in
                HStack {
                    // when uncommented the following line returns the following
                    // compile error:
                    // Use of unresolved identifier '$tangleTypeFilter'
//                    CheckBox(isChecked: $tangleTypeFilter.isChecked)
                    CheckBox(isChecked: tangleTypeFilter.binding)
                    Text("checked? \(tangleTypeFilter.isChecked.description)")
                }
            }
            CheckBox(isChecked: $singleFilter.isChecked)
        }
    }
}
struct CheckBox: View {
    @Binding var isChecked: Bool {
        didSet {
            print("setting isChecked: \(isChecked)")
        }
    }
    var imageName: String {
        return isChecked ? "checkmark.square" : "square"
    }
    var body: some View {
        Button(action: {
            self.isChecked.toggle()
        }) {
            Image(systemName: self.imageName)
        }
    }
}
enum TangleType: String, Codable, CaseIterable {
    static let filterArray: [TangleTypeFilter] = {
        var result: [TangleTypeFilter] = []
        for tangleType in TangleType.allCases {
            result.append(TangleTypeFilter(tangleType: tangleType))
        }
        return result
    }()
    case grid
    case row
}
class TangleFilter: ObservableObject {
    @Published var tangleTypes: [TangleTypeFilter] = TangleType.filterArray
}
class TangleTypeFilter: ObservableObject {
    var tangleType: TangleType
    @Published var isChecked: Bool
    lazy var binding: Binding<Bool> = Binding(get: {
        return self.isChecked
    }, set: {
        self.isChecked = $0
    })
    init(tangleType: TangleType) {
        self.tangleType = tangleType
        self.isChecked = false
    }
}
extension TangleTypeFilter: Hashable {
    static func == (lhs: TangleTypeFilter, rhs: TangleTypeFilter) -> Bool {
        return lhs.tangleType == rhs.tangleType
    }
    func hash(into hasher: inout Hasher) {
        hasher.combine(tangleType)
    }
}



Answered by OOPer in 656488022
The problem seems to be quite clear, no?

Code Block
class TangleFilter: ObservableObject {
@Published var tangleTypes: [TangleTypeFilter] = TangleType.filterArray
}

When TangleTypeFilter is a reference type, changing some property would not be considered as changing tangleTypes.
Whether or not the property is @Published.
Thus, updating isChecked would not update UI.

One way of achieving what you want is to make your TangleTypeFilter a struct:
Code Block
struct ContentView: View {
@State var isChecked: Bool = false
@ObservedObject var filter = TangleFilter()
@State var singleFilter: TangleTypeFilter = TangleTypeFilter(tangleType: .grid)
var body: some View {
VStack{
List(filter.tangleTypes.indices, id: \.self) {tangleTypeIndex in
HStack {
CheckBox(isChecked: $filter.tangleTypes[tangleTypeIndex].isChecked)
Text("checked? \(filter.tangleTypes[tangleTypeIndex].isChecked.description)")
}
}
CheckBox(isChecked: $singleFilter.isChecked)
}
}
}
struct CheckBox: View {
@Binding var isChecked: Bool {
didSet {
print("setting isChecked: \(isChecked)")
}
}
var imageName: String {
return isChecked ? "checkmark.square" : "square"
}
var body: some View {
Button(action: {
self.isChecked.toggle()
}) {
Image(systemName: self.imageName)
}
}
}
enum TangleType: String, Codable, CaseIterable {
static let filterArray: [TangleTypeFilter] = {
var result: [TangleTypeFilter] = []
for tangleType in TangleType.allCases {
result.append(TangleTypeFilter(tangleType: tangleType))
}
return result
}()
case grid
case row
}
class TangleFilter: ObservableObject {
@Published var tangleTypes: [TangleTypeFilter] = TangleType.filterArray
}
struct TangleTypeFilter {
var tangleType: TangleType
var isChecked: Bool
init(tangleType: TangleType) {
self.tangleType = tangleType
self.isChecked = false
}
}


Accepted Answer
The problem seems to be quite clear, no?

Code Block
class TangleFilter: ObservableObject {
@Published var tangleTypes: [TangleTypeFilter] = TangleType.filterArray
}

When TangleTypeFilter is a reference type, changing some property would not be considered as changing tangleTypes.
Whether or not the property is @Published.
Thus, updating isChecked would not update UI.

One way of achieving what you want is to make your TangleTypeFilter a struct:
Code Block
struct ContentView: View {
@State var isChecked: Bool = false
@ObservedObject var filter = TangleFilter()
@State var singleFilter: TangleTypeFilter = TangleTypeFilter(tangleType: .grid)
var body: some View {
VStack{
List(filter.tangleTypes.indices, id: \.self) {tangleTypeIndex in
HStack {
CheckBox(isChecked: $filter.tangleTypes[tangleTypeIndex].isChecked)
Text("checked? \(filter.tangleTypes[tangleTypeIndex].isChecked.description)")
}
}
CheckBox(isChecked: $singleFilter.isChecked)
}
}
}
struct CheckBox: View {
@Binding var isChecked: Bool {
didSet {
print("setting isChecked: \(isChecked)")
}
}
var imageName: String {
return isChecked ? "checkmark.square" : "square"
}
var body: some View {
Button(action: {
self.isChecked.toggle()
}) {
Image(systemName: self.imageName)
}
}
}
enum TangleType: String, Codable, CaseIterable {
static let filterArray: [TangleTypeFilter] = {
var result: [TangleTypeFilter] = []
for tangleType in TangleType.allCases {
result.append(TangleTypeFilter(tangleType: tangleType))
}
return result
}()
case grid
case row
}
class TangleFilter: ObservableObject {
@Published var tangleTypes: [TangleTypeFilter] = TangleType.filterArray
}
struct TangleTypeFilter {
var tangleType: TangleType
var isChecked: Bool
init(tangleType: TangleType) {
self.tangleType = tangleType
self.isChecked = false
}
}


Hi OOPer,
This was definitely not obvious (I'm gonna reserve judgement on whether this says more about me or the complexity of my question)

having said that, I think I now see:
when SwiftUI depends on properties in 'child' model objects stored in an array in a parent model object:
  1. the parent should conform to ObservableObject (and be a ref type)

  2. the children in the array need to be value type (struct)

does this mean that updating a property in a value type that is stored in an array will replace the array element with a new copy of the value type.
initially: filter.tangleTypes = [0x123456, 0x123466]
filter.tangleTypes[1].isChecked = true
Are you saying at this point filter.tangleTypes = [0x123456, 0x123476]
?

as always thanks for taking the time to share :-)

This was definitely not obvious

Seems obvious is sort of subjective and it is not for you.

does this mean that updating a property in a value type that is stored in an array will replace the array element with a new copy of the value type. 

Yes. Have you heard that Swift Array is a value type?

initially: filter.tangleTypes = [0x123456, 0x123466]

What do you mean? What are 0x123456...?
Are you talking about the case of Array containing struct (value type)?


Yes. Have you heard that Swift Array is a value type?

I was not aware. For quite some time I've imagined Swift arrays more like NSArray



> initially: filter.tangleTypes = [0x123456, 0x123466]


What do you mean? What are 0x123456...?
Are you talking about the case of Array containing struct (value type)?

This was my attempt to:
  1. better understand how value types are stored in array

  2. the mechanism used for swiftUI to recognize that an @Published property has changed

0x123456 was intended to be a memory location (address)
I was under the impression that a swift array containing structs would be represented as a list of memory locations (one per struct/item in the array)
Further, I was imagining that when the item/struct was modified the array (modelled as a list of memLocations) would now include a new location for the modified item. (and that this new address in the array was what would allow swiftUI View classes to be made aware of the changed in the @Published property in the appropriate ObservableObject.
I recently learned that value types in an array don't really have a memory location.
https://forums.swift.org/t/memory-address-of-value-types-and-reference-types/6637/7

I guess they're just stored directly in the array's allocated memory? As such when the value object changes in anyway, the array's memory/contents are modified, and detected by any swiftUI View classes that happen to be listening.

Mike






For quite some time I've imagined Swift arrays more like NSArray

You should better have in mind, that in Swift, Array and Dictionary are value types. The behavior of them are far different from NSArray and NSDictionary.

0x123456 was intended to be a memory location (address)

Seems you are too experienced about the implementation details of runtime of other programming languages.

When you want to discuss about value types, memory location cannot be a good metaphor.
Conceptually, every variable of value type holds the copy of the value for its own.

I was under the impression that a swift array containing structs would be represented as a list of memory locations (one per struct/item in the array)

Please be free from the implementation details and concentrate on how value types should work.

When a property of an element (value type) of an Array is modified, Swift treats as the Array itself is modified. That's the nature of value type in Swift.

When a property of an element (value type) of an Array is modified, Swift treats as the Array itself is modified. That's the nature of value type in Swift.

This nicely summarizes my main take away message from this exchange.
thx again!


Having problems with a SwiftUI checkbox binding I'm implementing...
 
 
Q