didSet behaviour on List type

I notice that when we modify a list by updating an existing element by the index, the didSet is called. this Behaviour surprised me, as I assumed that the value of the property has not changed, as it is still the same list just with a modified value.

(I assume when you say "list" you mean Array because the Swift standard library has no List type.)

Yes, your observation is correct, and this is how all value types (structs and enums) behave in Swift. The technical explanation is that didSet fires whenever you call a mutating method/property/subscript on a variable of value type. (Sidenote: didSet will fire even if the method doesn't actually mutate anything.)

Methods must be explicitly marked as mutating to be allowed to mutate self. Example: Array.append(_:).

Property and subscript setters (including the _modify/modify accessor, which is how mutating an array element by index is implemented) are mutating by default unless they're explicitly marked as nonmutating.

3 Likes

why is didSet fired in such cases? Modifying an array is counted as a change in the value of the property?

Looking back with the convenience of hindsight… it is possible that Swift could have been designed in a way to communicate value semantics effectively while also clearly breaking apart where value semantics end and where reference semantics begin…

The argument could be made that variable value types in Swift are not "traditional" immutable data structures… but they adopt similar ideas and philosophies IMO (assuming that we are talking about variable values and not constant values). What we colloquially call "modifying" an Array instance is producing a new value (but this value might be written in-place… which is one difference between a Swift struct and an immutable data structure from a purely functional programming context).

If you really want to dive deep on this specific data structure… yes Array is a copy-on-write data structure built on an object reference… but this is "progressive disclosure". For the most part… product engineers go about their business with the assuming that Array is a value type with value semantics. If the infra engineer breaks value semantics… that is a bug in the infra that needs to be fixed.

I'm not 100% sure what you're asking. Mutating an array element is considered a mutation of the entire array, in the same that any mutation of a component of a value type is considered a mutation of the whole.

Consider this example of nested structs:

struct A {
    var b: B {
        didSet { print("didSet A.b") }
    }
}

struct B {
    var c: Int {
        didSet { print("didSet B.c") }
    }
}

var a = A(b: B(c: 1)) {
    didSet { print("didSet a") }
}

The following line mutates a nested property and this will invoke the didSet handlers along the entire chain:

a.b.c += 1
// prints:
// didSet B.c
// didSet A.b
// didSet a

Could you provide an example as it is not immediately obvious what you are talking about. For example this work for me:

var a = [1, 2] {
    didSet { print(a) }
}
a[0] = 42 // [42, 2]

I suspect that the original question was about SwiftUI "list", which is actually a struct and thus a value type, exactly as an array. There is nothing to complain about, @BitByAVampire, when you modify a value type, Swift creates a new instance of that value (copy-on-write), and you are not dealing with with the "same" value (e.g. "list").

I can understand the frustration, as I myself prefer classes/objects (reference types), and do not use SwiftUI. Furthermore, Swift Concurrency encourages using value types, IMHO undermining OOP.

And I also heard (cannot say for sure) that SwiftUI recreates its tree of structs anew every time when something is changed, which is... suboptimal?

The SwiftUI List type has no publicly accessible mutating methods or mutable properties. So from the perspective of a SwiftUI user, it can't be mutated anyway.

To clarify, Swift generally does not use copy-on-write when you mutate a variable of value type. The common collection types in the standard library (incl. Array, Dictionary, Set, String) do use copy-on-write, but that's because they have been carefully implemented to do that.

Regardless of copy-on-write, I don't think the statement "when you modify a value type, Swift creates a new instance of that value" is true. Swift does modify storage in place when you mutate a variable of value type.

2 Likes