Detailed @Observable through Swift Macros

Can Akyıldız
5 min readDec 16, 2023

--

Hello everyone!

Today I will be talking about Observation feature in SwiftUI that lets you define your models using @Observable which is built with new Swift Macros feature to have a reactive behavior for your swiftUI views to respond to changes in that model.

Introduction

Observation feature has very similar dynamics as ObservableObject. But instead of all the property wrappers and protocols, we are assigning macros and then never deal with @Published or some Object property wrapper.

Since @Observable is a macro, initially we will be looking at implementations behind declarations that wrap things up for us and clearing our models from writing @Published or Object wrappers in our views. Then we will be showing what's changed, comparing the whole thing with ObservableObject components and how we can use this new feature.

So let’s tap on @Observable and see what’s up!

Under the hood of things (A bit more advanced)

We have a few macros under the tree of @Observable macro. I will try to explain things on a high level as much as possible. Let me clear a better picture with some visuals.

Expanded state of @Observable Macro

So we will have this example struct called CarGallery. To cover macro side of things first, our end product would look like this on the left, then when you expand it, we would see the expansion on right side.

We could see a lot of thigns going on when macro is expanded, like ObservationTracked macros, ObservationIgnored registrar let decleration, non isolated functions, extension to Observable.. a lot of things!

Lets go by moving from the root and to surface level.

@attached(extension, conformances: Observable)

This conformance/extension macro is helpful with making our class conform to Observable protocol. Which is a type that emits changes upon receiving notifications. But important to note this quote from Apple docs:

Conforming to this protocol signals to other APIs that the type supports observation. However, applying the Observable protocol by itself to a type doesn’t add observation functionality to the type. Instead, always use the Observable() macro when adding observation support to a type.

If you are finding this new concept hard to understand after your learning days, I would highly recommend that you can consider it similar to ObservableObject conformance:

@attached(memberAttribute)

As far as memberAttribute, it helps with adding @ObservationTracked keyword/attribute when @Observable macro is expanded. Just attribute adding. It doesn’t create the functionality behind ObservationTracked, that would be done by Peer Macro which we will go over in a bit.

I would compare this to @Published attribute, they are most probably working under the same dynamics in my opinion:

@attached(member)

what @attached(member) does is that it creates members of _$observationRegistrar which is initializing ObservationRegistrar class,

which contains all of these methods above that we will be using down below.

Afterwards, it creates method of withMutation and access as you can see

Those are mainly helpful with observation and tracking changes by using ObservationRegistrar methods. When you make a change, under the hood they are updating our properties, working in coordination with Peer macro of ObservationTracked.

@attached(peer)

This is the whole functionality behind ObservationTracked attribute you see. Actually it’s ObservationTracked attribute itself but we are not able to apply this macro without Member Attribute macro’s help, which is because it will be the one who puts this macro on map by a little renaming.

So originally it’s built under the class of ObservationTrackedMacro struct, of type PeerMacro. I can only imagine the craziness and amount of code in its creation — because hell mate! PeerMacro is a badass.

If you are any more curios in macros, you could check my Swift Macros article/video to learn more about them.

@Observable In Practice

A good example would be showing how things are replaced. When we were dealing with ObservableObject, we were adding the protocol conformance, @Published property wrapper for each and every variable that we want to listen changes of and so on.

Now picture has changed to be like this above.

When using this CarGallery in our SwiftUI View, we have made a transition of no longer needing to use @ObservedObject, @StateObject but directly define with a let declaration like here.

It will automatically update our properties in the CarGallery model.

Array of Observables

Let’s also see how we can work with an array of @Observable marked objets and comparing with older version.

I would call this probably my favorite feature of the whole feature, because I literally hated creating another observable object to contain the list of items.

Binding

And looking at how @Binding was affected, we don’t see a lot of apparent changes, but here it is!

We can just add a @Bindable property wrapper and we will be fine!

Conclusion

That was pretty much it guys! I hope it was helpful to you and I’m wishing you a nice day! Happy coding

Goodbye.

--

--