Enforce `@Observable` through a protocol

I have more or less the following code:

public struct SomeView: View {
    var valueUsed: Bool {
        injectedDependency.someObservedValue.boolValue
    }
    let injectedDependency: InjectedDependencyImplementing

    var body: some View {
        switch valueUsed {
            case true: Text("on")
            case false: Text("off")
        }
    }
}

protocol InjectedDependencyImplementing {
    var someObservedValue: SomethingWithABool
}

// @Observable is missing here
class Imp: InjectedDependencyImplementing {
    var someObservedValue: SomethingWithABool = .something
}

This code updates nothing on valueUsed until I add @Observable. But it's not hard enforceable through a protocol like its predecessor ObservableObject. Is there any way I can trigger a compile error when I forget to set it in my actual implementation or mock?

1 Like

I'm not sure if I can think of a great compile time test for that… it might be possible… but it is also worth considering that it might be totally legit for an engineer to "roll their own" Observation behavior without using Macro helpers. Even if the Observable macro was attached to the type… could the engineer not just make someObservedValue a computed property (which does not attach an observation helper)?

There is an Obsevable protocol that can be checked for conformance. This is how the environment modifier works.[1]

My guess is a more impactful set of tests would be runtime tests built on withObservationTracking to confirm that your production type will set the observable property. It's a little challenging because the "did set" version of Observable is AFAIK private (only used by SwiftUI)… this might make things complicated if you have concurrency and actors involved… but it might be easier to test the "did set" logic if everything is running on main. Good luck!


  1. environment(_:) | Apple Developer Documentation ↩︎

1 Like

If I take a look at what the Macro implements, I see a conformance to the Observation.Observable protocol, but since it's empty adding that to my protocol declaration it still happily compiles if I remove the macro from the class implementing:

protocol Foo: Observable { }

class Bar: Foo { } // Just compiles

Given the big difference in expected behavior and the general "magicness" of how it works, missing a @Observable in one tiny spot can be quite hard to spot.

It's actually possible to chain Observable classes: Munchee/Munchee/Order/EditOrder/EditOrderView.swift at main · LucasVanDongen/Munchee · GitHub

@Observable
final class EditOrderViewModel {
    var restaurantName: String { order.restaurant.name }
    var showsAddedProducts: Bool { !order.lines.isEmpty }
    var showsOrderTotal: Bool { !order.lines.isEmpty }

It will just pass forward everything from the Order as soon as it changes there. Pretty cool!

The @Observable macro adds an _$observationRegistrar stored property of type ObservationRegistrar, so you could always add that requirement to your protocol and then the application of @Observable would satisfy it:

import Observation
protocol FooProtocol: Observable {
  var _$observationRegistrar: ObservationRegistrar { get }
}
@Observable
class Foo: FooProtocol { }  // ✅
class Bar: FooProtocol { }  // 🛑

Of course this doesn't prove that the @Observable macro was applied, and there really isn't any way to do that. But it's an approximation.

2 Likes

I did notice that possibility, but in Xcode 15.4 I’m getting an error both on public and regular declared classes because it’s declared private.

Property '_$observationRegistrar' must be declared internal because it matches a requirement in internal protocol 'ForceObservable'

Edit:

So this approach works, but only for non-public classes:

protocol ForceObservable: Observable {
    nonisolated func access<Member>(
        keyPath: KeyPath<Test, Member>
    )
}

@Observable
class Test: ForceObservable {
}