I find myself using a derived class that I mark with the @Observable
macro. For every computed property of the base class I need to go through the following for the observability for this property to work:
class Base {
var size: CGSize { get set }
}
@Observable class Derived : Base {
override var size: CGSize {
set {
withMutation(keyPath: \.size) {
super.size = newValue
}
}
get {
access(keyPath: \.size)
return super.size
}
}
}
The pattern is expected but really, I have to do all this typing for every one of the dozen or whatever properties?
Am I doing this wrong? Is there a better/cleverer/more efficient way, like a one-liner per property? I do not control the base class (e.g., SKScene
from SpriteKit
).
I was thinking of trying a macro for that but honestly, I've never written one so not sure if it's worth investing the time and effort at this point. But if anyone knows of a different approach here that is better, please show it.
I think there is no other choice than macros. The simplest solution is to just write an accessor macro like this:
// declaration
@attached(accessor, names: named(set), named(get))
public macro TrackSuper() = #externalMacro(module: "...", type: "TrackSuper")
// implementation
enum TrackSuper: AccessorMacro {
static func expansion(of node: AttributeSyntax, providingAccessorsOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) throws -> [AccessorDeclSyntax] {
guard let name = declaration.as(VariableDeclSyntax.self)?.bindings.first?.pattern
.as(IdentifierPatternSyntax.self)?.identifier else {
return []
}
return [
"""
get {
access(keyPath: \\.\(name))
return super.\(name)
}
""",
"""
set {
withMutation(keyPath: \\.\(name)) {
super.\(name) = newValue
}
}
"""
]
}
}
@main
struct MyMacroPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
TrackSuper.self
]
}
Usage:
class Foo {
var size: Int = 0
}
@Observable
class Bar: Foo {
// the order here is important - @TrackSuper must go first
// this is perhaps due to a implementation detail of @Observable
@TrackSuper
@ObservationIgnored
override var size: Int
}
A second design is a member macro, automatically generating overrides of the properties. The user can specify the properties that they want to override when applying the macro.
// declaration
@attached(member, names: arbitrary)
public macro TrackSuper<Root: AnyObject, each T>(_ properties: repeat (KeyPath<Root, each T>, (each T).Type)) = #externalMacro(module: "...", type: "TrackSuper")
// implementation
enum TrackSuper: MemberMacro {
private struct PropertyNameAndType {
let name: TokenSyntax
let type: TokenSyntax
}
static func expansion(of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext) throws -> [DeclSyntax] {
guard let tupleExprs = node.arguments?.as(LabeledExprListSyntax.self)?.compactMap({ $0.expression.as(TupleExprSyntax.self) })
else {
return []
}
let namesAndTypes = tupleExprs.compactMap(tupleExpressionToNameAndType)
return namesAndTypes.map {
"""
override var \($0.name): \($0.type) {
get {
access(keyPath: \\.\($0.name))
return super.\($0.name)
}
set {
withMutation(keyPath: \\.\($0.name)) {
super.\($0.name) = newValue
}
}
}
"""
}
}
private static func tupleExpressionToNameAndType(_ expr: TupleExprSyntax) -> PropertyNameAndType? {
guard let keyPath = expr.elements.first?.expression.as(KeyPathExprSyntax.self),
case let .property(component) = keyPath.components.first?.component,
let type = expr.elements.dropFirst().first?.expression.as(MemberAccessExprSyntax.self)?.base,
let typeToken = type.as(DeclReferenceExprSyntax.self)
else {
return nil
}
return PropertyNameAndType(name: component.declName.baseName, type: typeToken.baseName)
}
}
@main
struct MyMacroPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
TrackSuper.self,
]
}
Usage:
class Foo {
var size = 0
var somethingElse = ""
}
// in this case the order doesn't matter
@TrackSuper(
(\Foo.size, Int.self),
(\.somethingElse, String.self)
/* and so on... */
)
@Observable
class Bar: Foo {
// the class body can be left empty
}
Note that you also need to specify the types of the properties, otherwise the macro doesn't know the type of the property to generate.