Reputation: 40599
I have found what I think is a bug in @ObjectBinding. For cases when a @BindableObject get its didChange.send() called asynchronously, sometimes views that depend on that change, do not get invalidated, so their bodies are never requested again, and their look is not updated.
The rows in the List
view should update every second. At first it works fine. However, when new rows are added with the '+' button, things begin to go sideways.
I made the rows tappable, so than when they are tapped, a @State variable changes to alter the color of text. Because this binding is detected properly, the view is invalidated and its look changes. This shows that the timer was actually running, even though the view did not show it until it was tapped.
Note that the only purpose of the code below is to demonstrate this odd behaviour. I'm not looking for a workaround. With this question I am aiming to confirm if this is a bug, and if not, what I am doing wrong.
In some other cases, replacing @ObjectBinding by @EnvironmentObject fixes it. In this case though, it only improves a little.
I came across this issue when answering this other question: SwiftUI and Combine not working smoothly when downloading image
Update: By the way, if @ObjectBinding is replaced by @State things go smoothly.
import SwiftUI
import Combine
struct Row: Equatable {
let id: Int
let name: String
}
struct ContentView : View {
@State private var rows: [Row] = [Row(id: 0, name: "Row #0"), Row(id: 1, name: "Row #1")]
var body: some View {
NavigationView {
List(rows.identified(by: \.id)) { row in
RowView(row: row, rowData: RowData())
}
.navigationBarItems(trailing:
Button(action: {
self.addRow()
}, label: {
Text("+").font(.system(size: 36.0))
}))
}
}
func addRow() {
rows.append(Row(id: rows.count, name: "Row #\(rows.count)"))
}
}
struct RowView: View {
let row: Row
@ObjectBinding var rowData: RowData
@State private var showBlue = false
var body: some View {
Text("\(row.name) - Seconds \(rowData.value)").foregroundColor(showBlue ? Color.blue : Color.primary)
.tapAction {
self.showBlue.toggle()
}
}
}
class RowData: BindableObject {
var didChange = PassthroughSubject<Void, Never>()
var value: Int = 1 {
didSet { didChange.send() }
}
init() {
update()
}
func update() {
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) {
self.value += 1
self.update()
}
}
}
Upvotes: 0
Views: 884
Reputation: 166
You need to replace your didChange with let objectWillChange = ObservableObjectPublisher()
as ObservableObject implicitly already implements a PassthroughtSubject.
Also, Apple docs recommends now to use @Published instead of the didSet: @Published var value: Int = 1
In your update function, replace self.update() with self.objectWillChange.send()
I found this link explaining how to trigger a view invalidation.
Upvotes: 1