[Pitch] Observation

A-ha, I think I see better what you mean now. So while compile-generated conformance could only work for reference types, the observation machinery could allow reference-like value types to be conformed to Observable as well, provided that the (library) developer does the work.

1 Like

Right, it's a similar story compared how many AsyncSequence types are implemented. They are often baked with an object under the parent struct. I mean, we're just observing for a change so that we can react on the observers side. We're not manipulating the Observable, hence there's no ReferenceKeyPath type required (which doesn't even exist). All we want is to look into something that we can identify by an identity, but there's not much won forcing comparison through === than via lhs.id == rhs.id unless I'm missing some significant important implementation details behind all this. I would love to make MyRecord to conform to Observable and expose that to SwiftUI without workarounds via willChange publisher.

And to round everything up, this pitch even dropped the Object suffix. While I know that we're trying to avoid a name collision here, but we could also interpret this as a transition from ObservableObject: AnyObject to Observable: Identifiable.

1 Like

That is subjective and makes no difference to my points.

I think the point being made is that Combine is not available on non-Darwin platform, while observation in itself is a fairly useful concept irrespective of a platform it's applied on.

6 Likes

Ah, I suspected as much by now.

Then, consequently, the impetus should be making Combine (potentially better and) cross-platform. I don't see the technical limitation there. Also, Foundation is going open-source and cross-platform and depends on Combine anyway ... all that should play into the same direction, no?

Even if Combine were already open source and cross platform, moving away from it to a native Swift solution would be the obvious and desirable path. And even if we built a concurrency wrapper around Combine, we'd still want to move away from it to a fully native version, if only for the performance benefits. So I don't think it'd ever be desirable to keep Combine in this role.

And there's no Foundation dependency on Combine generally. What are you referring to?

4 Likes

This has strayed a touch off topic, but there have been some interesting questions that have been brought up (both here and in direct messages to me) that are worth some re-evaluating some of this pitch.

  1. How will the application of type wrappers inter-play? Do macros make more sense as a solution?
    Provisionally I think that with declaration macros we might have a more apt solution)

  2. How can we handle observations of computed properties?
    Macros have some hope for making this more of a reality without needing additional support as often; lessening the ramp of the progressive disclosure to more advanced topics

  3. How can we handle observation of sets of properties?
    This seems like something that can be done via field sets. I have some local drafts to do precisely this, however it changes the observer to no longer be type level generic upon the member but function level generic on the member.

  4. Can actors or even structures participate in observation?
    Type wrappers don't really support this, but macros seem to have a path forward for this. Which to me signals this is perhaps the right move.

  5. Do we need to have both leading and trailing edge (i.e. both will and did set events)? Can it be reasonably expressed as one singular event?
    As it stands we only need the leading edge (willSet). Perhaps it would be helpful if folks that have distinct need for didSet side changes to offer some concrete examples of usages of why observing on that side of the signal is useful (and ONLY solvable by didSet side observation).

  6. Can change events be triggered for dependent key paths? e.g. the key paths effecting key paths APIs
    This seems to be possible. It might need some alteration to the ObservationList API.

  7. Do observers need to be specifically generic upon the member values? or can they be function level generic on the change and only type level generic upon the observable they are about?
    Yes; see item 3 for details.

Who is "we" here? SwiftUI? Other than SwiftUI the vast majority of uses would be for didSet, as users may access properties which they expect to be updated. Unless this feature can prevent those accesses, didSet seems necessary for the most common direct usage I'd expect out of this feature.

7 Likes

I second this. While reading the Q&As I immediately felt the same way. It's SwiftUI that wants and needs willSet events. Back before SwiftUI, like one or two years before, the re was a SwiftUI-like concept shared during the WWDC and dubbed as "layout driven UI" which used didSet events to respond to changes in UIKit.

Is that the answer to my question regrading AnyObject and Identifiable? If it is, could you please expand on this as I have no clue how and why macros would be needed here at all.


Can ObservationTracking.Options.compareEquality solve a very common problem of observation like discarding duplicate values?

