Yep, I did something very similar following this strategy:
Get the relevant source (fragment) for the starting type (e.g. ObservationRegistrar) and put it into the app.
Rename it to "MyObservationRegistrar" (and keep it renamed when bringing new sources in).
compile (using the old compiler!) - compilation fails because of dependent types / functions
bring them one by one using the same approach, starting from (1).
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
I understand why they canāt backport the SwiftUI specific observation features, but why canāt they backport withObservationTracking and the @Observable macro?
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.
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.