SwiftUI - StateObject in a row subview is initialised and deinit multiple times when scrolling

Viewed 299

I want to add a StateObject to a subview of a row displayed in a sectioned list in order to perform some work and update the subview when a @Published property from the StateObject is updated (e.g. download an image). The work should be performed when the subview appears (.onAppear { }).

The row is initialised once, and appears / disappears when scrolling. When adding items, the view is initialised again (not necessarily redrawn but that's normal if nothing changed). Similarly, the subview is initialised once, and appears / disappears when scrolling. When adding items, the view is initialised again (not necessarily redrawn but that's normal if nothing changed).

But the StateObject from the subview is initialised and deinit multiple times when I scroll. It should not as it's a StateObject and not an ObservedObject. Why is that happening?

enter image description here

When I add this StateObject to the row instead, the StateObject is correctly initialised once (even when adding new items to the list) and is never deinit.

enter image description here

Sample code provided below .

import SwiftUI

struct SectionTest: Identifiable {
    var id: String { title }
    var title: String
    var items = [Item]()
}

struct Item: Identifiable {
    var id: String { name }
    var name: String
    var section: String
}

class Loader: ObservableObject {
    private var name: String
    init(name: String) {
        self.name = name
        print("Loader • init \(name)")
    }
    
    func load() {
        print("Loader • load \(name)")
    }
    
    deinit {
        print("Loader • deinit \(name)")
    }
}

struct LoaderView: View {
    @StateObject private var loader: Loader
    let name: String
    
    init(name: String) {
        print("LoaderView • init \(name)")
        self.name = name
        self._loader = StateObject(wrappedValue: Loader(name: "LoaderView ".appending(name)))
    }
    
    var body: some View {
        Rectangle()
            .frame(height: 200)
            .onAppear { loader.load() }
            
            .onAppear {
                print("LoaderView • appear \(name)")
            }
            .onDisappear {
                print("LoaderView • disappear \(name)")
            }
    }
}

struct RowView: View {
    @StateObject private var loader: Loader
    let name: String
    
    init(name: String) {
        print("RowView • init \(name)")
        self.name = name
        self._loader = StateObject(wrappedValue: Loader(name: "RowView ".appending(name)))
    }
    
    var body: some View {
        VStack(alignment: .leading) {
            Text(name)
                .foregroundColor(.red)
            LoaderView(name: name)
        }
        .onAppear {
            print("RowView • appear \(name)")
        }
        .onDisappear {
            print("RowView • disappear \(name)")
        }
    }
}

struct StateObjectListView: View {
    
    @State private var items = [
        Item(name: UUID().uuidString, section: "First"),
        Item(name: UUID().uuidString, section: "First"),
        Item(name: UUID().uuidString, section: "First"),
        Item(name: UUID().uuidString, section: "Second"),
        Item(name: UUID().uuidString, section: "Second"),
        Item(name: UUID().uuidString, section: "Third"),
        Item(name: UUID().uuidString, section: "Third"),
        Item(name: UUID().uuidString, section: "Third")
    ]
    
    private var sections: [SectionTest] {
        Dictionary(grouping: items) { $0.section }
            .map { SectionTest(title: $0.key, items: $0.value.sorted { $0.name < $1.name }) }
            .sorted { $0.title < $1.title }
    }
    
    var body: some View {
        NavigationView {
            VStack {
                Button("Add") {
                    let section = ["First", "Second", "Third"].randomElement()!
                    items.append(Item(name: UUID().uuidString, section: section))
                }
                ScrollView {
                    LazyVStack {
                        ForEach(sections) { section in
                            Section(header: Text(section.title).font(.caption2)) {
                                ForEach(section.items) { item in
                                    RowView(name: item.name)
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
0 Answers
Related