swiftmacrosobservableproperty-wrapper

Can't use Property Wrappers on a class marked with @Observable


I have a class which is similar to:

final class Person {
    @Resettable var name = "John Doe"
    
    func revertName() {
        name = $name
    }
}

@propertyWrapper
struct Resettable<T> {
    var wrappedValue: T
    let projectedValue: T
    
    init(wrappedValue: T) {
        self.wrappedValue = wrappedValue
        self.projectedValue = wrappedValue
    }
}

However, when I annotate Person with the new @Observable macro I got the following error:

Property wrapper cannot be applied to a computed property

I'm assuming this happens because the @Observable macro adds a getter and setter to all properties in a class.

Is there any way I can use my @Resettable property wrapper and the @Observable macro at the same time?

Context: In my case it's class GameScene: SKScene and I use @Resettable to reset things like var runningSpeed and whatnot. The game scene must be marked with @Observable because the GameScene is modified using a SwiftUI view called GameSettingsView. This view uses @Bindable and @Observable to modify the game scene.


Solution

  • You are correct - @Observable turns the stored properties into computed properties, so you cannot put property wrappers on them.

    You can try turning your property wrapper also into a macro. For your Resettable, this is rather simple. You just generate a new property, prefixed with $ with the same initialiser.

    A simple implementation I wrote (I'm rather bad at using SwiftSyntax, so please excuse me)

    public enum Resettable: PeerMacro {
        public static func expansion(of node: AttributeSyntax, providingPeersOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) throws -> [DeclSyntax] {
            // only apply to vars
            guard let varDecl = declaration.as(VariableDeclSyntax.self),
                varDecl.bindingSpecifier.text == "var" else {
                return []
            }
            return varDecl.bindings.filter {
                // apply only to those with initialisers
                $0.initializer != nil && 
                // and also the name of the variable does not start with underscore
                !$0.pattern.as(IdentifierPatternSyntax.self)!.identifier
                    .text.starts(with: "_") 
            }.map {
                return "let $\($0.pattern) = \($0.initializer!.value)" as DeclSyntax
            }
        }
    }
    
    @main
    struct ResettablePlugin: CompilerPlugin {
        let providingMacros: [Macro.Type] = [Resettable.self]
    }
    
    // ...
    
    @attached(peer, names: prefixed(`$`))
    public macro Resettable() = #externalMacro(module: "SomeModule", type: "Resettable")
    

    Notes: