@Philippe_Hausler is the API available in the latest nightly-main toolchain?
The view in this case (when the model side would be using @Observable
instead of ObservableObject
) will only update once a second, not 100 times a second.
And it would be written as such:
@Observable final class Model {
var state = 0
var unrelated = 0
init() {
Timer.scheduledTimer(withTimeInterval: 1/100, repeats: true) { [self] _ in
unrelated += 1
}
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [self] _ in
state += 1
}
}
}
var bodyCount = 0
struct ContentView: View {
@State private var model = Model()
var body: some View {
bodyCount += 1
return VStack {
Text(String(model.state))
}
}
}
Hi @Philippe_Hausler,
In your example you mark the Model with the @State property wrapper. This differs from the similar example in the proposal text.
Will it need @State?
For SwiftUI @State
represents state that is owned by the view. Observable types can be owned by views or owned externally. If it is owned externally it could be just private var model: Model
etc.
Perfect! Thanks for the explanation!
This is a really clean api and much less confusing than StateObject vs ObservableObject vs State.
I love this proposal very much!
I wrote up some ideas that I had about observing changes to move-only struct instances, and I'm posting the link here not to insist that anyone give it more consideration than they care to, but just to make sure that its existence is visible here on this more prominent thread for anyone who is interested but would have missed it otherwise.
@Philippe_Hausler, thank you.
I want to understand the granularity of this. For example If it was:
@Observable final class Model {
struct S {
var related = 0
var unrelated = 0
}
var state = S()
}
...
struct ContentView: View {
@State private var model = Model()
var body: some View {
bodyCount += 1
return VStack {
Text(String(model.state.related))
}
}
}
would the body be recalculated on state.unrelated
changes or only on state.related
changes?
Secondly, if I set state.related
variable to the same value, would it cause body recalculation or not?
I'm excited to see proposal developing! I'd like to revisit this assertion in the proposal:
In reality, having non-observed fields in a type that is observable is uncommon.
This might often be true for UI code, but from the perspective of someone working at the model and controller API level of a large-scale application, this has not been true. I'd strongly encourage you to revise this proposal to provide observability on a per-property basis, but provide a type-level convenience for marking everything observable where desired.
There are three big problems I see with the all-or-none approach:
-
It is valuable for a protocol to be able to declare an observability requirement on a property it introduces. We have widespread use of this with KVO (albeit not compiler-enforced) in the application I work on. If I understand correctly, this would not be possible in the current proposal unless your protocol adds a conformance requirement for full
ObservableObject
. This is not an appropriate requirement for most protocols to impose. -
API should be carefully designed to not expose or encourage unintended (and untested) usage patterns. It's important to provide thoughtful observability and notification patterns in your API, and to lead clients to watch for the right changes in the right way at the right time; but the current design makes that hard. Compare this to access control: Swift doesn't default a type's properties to public just because the type is public; you need to be intentional about what you expose.
-
Observability correctness is not free; e.g. as this proposal discusses, sometimes it requires you to add explicit transactionality in your implementation. By making all properties observable you now need to implement correctness even for those which didn't really need observability; and you may forget to do that. In contrast, having per-property observability annotations should lead you to thoughtfully validate and implement correctness for each observable property you expose.
I have to agree based on my experience developing a very large Swift library. For view-models, sure many of the properties are observable. For models and more of the types that have to do with business logic, we have some properties that need to be observable and many that do not.
Is it just a problem of the first value being missed? Could it be that you miss a few values by the time your task runs that starts an observation?
There doesn't seem to be any mention of dynamic member lookup in the proposal. Is it possible to observe a property through the dynamic member subscript?
Also, just want to echo previous clamor for instructions on getting this working on a toolchain. Seems like folks aren't having much luck?
I noticed that this proposal looks as though it will be leveraging AsyncStreams.
One of the current limitations of AsyncStream is that you can't have multiple consumers of a single stream. This is a very common use case when using combine.
For example, let's say you have a ViewModel class that has a State property. Your view controller might want to observe changes to that state property and update appropriately. Depending on how complex the view is, it might make sense to observe that state property multiple times to split up update logic. Some parts of your view might want to debounce changes, whereas others might not. In this case, the easiest solution would be to have separate consumers of those updates.
Will anything be done to address this limitation of AsyncSequences going forward? There is a few existing issues relating to this on the async-algorithms package, but they have been stagnant for quite some time. Multi-consumption of an `AsyncSequence` · Issue #110 · apple/swift-async-algorithms · GitHub for example
This is an interesting design direction to discuss in another thread, but this really seems off-topic for this review.
Just create multiple observations instead of trying to share an AsyncStream
.
I agree: a per-property annotation is a requirement for me.
ah so you will be able to create multiple observations on a single observable? Thats good.
I disagree; if AsyncStream is an important part of this proposal’s API surface, then AsyncStream limitations become important limitations of this API as well.
I also echo very strongly the @rballard has expressed about “if anything is observable, everything is observable.” I almost always wind up with internal-use properties on my classes that should not be observed.
If Swift didn’t insist on conflating instance variables and properties and only properties were observable this would be more reasonable, but because Swift has this conflation it actually feels less reasonable now than it would for, say, ObjC specifying that all properties are observable.
I do quite like the way it keeps the simple case simple though. If there was a way to say @Observable(.all)
to have all properties observable, or @Observable(.annotated)
to have only specially-annotated properties observable, that’d be wonderful. (Pretty much regardless of the exact spelling of how that can be made to happen.)
There has to be a limit here. Many other APIs often form part of the surface of another proposal. If for example you had an idea about how Collection
should be changed, this does not mean it's appropriate to raise it on a proposal for a new Collection
algorithm, unless the issue with collection is specifically entangled with how that algorithm works – even if your thoughts on how Collection
should change are insightful and important. In almost all cases, and I think in this case with the point re AsyncSequence
, that isn't the case.
Last minute question: what happens when mixing @Observable
with Codable
? My supposition is it will prevent automatically generated conformances from happening. Is this handled by the macro?
Overall an impressive proposal. Reviewers thus far have made super insightful comments on need-to-have functionality, so just a question or two and a few scattered comments on more superficial API design points:
Certainly, Swift has avoided making the distinction between stored and computed properties affect end users' code, but is it actually the case that for any final class C { ... }
, adding @Observable
really has no ABI-breaking effect?
Agree with @tera above about the concern regarding non-observability of superclass properties without @michelf's explicit workaround. While the limitation is understandable, I wonder if there's anything that can be done (even if it requires some ad-hoc internal APIs bolted into macros) to at least detect that there is a superclass with possibly non-observed properties and to emit a silenceable diagnostic warning.
It would be irrelevant to the end user in the simplest case thanks to macros, but have you considered enriching the requirements of Observable
and providing default implementations of these methods? They don't really seem to afford much leeway for manual customization, and perhaps such a design would simplify code generation routines for the macro—with accompanying binary size improvements for the end user maybe?
I can understand the impulse for making _$observationRegistrar
internal as an "implementation detail" of the observed type, but it doesn't seem like there's any functionality exposed there that a user who can user the observed type shouldn't be able to access which would preclude making this a protocol requirement. Just as property wrappers expose _property
and $property
, it seems fine to do something like that here.
I agree with @mpangburn regarding the design of ObservationTracking.withTracking
: as far as the end user can see, ObservationTracking
would only be a namespace (in which case, we've standardized on the uninhabited enum as the expression of a namespace). That there is library-internal functionality which perhaps justifies its struct
-ness seems an odd reason to have an externally exposed "rump" type.
Moreover, in its fullest invocation, the API is named: Observation.ObservationTracking.withTracking
, a schesis onomaton fitting if straight out of the Department of Redundancy Department. Seems the API could benefit from being simply withObservationTracking
, if it can't take a page out of the concurrency design with something like Observation.tracking
à la Task.async
.
(Whether internal or public, incidentally, ObservationTracking
would be more consistent with API naming guidelines for a protocol; a concrete type would be more fittingly something like ObservationTracker
.)