[Pitch] Observation (Revised)

Properties can change on any task/actor - the onChange must be able to be run in that actor context so the closure then must be Sendable.

1 Like

Can they change on a task/actor different from the call to withTracking()? Won’t it be a data race?

1 Like

yes they could; but it is the responsibility of those types to provide thread safety for those changes.
The observation machinery itself is thread safe but the types that store data must bring their own synchronization systems.

With changes(for:) and values(for:) no longer in the proposal, how are we to observe an @Observable from something that is not a SwiftUI view - something that may itself be Observable.

My use case may not be common, but when using Combine, I often broke the pipelines into smaller pieces and various structs would subscribe to upstream publishers and modify or join them before republishing them. In this way we could create small pieces that did one thing that could be appropriately named and flexibly assembled. I was thinking we could do the same with @Observables upstream that we might assemble with things from async algorithms etc.

Then again, I may be trying to hold it wrong.

Thanks,

Daniel

1 Like

Is there replacement for @AppStorage?

1 Like

Yup. This is a pattern I would use frequently, too. Especially important across module boundaries or for limiting view refreshes.

The two recommendations are to a) use computed properties, or b) use this pattern:

However, method A suffers from unnecessary view refreshes, while method B suffers from the async hop (which is one of the reasons async sequences were dropped) which will cause broken view invariants/ non synchronised view updates. I wrote a bit more about that here.

Yeah I am using a but there are times when I’d prefer b but as you note it’s always one step behind - like using observableobjects with uikit instead of SwiftUI.

I’ve also experimented with filling an async stream from b but again it is always one element behind

Unfortunately, this is a limitation of using asynchronous sequences and I'm not sure if there's any elegant way around it. I tried to go into it a bit in the other thread.

To make this work how we want it, we need Observable to generate trailing edge (didSet) synchronous notifications.

SwiftUI works by creating an animation transaction which includes a before changes snapshot and an after changes snapshot. And critically it all needs to happen in the same event loop cycle – so no actor hops or async calls.

As it stands, SwiftUI can create its 'before changes' snapshot when it's triggered by the first leading edge willSet notification of the event loop.

However, in order for dependent observers to make any changes they need to make, we need trailing edge didSet observations to occur before the end of the current event loop cycle. Trailing edge, because we need to know what the changes are in order that we can respond to them appropriately.

Finally, once the event loop cycle has concluded, SwiftUI can generate its 'after changes' snapshot and everything works as expected.

I don't really see any other way it could work.

6 Likes

I've missed the wagon on this, but seems like the Observable macro is conflicting with RxSwift's almost 6-year old Observable type (which is a common industry standard under Reactive Extensions).

Seems to me this could have been taken into consideration, knowing this will cause this issue:

import SwiftUI
import RxSwift

struct ContentView: View {
    var body: some View {
        Color.black
    }

    func observable() -> Observable<Int> { // Protocol 'Observable' does not have primary associated types that can be constrained
        .just(1)
    }
}

(Name Conflict from Xcode 15 · Issue #2532 · ReactiveX/RxSwift · GitHub)

I'm not sure what is a reasonable solution here aside for asking developers to set-up a manual typealias on each module they own. There are tens of thousands of apps using RxSwift and this basically makes all of them non-compilable under Xcode 15 beta 2.

The issue is with the new re-exporting of Observation, which wasn't an issue in beta 1.

I'd appreciate your guidance, and if you prefer this to be a new thread, I'm happy to do so.

1 Like

It was a consideration and addressed by [Observation] Ensure type access is qualified to the module name by phausler · Pull Request #66367 · apple/swift · GitHub which hopefully should address that concern.

That's great. So I'm guessing this will be a beta 3 thing that just didn't make the cut to beta 2 ?
Is there any way to test this ahead to confirm it will work?

We're actually part of the Swift Compatibility Suite, so thought this change would run against it:

Thank you !

The source compat suite cannot flag this, because you don’t import the new module. Both have to be imported for there to be an issue.

1 Like

Hey Philippe,
I've just downloaded the latest nightly snapshot of 5.9 and the problem still persists. Am I doing something wrong here?

image

image

1 Like

In this case one way to resolve this is to add a typealias Observable = RxSwift.Observable. The qualified namespaces were added to the macro emission so that if you do have a conflict you can actually still use both via type aliases.

1 Like

That's not really a super great solution (or a real solution in this case), because it means that every app that has RxSwift at the moment will not build.

It also means that every app that has more than one module (for example in our case almost 100 modules), will have to typealias RxSwift.Observable everywhere.

I'm sure there's a more reasonable solution to this:

  1. Is there any way to go back to Observation not being re-exported by SwiftUI? Explicitly importing Observation sounds like a reasonable thing when you need Observable.
  2. If not - perhaps it's possible to disambiguate, at the compiler level, a protocol Observable (from Observability) that has no associated type, vs. the RxSwift.Observable type which is always generic over some Element ?

Appreciate your help,
Shai

Really it sounds like Swift needs to generalize the feature added for Result where local symbols take precedence over Swift standard library symbols. I thought it was a general feature but it appears to only apply to the standard library. We ran into this with Regex as well. I don't know how the mechanism works now (I think @Douglas_Gregor wrote it), but I wonder if the standard library exports those frameworks if the alternate behavior will take effect.

3 Likes

Swift's form of name lookup means that introducing a new top-level public symbol is a breaking change that should require a semver major version bump and such, but obviously no one has been treating it like that. The standard library hack worked around the problem for specifically that, but this is an example of how it's not a problem specific to the standard library.

One solution might be to generalize the standard library hack to something like @introducedAt("2023-06-01") and if name lookup is ambiguous between symbols from two different libraries it picks the older one.

Another would be to go back to obj-c style name prefixes since Swift didn't actually remove the need for them.

I don’t see how treating every all additive API changes as breaking changes does anything to help the client who encounters a name collision when they upgrade.

What about availability pre-iOS 17 ?!

Ben addressed this in the review thread:

Regarding the topic of back deployment: whether or not a part of the Swift standard library will be back deployed is a decision for the platform vendor, not something that is part of the evolution review. It won't be decided here in this thread.

1 Like