How to approach updating UI for changes within objects held within a collection? After experimenting a bit, I think there are basically two approaches.
- The conformance to
BindableObject
by the object that contains the collection needs to be deep - not shallow. It needs to fire didChange if the collection, or anything recursively within the collection, changes. or - The parent/container can have shallow conformance to
BindableObject
if the row model themselves conform toBindableObject
and the row view declares the dependency with@ObjectBinding
.
You must do one or the other if you want a row to refresh when isTyping
changes value. I suspect that in the general case, #2 will be simpler.
This code is an example of #2. Imagine a collection of chat groups. Each chat group might have a property isTyping
which represents if someone in the group is currently typing. The row wants to be updated when isTyping
changes value, but the collection of rows does not change.
import SwiftUI
import Combine
struct ContentView : View {
@State var viewModel: ViewModel // Doesn't need to State. Could be ObjectBinding, Evironment, etc.
var body: some View {
List(viewModel.rows) { Row(rowModel: $0) }
}
}
struct Row : View {
@ObjectBinding var rowModel: RowModel
var body: some View {
HStack {
Text(rowModel.name)
Spacer()
Text(rowModel.isTyping ? "Yes" : "No")
}
}
}
class ViewModel: BindableObject {
let didChange = PassthroughSubject<Void, Never>()
init() {
rows = [RowModel].init(arrayLiteral: RowModel(), RowModel(), RowModel(), RowModel(), RowModel())
makeABunchOfChanges()
}
var rows: [RowModel] { didSet { didChange.send(()) } }
private func makeABunchOfChanges() {
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { x in
if self.rows.count < 10 {
self.rows.insert(RowModel(), at: Int.random(in: 0..<self.rows.count))
}
}
Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { x in
if self.rows.count > 8 {
self.rows.remove(at: Int.random(in: 0..<self.rows.count))
}
}
}
}
class RowModel: Identifiable, BindableObject {
let didChange = PassthroughSubject<Void, Never>()
let id: String = UUID().uuidString
let name: String
var isTyping: Bool { didSet { didChange.send(()) } }
private static var nextNumber: Int = 0
init() {
isTyping = false
name = "Row \(RowModel.nextNumber)"
RowModel.nextNumber += 1
makeABunchOfChanges()
}
private func makeABunchOfChanges() {
Timer.scheduledTimer(withTimeInterval: 0.7, repeats: true) { x in
guard Int.random(in: 0..<3) == 0 else { return }
self.isTyping = !self.isTyping
}
}
}