Hundley
Hundley

Reputation: 3587

SwiftUI @StateObject inside List rows

SwiftUI doesn't seem to persist @StateObjects for list rows, when the row is embedded inside a container like a stack or NavigationLink. Here's an example:

class MyObject: ObservableObject {
    init() { print("INIT") }
}

struct ListView: View {
    var body: some View {
        List(0..<40) { _ in
            NavigationLink(destination: Text("Dest")) {
                ListRow()
            }
        }
    }
}

struct ListRow: View {
    @StateObject var obj = MyObject()
    var body: some View {
        Text("Row")
    }
}

As you scroll down the list, you see "INIT" logged for each new row that appears. But scroll back up, and you see "INIT" logged again for every row - even though they've already appeared.

Now remove the NavigationLink:

List(0..<40) { _ in
    ListRow()
}

and the @StateObject behaves as expected: exactly one "INIT" for every row, with no repeats. The ObservableObject is persisted across view refreshes.

What rules does SwiftUI follow when persisting @StateObjects? In this example MyObject might be storing important state information or downloading remote assets - so how do we ensure it only happens once for each row (when combined with NavigationLink, etc)?

Upvotes: 3

Views: 1099

Answers (1)

Asperi
Asperi

Reputation: 258413

Here is what documentation says about StateObject:

///     @StateObject var model = DataModel()
///
/// SwiftUI creates a new instance of the object only once for each instance of
/// the structure that declares the object. 

and List really does not create new instance of row, but reuses created before and went offscreen. However NavigationLink creates new instance for label every time, so you see this.

Possible solution for your case is to move NavigationLink inside ListRow:

struct ListView: View {
    var body: some View {
        List(0..<40) { _ in
            ListRow()
        }
    }
}

and

struct ListRow: View {
    @StateObject var obj = MyObject()
    var body: some View {
       NavigationLink(destination: Text("Dest")) {     // << here !!
          Text("Row")
       }
    }
}

You can even separate them if, say, you want to reuse ListRow somewhere without navigation

struct LinkListRow: View {
    @StateObject var obj = MyObject()
    var body: some View {
       NavigationLink(destination: Text("Dest")) {
          ListRow(obj: obj)
       }
    }
}

Upvotes: 3

Related Questions