Skip to main content

Module observable

Module observable 

Source
Expand description

@Observable from Rust — a model SwiftUI re-renders on.

The last thing on SwiftUI’s surface this crate could not build, and the reason it looked impossible was wrong. The obvious blockers are not blockers: Observation.Observable is a marker protocol with zero requirements, and ObservationRegistrar and withObservationTracking are fully exported. What stood in the way was in the middle — an @Observable model is a class, and this crate mints structs.

It does not have to be a Swift class. Observable requires AnyObject, and an NSObject subclass is one; crate::objc::ClassBuilder mints those. The only piece missing was a conformance record whose type reference is a class rather than a nominal descriptor — IndirectObjCClass, one of the four kinds the format already has, which crate::conformance::Conformer now emits.

let model = observable::model(0);
let watched = model.clone();
// Whatever the block reads is what the handler is told about.
observable::track(move || { watched.get(); }, || println!("it changed"));
model.set(1);

§What an @Observable property actually is

Three calls around ordinary storage, which the macro writes for you:

get { registrar.access(self, keyPath: \.value); return storage }
set { registrar.willSet(self, keyPath: \.value)
      storage = newValue
      registrar.didSet(self, keyPath: \.value) }

Property::get and Property::set are that, with the storage on the Rust side because a class minted through the Objective-C runtime has no ivars.

§Several properties

A model has as many named properties as it is asked for, each its own type:

let model = observable::model(0);
let name = model.property("name", "Ada".to_string());
let ratio = model.property("ratio", 0.5f64);

What makes them independent is that the key path is an identity — what access records and didSet matches against — so each property needs one of its own, and a view that read only name is not rebuilt when ratio changes. Key paths are per name, not per model: \.name is one key path in Swift too, and the registrar keys its observations by (subject, keyPath), so two models with a name property still change independently.

Structs§

Model
An observable model: a class instance, a registrar, and its named properties.
Property
One named property of a Model: storage, and the key path that names it.

Functions§

environment_model
.environment(model) — put the model where every view below can read it.
focused_value_model
.focusedValue(model) — publish the model for the commands above it.
model
A new observable model, with one Int property holding initial.
object
A new observable model with no properties yet.
track
Run read, remembering every observable property it touches, and call on_change once when any of them next changes.