The "we" is a "everyone reading this" - since the primary use case was outlined as SwiftUI it is a primary driving factor here. My query was really to give a prompt of asking for concrete use cases to advocate that. Personally I feel that the symmetry of will and did makes sense. Since we need to support will sides of things for SwiftUI that clearly needs to exist; but there was feedback given (out of band of the forums) that having both was superfluous (which that argument has some merit). My arguments for the behavior including both leading and trailing edge did not land nearly as heavy hitting as feedback from the community.

You brought up a good point that spurred me to consider more about the restrictions about just classes. It seemed unnecessarily strict. Type wrappers have some design limitations that macros do not, effectively macros can do whatever you could write by hand, type wrappers however must be limited to certain limitations around what the compiler can determine from the type itself. Effectively for the general concept of exclusivity it only allows reference types to play in the passing of the wrapped self for the automatic systhesis portion of the observation feature. This restricts it to things that are AnyObject. Furthermore since type wrappers cannot have foreign isolation (e.g. isolation driven by an instance of an actor to a type that isnt an actor) the concept of actors is kinda off the table too. These two restrictions on the type led me down the path that for the default implementation portion of the types adopting Observable should perhaps use macros instead of type wrappers.

Yup! That is a thorn in the side of ObservableObject and sadly not one that is fixable (due to needs of supporting existing applications/behaviors and also some of the limitations of the design). By default the Observable support for SwiftUI will not update if there is a duplicate value assigned. e.g. setting a string property twice to "foo" will only cause 1 update.

Can you share that feedback? Having encountered the problem of willSet making @Published less useful, and see others encounter the same, such a limitation certainly seems ill advised. And as a general feature of Swift, it seems like we should err on the side of more flexibility, not less.

2 Likes

I'm not 100% sure I follow here as I couldn't play around with the current state of the project in any way. Are you still going to publish a package to toy around with, even if it's not based on macros yet?

Are you saying that it's not fixable for ObservableObject or is it not fixable for the proposed Observable? If this is going to be a general feature, I don't think we should force its defaults to align with the primary consumer such as SwiftUI. Instead it should provide the necessary tools for the consumer to opt-into the behavior it needs, but generally provide a greater and more common default. While filtering out duplicates is great, it should be opt-in not opt-out as we often want to catch every change, but only on a case by case basis to filter out duplicates (e.g. Void signals should not discard duplicates).

I apologize in advance in case I somehow misunderstood your point here.

1 Like

Edit: I think Foundation offers some observable API as Combine publishers. And its 1st import is Combine – if that means anything.

On that note: Would or could Foundation then migrate to this new and more native observer mechanism?

1 Like

I'm beginning to understand the necessity for a second solution, I'm just seeing all this also from the perspective of a newbie to the Swift world. How would I explain there are two different and relatively new observer mechanisms here? It's a bit unfortunate. With common sense (unbiased by expertise) you would expect that Combine will grow to serve also these new demands, that it will go open-source and x-platform, move to the standard library or expand and evolve in whatever way necessary ... I wish it was just named "SwiftUI View Model Observability Kit" haha :sweat_smile:

But surely @State vars are only created once, and then managed by SwiftUI, independently of the lifetime of the ContentView? Isn't that the point?

No StateObject has a closure that it uses to generate the object once and lazily, on every re-creation of the view there is no new object spawned. State will spawn new value every single time the view is instantiated, throw it away when re-injecting the last value that corresponds the view‘s lifetime. Put a very heavy value into State and you will smash your performance every time that view needs to be instantiated.

2 Likes

There was a fair discussion of the problems people had with the willSet semantics of @Published in this thread.

That is a known issue and one in-which is definitely under consideration by the SwiftUI team to adjust in light of Observable. To how they will change it to account for it? That I don't know yet.

1 Like

In general, very happy to see progress. There have been some great points already brought up so I won't reiterate them. However, I am curious how the ObservedChanges sequence is going to work. Is this going to be a unicast AsyncSequence or a multicast one? Furthermore, is the iterator going to retain the object that gets observed?

ObservedChanges is intended to be unicast. The ObservedChanges and ObservedChanges.Iterator will only hold the Observable weakly. However if we decide to relax the requirements from AnyObject to include structures too that may have to change. Sadly it must have a way to remove the observer in case of cancellation; so we need some sort of back reference.