Backporting Swift 5.9's observability

Yep, I did something very similar following this strategy:

  1. Get the relevant source (fragment) for the starting type (e.g. ObservationRegistrar) and put it into the app.
  2. Rename it to "MyObservationRegistrar" (and keep it renamed when bringing new sources in).
  3. compile (using the old compiler!) - compilation fails because of dependent types / functions
  4. bring them one by one using the same approach, starting from (1).
  5. repeat until you hit some bottom types / functions that are unavailable - if so either implement them properly or do some mocks.

In case of Observation the amount of sources was small, less then a dozen of files / types, so this process was quick.

I tried to leave everything in the original form. so struct -> struct, class -> class, enum -> enum, etc, just renamed with my prefix.

The next logical step would be having a wrapper (with another prefix) that would choose either the OS provided type / function or my type / function based on current OS version (#available check similar to this).

Right, five years from now, there would be no real benefit to having made Observable available before this release; there would be very real drawbacks to having replaced all the types with tuples. Weā€™re not designing a language for next year, weā€™re designing a language for the next, say, 40 years.

19 Likes

Could you be more specific about what drawbacks?
The only drawback I see is that ObservationRegistrar and ObservationTracking are public, so their APIs should be as clean as possible. However, as I mentioned, ObservationTracking isn't truly a struct. And both of these types could be left in the library as fancy wrappers around the quirky back-deployable API.
Users of Swift 5.9+ runtimes will be able to use the clean API, while users targeting prior releases would have the opportunity to use the back-deployable API.

1 Like

ObservationRegistrar gets inserted into your type by the macro, so it really should be Equatable, Hashable, Codable and Sendable so that doesn't block synthesis of those conformances for the type. While we believe that tuples should conform to these protocols, they don't currently.

That's the biggest issue for me, but even if/when we fix that, there's lots of other things that tuples can't do (conform to other protocols, have API other than free functions, have typealiases, allow new fields to be added while preserving ABI stability, ...) We don't know for sure that we'll want to do all of these things, but it's quite likely that we'll want to do some of them.

6 Likes

I don't see any conformances of ObservationRegistrar other than Sendable. I believe tuples are treated as Sendable when all their fields are Sendable. Am I wrong? It compiles fine.
Compiler Explorer

Ok, I got this, but you are describing limitations of tuples in general. I totally agree on this, but I'm solving a specific case - "Is it possible to backdeploy Observation?".

  • "conform to other protocols" - tuples in my solution don't need any conformances.
  • "have API other than free functions" - no dynamic dispatching needed so we are good here
  • "have typealiases" - I didn't get this one.
  • "allow new fields to be added while preserving ABI stability" - tuples themselves will not be part of the ABI. Functions that work on them can be marked with @_alwaysEmitIntoClient if needed.
1 Like

Iā€™m glad to hear weā€™re getting 40 more years of Swift usage in before weā€™re all forced to switch to the 100 year language.

11 Likes

I'd be glad to be wrong, but to me it feels more like ~4 years until we get yet another newer better and radically different version of observation machinery. Given that many of us have to support, say, 3 past OS versions (which of now is iOS14, iOS15, and iOS16) it might well be the case that once we finally could switch to the new thing ā€“ it's already obsolete and there's already a newer better thing we'd be able to switch to in another 4 years.

11 Likes

Discussing back-deploy ideas is good, but discussing real code is even better. So I rewrote the entire Observation runtime as a back-deploy version. In addition, I fixed several flaws in the original code and wrote a couple of sample projects. One is a cli app and the other is an example for SwiftUI.

26 Likes

Even SwiftUI working? Wow, I thought that's impossible.

It's possible with a wrapper view called AutoObservingView. It has a dynamic property built with @ObservedObject to send invalidation signals to the SwiftUI engine upon onChange event.

struct ContentView: View {
  var model: ContentViewModel

  var body: some View {
    AutoObservingView { // This one
      VStack {
        Image(systemName: "globe")
          .imageScale(.large)
          .foregroundStyle(.tint)
        Text("Hello, world!")
        Text("Counter: \(model.counter)")
        Button {
          model.counter += 1
        } label: {
          Text("Increment")
        }
      }
      .padding()
    }
  }
}

Making it work out of the box is much harder though. It will require an extension to the result builder API in the compiler like I described here. And some work has to be done on the SwiftUI side (I'm not sure if that's possible at all as view invalidation mechanism is very private).

7 Likes

Another idea occurred to me, and I rewrote everything once again. CollectionOfOne could serve as a nominal wrapper type that holds a tuple of properties. This offers two advantages:

  • We can now write extensions for these types, resulting in cleaner code.
  • We can conform to protocols. For instance, I have added conformances to Sendable, Equatable, Hashable and Codable to the ObservationRegistrar.

There are a couple of small things that should be addressed in the stdlib, but all in all, I think I've covered all reasonable drawbacks of the back-deploying.

What else would you like to improve? @Douglas_Gregor @scanon @Philippe_Hausler @Jumhyn

1 Like

A type can only conform to a protocol once. Conforming CollectionOfOne to any standard library protocol is source-breaking for users.

In any case, while back-deployability is desirable, I think itā€™s safe to say that we will not be limiting future additions to the standard library to tuples and free functionsā€”this is plainly not acceptable API design.

3 Likes

I'm very glad to have this option. I'd prefer it was available out of the box, but if that not possible then having this option is the next best thing.

Can you outline on the compatibility aspects: if your "substitutes" named the same way as the system functions won't there be a clash (with system code mistakenly calling your functions or vice versa)? Would a safer design be if your substitutes are named differently (e.g. with some suffix or prefix) to avoid any potential clash? Do or don't you think that on a newer OS the system version could/should be used somehow (e.g. via a method similar to this)?

I implemented this API in this way only to demonstrate that back-deployment is entirely possible. Therefore, I didn't pay much attention to naming. If the system implementation won't be back-deployed, there is no reason to keep things so cumbersome. In the event that this thread results in a "strict no", I will simply reimplement the entire API in a more supportable manner and publish it as a package (SPM/CocoaPods), which would serve as a drop-in replacement.
Naming is an open question. As you can see, I've suffixed the module name and macro names with "BD" (for Back-Deploy). However, if someone has better naming suggestions, I'm all ears.

This is completely up to you, but in my opinion, if a person only cares about iOS version 17 or higher, there is no point in using a custom implementation. On the other hand, if they care about iOS versions lower than 17, there is no point in using the system implementation.

There is nothing that the system implementation does that is impossible for a custom one.

For example, if you use it with SwiftUI and your target is less than iOS 17, you will just need to use a custom implementation and the AutoObservingView wrapper view. This renders internal support for the system Observable in SwiftUI useless. However, if you design a view for iOS 17 or higher, it is completely fine to use any implementation, and the system implementation may be preferable as it doesn't require the wrapper view.

6 Likes

I understand why they canā€™t backport the SwiftUI specific observation features, but why canā€™t they backport withObservationTracking and the @Observable macro?

The Observability framework is open source right? So we could just include it in our apps and only link to it on iOS 16 and below.

Then use the mechanism dmt showed above to use it with SwiftUI.

Itā€™s confusing that Observabilty isnt a separate framework but GitHub - apple/swift-collections: Commonly used data structures for Swift is separate which has some fundamentals like OrderedDictionary.

1 Like

That wouldn't be a horrible approach. It might even be plausible to create a package definition in the Observation directory. Some changes likely would need to be made - but that might be a good way to resolve that desire.

I think the major differential there is that swift-collections is not part of the toolchain (which consequently makes it part of the distributed SDK). That connection is what allows for components in the SDKs vended to utilize it.

The parts that would likely need to have some alterations would include defining the module name of ObservationMacros as something else (to avoid ambiguity), adding in a compiler extension section, limiting those changes to the macros to some sort of define that scoped the differentials out, and answering a question about how availability is spelled for those APIs. Plus perhaps some slightly different shims for the lock and thread local stuff.

5 Likes

Was this approach ever determined as feasible?

With the fruity Swift vendor releases coming soon, it'd be great to know if we could conditionally link Observation with apps based on earlier SDKs. The benefits to the community -- especially SwiftUI developers -- should be obvious.

1 Like

Idk what the point is by inventing such a language feature without support old iOS/macOS versions.

From community PoCs, it is not difficult at all https://github.com/onevcat/ObservationBP

At least it is much simpler than BP of Swift Concurrency.

Take your time to read the thread. Points from the community and points from the people behind the current implementation were discussed in details.