Thanks for getting back β and sure thing, I can outline my full example here. So I made an test view like this: β
@Observable
class Person {
var firstName: String
var lastName: String
init(firstName: String, lastName: String) {
self.firstName = firstName
self.lastName = lastName
}
}
struct ContentView: View {
@State private var person = Person(firstName: "John", lastName: "Appleseed")
@State private var names: Observed<String, Never>? = nil
let firstNames = ["John", "James", "Tim", "Bob", "Constantinople"]
let surnames = ["Appleseed", "Smith", "Bobbins"]
@State var count = 1
var body: some View {
VStack {
Button("Change name") {
Task {
count += 1
person.firstName = firstNames[count % firstNames.count]
await Task.yield() // sequence breaks if this is included
person.lastName = surnames[count % surnames.count]
}
}
}
.padding()
.task {
if names == nil {
names = Observed { [weak person] in
guard let person = person else { return nil }
return person.firstName + " " + person.lastName
}
}
if let names = names {
for await name in names {
print("Name changed to '\(name)'")
}
print("Sequence ended.")
}
}
}
}
Now without the yield, everything works as expected, the names are printed without tearing and all is good. When you add the yield, I expected to get multiple outputs and see inconsistent names like you say, but for it to keep working never the less. What actually happens β most of the time β is that you see one torn value eg with just the first name changed, but after that the sequence stops emitting. The sequence hasn't ended, the guard let person
has not returned nil, but the sequence never emits again.
I think in a real situation this needs to be considered. It would be easy to write code like β
Task {
person.firstName = await namesGetter.firstName(count: count)
person.lastName = await namesGetter.surname(count: count)
}
β and not realise that because of the await boundaries, your sequence is invalidated. Sure this code is incorrect from a transaction point of view and produces an inconsistent view. But the app would suddenly not react ever again and you'd have no way of knowing it happened. There needs to be resilience so that even if this operation is not transactional because it is across an await boundary, the sequence still emits even if it is multiple times, and also continues to emit in the future.
Note that Observe
might not even be being used in SwiftUI so there are many cases where this could accidentally happen.
I do think this is a bug β with print statements in the code to try to track it down, it happens less often and you have to click the button faster to see it. So I think it could be a race within the calls that set up the next continuation. It's quite possible I'm wrong of course!