Search code examples
swiftswiftuiswiftui-list

How SwiftUI recognizes modification of observed array?


I am trying to wrap my head around SwiftUI observable objects in connection with arrays. Say I have a model containing an array of data. I want this array to be viewed in a SwiftUI List, and I want to reflect addition and removal of items. I dont want List to be rebuilt after every addition - instead, I want it to animate addition of every new item while retaining scroll position etc.

I have read some tutorials and have came up with following simplified code:

class MyModel: ObservableObject {
    var myData: [SomeSpecialClass] = []

    func Add(item: SomeSpecialClass) {
        self.myData.append(item)
        self.objectWillChange.send()
    }
}

struct ContentView: View {
    @ObservedObject var model = MyModel()

    var body: some View {
        List {
            ForEach(model.myData) {item in
                // render item somehow
            }
        }
        Button("Add") {
            model.Add(SomeSpecialClass())
        }
    }
}

But then, what objectWillChange.send() actually does? How does List recognize that item was added and which one? How it will decide whether to animate addition, or just rebuild everything?


Solution

  • So the following video sheds some light on the subject: developer.apple.com/videos/play/wwdc2021/10022

    Briefly - swiftUI never does simple full rebuild of a view. When a state (or observed object etc.) of a view changes, it generates a new view structure, which is then merged into existing (not simply replacing the existing structure). The merge is a process, that detects which views were removed (which will then be animated as transition away), which were added (that will be animated as transition in), and which were changed (that will remain - not be rebuilt - but their changed properties will be animated to a new state). Thus, if you build a new List with some new items after ObservedObject is changed, SwiftUI recognizes automatically which items are new (and will be animated in), which maybe changed position, and which should remain. Automatically, because that is the way it works with all views.

    It seems that the whole process is quite optimized, so merge of even a large list is fast. So even if it seems in code that you build totally new List after a minor change, the SwiftUI mechanism ensures that it just changes the existing. So in ObservableObject, calling self.objectWillChange.send() is enough, because it notifies view that it should build itself again. SwiftUI itself will automatically ensure, that it merges with existing list, and all additions and removals will be automatically recognized.

    In order to work, this mechanism needs to recognize which views are the same in existing and new structures. This is connected to the concept of identity, well explained in the WDC video. Again briefly, it is tightly connected to id of List items, and .id() modifier of views. If a newly created List contains items with some new ids, these items are considered to be added. Items with same ids are considered to by just modified. If new List misses some old ids, they are considered to be removed.