From 4827461280f41017004d85f50ac3314a6c40210e Mon Sep 17 00:00:00 2001 From: Kevin Romero Peces-Barba Date: Sat, 14 Sep 2024 21:06:06 -0400 Subject: [PATCH 01/11] Implement @ObservableDefaults macro This addresses #142 by creating a new macro that can be used inside `@Observable` classes. The macro is implemented in a new `DefaultsMacros` module. The decision to do so is based on the introduction of a new dependency on `swift-syntax`, and to keep the main module dependency-free. --- Package.resolved | 15 +++ Package.swift | 31 +++++ .../DefaultsMacros/ObservableDefaults.swift | 10 ++ .../ObservableDefaultsMacro.swift | 117 ++++++++++++++++ .../ObservableDefaultsPlugin.swift | 9 ++ .../DefaultsMacrosTests.swift | 127 ++++++++++++++++++ 6 files changed, 309 insertions(+) create mode 100644 Package.resolved create mode 100644 Sources/DefaultsMacros/ObservableDefaults.swift create mode 100644 Sources/DefaultsMacrosDeclarations/ObservableDefaultsMacro.swift create mode 100644 Sources/DefaultsMacrosDeclarations/ObservableDefaultsPlugin.swift create mode 100644 Tests/DefaultsMacrosTests/DefaultsMacrosTests.swift diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..7f3ac9b --- /dev/null +++ b/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "1ba60ebd54db82e47e39bc8db179589187c069067eb0a8cd6ec19d2301c5abc4", + "pins" : [ + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax", + "state" : { + "revision" : "cb53fa1bd3219b0b23ded7dfdd3b2baff266fd25", + "version" : "600.0.0" + } + } + ], + "version" : 3 +} diff --git a/Package.swift b/Package.swift index fe3d4bf..f92aa0d 100644 --- a/Package.swift +++ b/Package.swift @@ -1,5 +1,6 @@ // swift-tools-version:5.10 import PackageDescription +import CompilerPluginSupport let package = Package( name: "Defaults", @@ -16,18 +17,48 @@ let package = Package( targets: [ "Defaults" ] + ), + .library( + name: "DefaultsMacros", + targets: [ + "DefaultsMacros" + ] ) ], + dependencies: [ + .package(url: "https://github.com/swiftlang/swift-syntax", from: "600.0.0") + ], targets: [ .target( name: "Defaults", resources: [.copy("PrivacyInfo.xcprivacy")] ), + .macro( + name: "DefaultsMacrosDeclarations", + dependencies: [ + "Defaults", + .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), + .product(name: "SwiftCompilerPlugin", package: "swift-syntax") + ] + ), + .target( + name: "DefaultsMacros", + dependencies: ["Defaults", "DefaultsMacrosDeclarations"] + ), .testTarget( name: "DefaultsTests", dependencies: [ "Defaults" ] + ), + .testTarget( + name: "DefaultsMacrosTests", + dependencies: [ + "DefaultsMacros", + "DefaultsMacrosDeclarations", + .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), + .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), + ] ) ] ) diff --git a/Sources/DefaultsMacros/ObservableDefaults.swift b/Sources/DefaultsMacros/ObservableDefaults.swift new file mode 100644 index 0000000..9abd37f --- /dev/null +++ b/Sources/DefaultsMacros/ObservableDefaults.swift @@ -0,0 +1,10 @@ +import Foundation + +import Defaults + +@attached(accessor, names: named(get), named(set)) +public macro ObservableDefaults(_ key: Defaults.Key) = + #externalMacro( + module: "DefaultsMacrosDeclarations", + type: "ObservableDefaultsMacro" + ) diff --git a/Sources/DefaultsMacrosDeclarations/ObservableDefaultsMacro.swift b/Sources/DefaultsMacrosDeclarations/ObservableDefaultsMacro.swift new file mode 100644 index 0000000..611dc0e --- /dev/null +++ b/Sources/DefaultsMacrosDeclarations/ObservableDefaultsMacro.swift @@ -0,0 +1,117 @@ +import SwiftCompilerPlugin +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +public struct ObservableDefaultsMacro: AccessorMacro { + public static func expansion( + of node: AttributeSyntax, + providingAccessorsOf declaration: some DeclSyntaxProtocol, + in context: some MacroExpansionContext + ) throws -> [AccessorDeclSyntax] { + // Must be attached to a property declaration. + guard let variableDeclaration = declaration.as(VariableDeclSyntax.self) else { + throw ObservableDefaultsError.notAttachedToProperty + } + + // Must be attached to a variable property (i.e. `var` and not `let`). + guard variableDeclaration.bindingSpecifier.tokenKind == .keyword(.var) else { + throw ObservableDefaultsError.notAttachedToVariable + } + + // Must be attached to a single property. + guard variableDeclaration.bindings.count == 1, let binding = variableDeclaration.bindings.first else { + throw ObservableDefaultsError.notAttachedToSingleProperty + } + + // Must not provide an initializer for the property (i.e. not assign a value). + guard binding.initializer == nil else { + throw ObservableDefaultsError.attachedToPropertyWithInitializer + } + + // Must not be attached to property with existing accessor block. + guard binding.accessorBlock == nil else { + throw ObservableDefaultsError.attachedToPropertyWithAccessorBlock + } + + // Must use Identifier Pattern. + // See https://swiftinit.org/docs/swift-syntax/swiftsyntax/identifierpatternsyntax + guard let pattern = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier else { + throw ObservableDefaultsError.attachedToPropertyWithoutIdentifierProperty + } + + // Must receive arguments + guard let arguments = node.arguments else { + throw ObservableDefaultsError.calledWithoutArguments + } + + // Must be called with Labeled Expression. + // See https://swiftinit.org/docs/swift-syntax/swiftsyntax/labeledexprlistsyntax + guard let expressionList = arguments.as(LabeledExprListSyntax.self) else { + throw ObservableDefaultsError.calledWithoutLabeledExpression + } + + // Must only receive one argument. + guard expressionList.count == 1, let expression = expressionList.first?.expression else { + throw ObservableDefaultsError.calledWithMultipleArguments + } + + return [ + #""" + get { + access(keyPath: \.\#(pattern)) + return Defaults[\#(expression)] + } + """#, + #""" + set { + withMutation(keyPath: \.\#(pattern)) { + Defaults[\#(expression)] = newValue + } + } + """# + ] + } +} + +enum ObservableDefaultsError: Error { + case notAttachedToProperty + case notAttachedToVariable + case notAttachedToSingleProperty + case attachedToPropertyWithInitializer + case attachedToPropertyWithAccessorBlock + case attachedToPropertyWithoutIdentifierProperty + case calledWithoutArguments + case calledWithoutLabeledExpression + case calledWithMultipleArguments + case calledWithoutFunctionSyntax + case calledWithoutKeyArgument + case calledWithUnsupportedExpression +} + +extension ObservableDefaultsError: CustomStringConvertible { + var description: String { + switch self { + case .notAttachedToProperty: + "@ObservableDefaults must be attached to a property." + case .notAttachedToVariable: + "@ObservableDefaults must be attached to a `var` property." + case .notAttachedToSingleProperty: + "@ObservableDefaults can only be attached to a single property." + case .attachedToPropertyWithInitializer: + "@ObservableDefaults must not be attached with a property with a value assigned. To create set default value, provide it in the `Defaults.Key` definition." + case .attachedToPropertyWithAccessorBlock: + "@ObservableDefaults must not be attached to a property with accessor block." + case .attachedToPropertyWithoutIdentifierProperty: + "@ObservableDefaults could not identify the attached property." + case .calledWithoutArguments, + .calledWithoutLabeledExpression, + .calledWithMultipleArguments, + .calledWithoutFunctionSyntax, + .calledWithoutKeyArgument, + .calledWithUnsupportedExpression: + "@ObservableDefaults must be called with (1) argument of type `Defaults.Key`" + } + } +} diff --git a/Sources/DefaultsMacrosDeclarations/ObservableDefaultsPlugin.swift b/Sources/DefaultsMacrosDeclarations/ObservableDefaultsPlugin.swift new file mode 100644 index 0000000..c090bd2 --- /dev/null +++ b/Sources/DefaultsMacrosDeclarations/ObservableDefaultsPlugin.swift @@ -0,0 +1,9 @@ +import SwiftCompilerPlugin +import SwiftSyntaxMacros + +@main +struct DefaultsMacrosPlugin: CompilerPlugin { + let providingMacros: [Macro.Type] = [ + ObservableDefaultsMacro.self + ] +} diff --git a/Tests/DefaultsMacrosTests/DefaultsMacrosTests.swift b/Tests/DefaultsMacrosTests/DefaultsMacrosTests.swift new file mode 100644 index 0000000..1dc5749 --- /dev/null +++ b/Tests/DefaultsMacrosTests/DefaultsMacrosTests.swift @@ -0,0 +1,127 @@ +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest + +// Macro implementations build for the host, so the corresponding module is not available when cross-compiling. +// Cross-compiled tests may still make use of the macro itself in end-to-end tests. +#if canImport(DefaultsMacrosDeclarations) +@testable import DefaultsMacros +@testable import DefaultsMacrosDeclarations + +let testMacros: [String: Macro.Type] = [ + "ObservableDefaults": ObservableDefaultsMacro.self, +] +#endif + +final class ObservableDefaultsMacrosTests: XCTestCase { + func testObservableDefaultsWithKeyPath() throws { + #if canImport(DefaultsMacrosDeclarations) + assertMacroExpansion( + #""" + @Observable + class ObservableClass { + @ObservableDefaults(\.name) + @ObservationIgnored + var name: String + } + """#, + expandedSource: + #""" + @Observable + class ObservableClass { + @ObservationIgnored + var name: String { + get { + access(keyPath: \.name) + return Defaults[\.name] + } + set { + withMutation(keyPath: \.name) { + Defaults[\.name] = newValue + } + } + } + } + """#, + macros: testMacros, + indentationWidth: .tabs(1) + ) + #else + throw XCTSkip("Macros are only supported when running tests for the host platform") + #endif + } + + func testObservableDefaultsWithFunctionCall() throws { + #if canImport(DefaultsMacrosDeclarations) + assertMacroExpansion( + #""" + @Observable + class ObservableClass { + @ObservableDefaults(getName()) + @ObservationIgnored + var name: String + } + """#, + expandedSource: + #""" + @Observable + class ObservableClass { + @ObservationIgnored + var name: String { + get { + access(keyPath: \.name) + return Defaults[getName()] + } + set { + withMutation(keyPath: \.name) { + Defaults[getName()] = newValue + } + } + } + } + """#, + macros: testMacros, + indentationWidth: .tabs(1) + ) + #else + throw XCTSkip("Macros are only supported when running tests for the host platform") + #endif + } + + func testObservableDefaultsWithProperty() throws { + #if canImport(DefaultsMacrosDeclarations) + assertMacroExpansion( + #""" + @Observable + class ObservableClass { + @ObservableDefaults(propertyName) + @ObservationIgnored + var name: String + } + """#, + expandedSource: + #""" + @Observable + class ObservableClass { + @ObservationIgnored + var name: String { + get { + access(keyPath: \.name) + return Defaults[propertyName] + } + set { + withMutation(keyPath: \.name) { + Defaults[propertyName] = newValue + } + } + } + } + """#, + macros: testMacros, + indentationWidth: .tabs(1) + ) + #else + throw XCTSkip("Macros are only supported when running tests for the host platform") + #endif + } +} From 21c4281876f5e58525987cd8f485ebfc3c6072b2 Mon Sep 17 00:00:00 2001 From: Kevin Romero Peces-Barba Date: Wed, 18 Sep 2024 12:13:40 -0400 Subject: [PATCH 02/11] Fix test names, add tests for @Observable result Tests for the macro declaration were in the wrong test target and the test method names were not accurate. Added another test target to actually test that the macro works when used in an @Observable class. --- Package.swift | 9 +- .../ObservableDefaultsMacroTests.swift} | 51 ++++++-- .../ObservableDefaultsTests.swift | 111 ++++++++++++++++++ 3 files changed, 163 insertions(+), 8 deletions(-) rename Tests/{DefaultsMacrosTests/DefaultsMacrosTests.swift => DefaultsMacrosDeclarationsTests/ObservableDefaultsMacroTests.swift} (70%) create mode 100644 Tests/DefaultsMacrosTests/ObservableDefaultsTests.swift diff --git a/Package.swift b/Package.swift index f92aa0d..500ee0b 100644 --- a/Package.swift +++ b/Package.swift @@ -52,13 +52,20 @@ let package = Package( ] ), .testTarget( - name: "DefaultsMacrosTests", + name: "DefaultsMacrosDeclarationsTests", dependencies: [ "DefaultsMacros", "DefaultsMacrosDeclarations", .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), ] + ), + .testTarget( + name: "DefaultsMacrosTests", + dependencies: [ + "Defaults", + "DefaultsMacros", + ] ) ] ) diff --git a/Tests/DefaultsMacrosTests/DefaultsMacrosTests.swift b/Tests/DefaultsMacrosDeclarationsTests/ObservableDefaultsMacroTests.swift similarity index 70% rename from Tests/DefaultsMacrosTests/DefaultsMacrosTests.swift rename to Tests/DefaultsMacrosDeclarationsTests/ObservableDefaultsMacroTests.swift index 1dc5749..ae9ee41 100644 --- a/Tests/DefaultsMacrosTests/DefaultsMacrosTests.swift +++ b/Tests/DefaultsMacrosDeclarationsTests/ObservableDefaultsMacroTests.swift @@ -13,14 +13,14 @@ let testMacros: [String: Macro.Type] = [ ] #endif -final class ObservableDefaultsMacrosTests: XCTestCase { - func testObservableDefaultsWithKeyPath() throws { +final class ObservableDefaultsMacroTests: XCTestCase { + func testExpansionWithMemberSyntax() throws { #if canImport(DefaultsMacrosDeclarations) assertMacroExpansion( #""" @Observable class ObservableClass { - @ObservableDefaults(\.name) + @ObservableDefaults(Defaults.Keys.name) @ObservationIgnored var name: String } @@ -33,11 +33,11 @@ final class ObservableDefaultsMacrosTests: XCTestCase { var name: String { get { access(keyPath: \.name) - return Defaults[\.name] + return Defaults[Defaults.Keys.name] } set { withMutation(keyPath: \.name) { - Defaults[\.name] = newValue + Defaults[Defaults.Keys.name] = newValue } } } @@ -51,7 +51,44 @@ final class ObservableDefaultsMacrosTests: XCTestCase { #endif } - func testObservableDefaultsWithFunctionCall() throws { + func testExpansionWithDotSyntax() throws { + #if canImport(DefaultsMacrosDeclarations) + assertMacroExpansion( + #""" + @Observable + class ObservableClass { + @ObservableDefaults(.name) + @ObservationIgnored + var name: String + } + """#, + expandedSource: + #""" + @Observable + class ObservableClass { + @ObservationIgnored + var name: String { + get { + access(keyPath: \.name) + return Defaults[.name] + } + set { + withMutation(keyPath: \.name) { + Defaults[.name] = newValue + } + } + } + } + """#, + macros: testMacros, + indentationWidth: .tabs(1) + ) + #else + throw XCTSkip("Macros are only supported when running tests for the host platform") + #endif + } + + func testExpansionWithFunctionCall() throws { #if canImport(DefaultsMacrosDeclarations) assertMacroExpansion( #""" @@ -88,7 +125,7 @@ final class ObservableDefaultsMacrosTests: XCTestCase { #endif } - func testObservableDefaultsWithProperty() throws { + func testExpansionWithProperty() throws { #if canImport(DefaultsMacrosDeclarations) assertMacroExpansion( #""" diff --git a/Tests/DefaultsMacrosTests/ObservableDefaultsTests.swift b/Tests/DefaultsMacrosTests/ObservableDefaultsTests.swift new file mode 100644 index 0000000..512111a --- /dev/null +++ b/Tests/DefaultsMacrosTests/ObservableDefaultsTests.swift @@ -0,0 +1,111 @@ +import XCTest + +import Defaults +@testable import DefaultsMacros + +private let testKey = "testKey" +private let defaultValue = "defaultValue" +private let newValue = "newValue" + +private extension Defaults.Keys { + static let test = Defaults.Key(testKey, default: defaultValue) +} + +@available(macOS 14, iOS 17, tvOS 17, watchOS 10, visionOS 1, *) +final class ObservableDefaultsTests: XCTestCase { + override class func setUp() { + super.setUp() + Defaults[.test] = defaultValue + } + + override func tearDown() { + super.tearDown() + Defaults.removeAll() + } + + // MARK: Member Syntax + + @Observable + final class TestModelWithMemberSyntax { + @ObservableDefaults(Defaults.Keys.test) + @ObservationIgnored + var testValue: String + } + + func testMacroWithMemberSyntax() { + let model = TestModelWithMemberSyntax() + XCTAssertEqual(model.testValue, defaultValue) + + let userDefaultsValue = UserDefaults.standard.string(forKey: testKey) + XCTAssertEqual(userDefaultsValue, defaultValue) + + UserDefaults.standard.set(newValue, forKey: testKey) + XCTAssertEqual(model.testValue, newValue) + } + + // MARK: Dot syntax + + @Observable + final class TestModelWithDotSyntax { + @ObservableDefaults(.test) + @ObservationIgnored + var testValue: String + } + + func testMacroWithDotSyntax() { + let model = TestModelWithDotSyntax() + XCTAssertEqual(model.testValue, defaultValue) + + let userDefaultsValue = UserDefaults.standard.string(forKey: testKey) + XCTAssertEqual(userDefaultsValue, defaultValue) + + UserDefaults.standard.set(newValue, forKey: testKey) + XCTAssertEqual(model.testValue, newValue) + } + + // MARK: Function call + + static func getKey() -> Defaults.Key { + return .test + } + + @Observable + final class TestModelWithFunctionCall { + @ObservableDefaults(getKey()) + @ObservationIgnored + var testValue: String + } + + func testMacroWithFunctionCall() { + let model = TestModelWithFunctionCall() + XCTAssertEqual(model.testValue, defaultValue) + + let userDefaultsValue = UserDefaults.standard.string(forKey: testKey) + XCTAssertEqual(userDefaultsValue, defaultValue) + + UserDefaults.standard.set(newValue, forKey: testKey) + XCTAssertEqual(model.testValue, newValue) + } + + // MARK: Property + + private static var key = Defaults.Keys.test + + @Observable + final class TestModelWithProperty { + @ObservableDefaults(key) + @ObservationIgnored + var testValue: String + } + + func testMacroWithProperty() { + let model = TestModelWithProperty() + XCTAssertEqual(model.testValue, defaultValue) + + let userDefaultsValue = UserDefaults.standard.string(forKey: testKey) + XCTAssertEqual(userDefaultsValue, defaultValue) + + UserDefaults.standard.set(newValue, forKey: testKey) + XCTAssertEqual(model.testValue, newValue) + } +} From 85587a02400a3d2eb9cecffa499bc60096018afc Mon Sep 17 00:00:00 2001 From: Kevin Romero Peces-Barba Date: Wed, 18 Sep 2024 12:39:22 -0400 Subject: [PATCH 03/11] Add docstring to ObservableDefaults macro --- .../DefaultsMacros/ObservableDefaults.swift | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/Sources/DefaultsMacros/ObservableDefaults.swift b/Sources/DefaultsMacros/ObservableDefaults.swift index 9abd37f..095b1f9 100644 --- a/Sources/DefaultsMacros/ObservableDefaults.swift +++ b/Sources/DefaultsMacros/ObservableDefaults.swift @@ -1,7 +1,43 @@ +import Defaults import Foundation -import Defaults +/** +Attached macro that adds support for using ``Defaults`` in ``@Observable`` classes. **Important**: to +prevent issues with ``@Observable``, you'll need to also add ``@ObservationIgnored`` to the attached +property. + +This macro adds accessor blocks to the attached property similar to those added by `@Observable`. +For example, given the following source: + +```swift +@Observable +final class CatModel { + @ObservableDefaults(Defaults.Keys.cat) + @ObservationIgnored + private var catName: String +} +``` + +The macro will generate the following expansion: +```swift +@Observable +final class CatModel { + @ObservationIgnored + private var catName: String { + get { + access(keypath: \.catName) + return Defaults[Defaults.Keys.cat] + } + set { + withMutation(keyPath: \catName) { + Defaults[Defaults.Keys.cat] = newValue + } + } + } +} +``` +*/ @attached(accessor, names: named(get), named(set)) public macro ObservableDefaults(_ key: Defaults.Key) = #externalMacro( From 028cdfb3aff86c321af2829fb2076b7943568317 Mon Sep 17 00:00:00 2001 From: Kevin Romero Peces-Barba Date: Wed, 18 Sep 2024 12:51:03 -0400 Subject: [PATCH 04/11] Fix incorrect filename for macros plugin, fix lint --- ...bservableDefaultsPlugin.swift => DefaultsMacrosPlugin.swift} | 0 Tests/DefaultsMacrosTests/ObservableDefaultsTests.swift | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename Sources/DefaultsMacrosDeclarations/{ObservableDefaultsPlugin.swift => DefaultsMacrosPlugin.swift} (100%) diff --git a/Sources/DefaultsMacrosDeclarations/ObservableDefaultsPlugin.swift b/Sources/DefaultsMacrosDeclarations/DefaultsMacrosPlugin.swift similarity index 100% rename from Sources/DefaultsMacrosDeclarations/ObservableDefaultsPlugin.swift rename to Sources/DefaultsMacrosDeclarations/DefaultsMacrosPlugin.swift diff --git a/Tests/DefaultsMacrosTests/ObservableDefaultsTests.swift b/Tests/DefaultsMacrosTests/ObservableDefaultsTests.swift index 512111a..0362414 100644 --- a/Tests/DefaultsMacrosTests/ObservableDefaultsTests.swift +++ b/Tests/DefaultsMacrosTests/ObservableDefaultsTests.swift @@ -7,7 +7,7 @@ private let testKey = "testKey" private let defaultValue = "defaultValue" private let newValue = "newValue" -private extension Defaults.Keys { +extension Defaults.Keys { static let test = Defaults.Key(testKey, default: defaultValue) } From 70f569efe325596a464ad9d009e042f2893faa11 Mon Sep 17 00:00:00 2001 From: Kevin Romero Peces-Barba Date: Wed, 18 Sep 2024 13:36:44 -0400 Subject: [PATCH 05/11] Rename `@ObservableDefaults` macro to `@Default` This is to be consistent with the `@Default` property wrapper. --- ...ObservableDefaults.swift => Default.swift} | 6 +-- ...DefaultsMacro.swift => DefaultMacro.swift} | 38 +++++++++---------- .../DefaultsMacrosPlugin.swift | 2 +- ...croTests.swift => DefaultMacroTests.swift} | 12 +++--- ...DefaultsTests.swift => DefaultTests.swift} | 10 ++--- 5 files changed, 34 insertions(+), 34 deletions(-) rename Sources/DefaultsMacros/{ObservableDefaults.swift => Default.swift} (86%) rename Sources/DefaultsMacrosDeclarations/{ObservableDefaultsMacro.swift => DefaultMacro.swift} (68%) rename Tests/DefaultsMacrosDeclarationsTests/{ObservableDefaultsMacroTests.swift => DefaultMacroTests.swift} (92%) rename Tests/DefaultsMacrosTests/{ObservableDefaultsTests.swift => DefaultTests.swift} (93%) diff --git a/Sources/DefaultsMacros/ObservableDefaults.swift b/Sources/DefaultsMacros/Default.swift similarity index 86% rename from Sources/DefaultsMacros/ObservableDefaults.swift rename to Sources/DefaultsMacros/Default.swift index 095b1f9..35edf95 100644 --- a/Sources/DefaultsMacros/ObservableDefaults.swift +++ b/Sources/DefaultsMacros/Default.swift @@ -12,7 +12,7 @@ For example, given the following source: ```swift @Observable final class CatModel { - @ObservableDefaults(Defaults.Keys.cat) + @Default(Defaults.Keys.cat) @ObservationIgnored private var catName: String } @@ -39,8 +39,8 @@ final class CatModel { ``` */ @attached(accessor, names: named(get), named(set)) -public macro ObservableDefaults(_ key: Defaults.Key) = +public macro Default(_ key: Defaults.Key) = #externalMacro( module: "DefaultsMacrosDeclarations", - type: "ObservableDefaultsMacro" + type: "DefaultMacro" ) diff --git a/Sources/DefaultsMacrosDeclarations/ObservableDefaultsMacro.swift b/Sources/DefaultsMacrosDeclarations/DefaultMacro.swift similarity index 68% rename from Sources/DefaultsMacrosDeclarations/ObservableDefaultsMacro.swift rename to Sources/DefaultsMacrosDeclarations/DefaultMacro.swift index 611dc0e..f9cb71e 100644 --- a/Sources/DefaultsMacrosDeclarations/ObservableDefaultsMacro.swift +++ b/Sources/DefaultsMacrosDeclarations/DefaultMacro.swift @@ -4,7 +4,7 @@ import SwiftSyntax import SwiftSyntaxBuilder import SwiftSyntaxMacros -public struct ObservableDefaultsMacro: AccessorMacro { +public struct DefaultMacro: AccessorMacro { public static func expansion( of node: AttributeSyntax, providingAccessorsOf declaration: some DeclSyntaxProtocol, @@ -12,49 +12,49 @@ public struct ObservableDefaultsMacro: AccessorMacro { ) throws -> [AccessorDeclSyntax] { // Must be attached to a property declaration. guard let variableDeclaration = declaration.as(VariableDeclSyntax.self) else { - throw ObservableDefaultsError.notAttachedToProperty + throw DefaultMacroError.notAttachedToProperty } // Must be attached to a variable property (i.e. `var` and not `let`). guard variableDeclaration.bindingSpecifier.tokenKind == .keyword(.var) else { - throw ObservableDefaultsError.notAttachedToVariable + throw DefaultMacroError.notAttachedToVariable } // Must be attached to a single property. guard variableDeclaration.bindings.count == 1, let binding = variableDeclaration.bindings.first else { - throw ObservableDefaultsError.notAttachedToSingleProperty + throw DefaultMacroError.notAttachedToSingleProperty } // Must not provide an initializer for the property (i.e. not assign a value). guard binding.initializer == nil else { - throw ObservableDefaultsError.attachedToPropertyWithInitializer + throw DefaultMacroError.attachedToPropertyWithInitializer } // Must not be attached to property with existing accessor block. guard binding.accessorBlock == nil else { - throw ObservableDefaultsError.attachedToPropertyWithAccessorBlock + throw DefaultMacroError.attachedToPropertyWithAccessorBlock } // Must use Identifier Pattern. // See https://swiftinit.org/docs/swift-syntax/swiftsyntax/identifierpatternsyntax guard let pattern = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier else { - throw ObservableDefaultsError.attachedToPropertyWithoutIdentifierProperty + throw DefaultMacroError.attachedToPropertyWithoutIdentifierProperty } // Must receive arguments guard let arguments = node.arguments else { - throw ObservableDefaultsError.calledWithoutArguments + throw DefaultMacroError.calledWithoutArguments } // Must be called with Labeled Expression. // See https://swiftinit.org/docs/swift-syntax/swiftsyntax/labeledexprlistsyntax guard let expressionList = arguments.as(LabeledExprListSyntax.self) else { - throw ObservableDefaultsError.calledWithoutLabeledExpression + throw DefaultMacroError.calledWithoutLabeledExpression } // Must only receive one argument. guard expressionList.count == 1, let expression = expressionList.first?.expression else { - throw ObservableDefaultsError.calledWithMultipleArguments + throw DefaultMacroError.calledWithMultipleArguments } return [ @@ -75,7 +75,7 @@ public struct ObservableDefaultsMacro: AccessorMacro { } } -enum ObservableDefaultsError: Error { +enum DefaultMacroError: Error { case notAttachedToProperty case notAttachedToVariable case notAttachedToSingleProperty @@ -90,28 +90,28 @@ enum ObservableDefaultsError: Error { case calledWithUnsupportedExpression } -extension ObservableDefaultsError: CustomStringConvertible { +extension DefaultMacroError: CustomStringConvertible { var description: String { switch self { case .notAttachedToProperty: - "@ObservableDefaults must be attached to a property." + "@Default must be attached to a property." case .notAttachedToVariable: - "@ObservableDefaults must be attached to a `var` property." + "@Default must be attached to a `var` property." case .notAttachedToSingleProperty: - "@ObservableDefaults can only be attached to a single property." + "@Default can only be attached to a single property." case .attachedToPropertyWithInitializer: - "@ObservableDefaults must not be attached with a property with a value assigned. To create set default value, provide it in the `Defaults.Key` definition." + "@Default must not be attached with a property with a value assigned. To create set default value, provide it in the `Defaults.Key` definition." case .attachedToPropertyWithAccessorBlock: - "@ObservableDefaults must not be attached to a property with accessor block." + "@Default must not be attached to a property with accessor block." case .attachedToPropertyWithoutIdentifierProperty: - "@ObservableDefaults could not identify the attached property." + "@Default could not identify the attached property." case .calledWithoutArguments, .calledWithoutLabeledExpression, .calledWithMultipleArguments, .calledWithoutFunctionSyntax, .calledWithoutKeyArgument, .calledWithUnsupportedExpression: - "@ObservableDefaults must be called with (1) argument of type `Defaults.Key`" + "@Default must be called with (1) argument of type `Defaults.Key`" } } } diff --git a/Sources/DefaultsMacrosDeclarations/DefaultsMacrosPlugin.swift b/Sources/DefaultsMacrosDeclarations/DefaultsMacrosPlugin.swift index c090bd2..e257ef7 100644 --- a/Sources/DefaultsMacrosDeclarations/DefaultsMacrosPlugin.swift +++ b/Sources/DefaultsMacrosDeclarations/DefaultsMacrosPlugin.swift @@ -4,6 +4,6 @@ import SwiftSyntaxMacros @main struct DefaultsMacrosPlugin: CompilerPlugin { let providingMacros: [Macro.Type] = [ - ObservableDefaultsMacro.self + DefaultMacro.self ] } diff --git a/Tests/DefaultsMacrosDeclarationsTests/ObservableDefaultsMacroTests.swift b/Tests/DefaultsMacrosDeclarationsTests/DefaultMacroTests.swift similarity index 92% rename from Tests/DefaultsMacrosDeclarationsTests/ObservableDefaultsMacroTests.swift rename to Tests/DefaultsMacrosDeclarationsTests/DefaultMacroTests.swift index ae9ee41..ab71566 100644 --- a/Tests/DefaultsMacrosDeclarationsTests/ObservableDefaultsMacroTests.swift +++ b/Tests/DefaultsMacrosDeclarationsTests/DefaultMacroTests.swift @@ -9,18 +9,18 @@ import XCTest @testable import DefaultsMacrosDeclarations let testMacros: [String: Macro.Type] = [ - "ObservableDefaults": ObservableDefaultsMacro.self, + "Default": DefaultMacro.self, ] #endif -final class ObservableDefaultsMacroTests: XCTestCase { +final class DefaultMacroTests: XCTestCase { func testExpansionWithMemberSyntax() throws { #if canImport(DefaultsMacrosDeclarations) assertMacroExpansion( #""" @Observable class ObservableClass { - @ObservableDefaults(Defaults.Keys.name) + @Default(Defaults.Keys.name) @ObservationIgnored var name: String } @@ -57,7 +57,7 @@ final class ObservableDefaultsMacroTests: XCTestCase { #""" @Observable class ObservableClass { - @ObservableDefaults(.name) + @Default(.name) @ObservationIgnored var name: String } @@ -94,7 +94,7 @@ final class ObservableDefaultsMacroTests: XCTestCase { #""" @Observable class ObservableClass { - @ObservableDefaults(getName()) + @Default(getName()) @ObservationIgnored var name: String } @@ -131,7 +131,7 @@ final class ObservableDefaultsMacroTests: XCTestCase { #""" @Observable class ObservableClass { - @ObservableDefaults(propertyName) + @Default(propertyName) @ObservationIgnored var name: String } diff --git a/Tests/DefaultsMacrosTests/ObservableDefaultsTests.swift b/Tests/DefaultsMacrosTests/DefaultTests.swift similarity index 93% rename from Tests/DefaultsMacrosTests/ObservableDefaultsTests.swift rename to Tests/DefaultsMacrosTests/DefaultTests.swift index 0362414..00ed9a4 100644 --- a/Tests/DefaultsMacrosTests/ObservableDefaultsTests.swift +++ b/Tests/DefaultsMacrosTests/DefaultTests.swift @@ -12,7 +12,7 @@ extension Defaults.Keys { } @available(macOS 14, iOS 17, tvOS 17, watchOS 10, visionOS 1, *) -final class ObservableDefaultsTests: XCTestCase { +final class DefaultTests: XCTestCase { override class func setUp() { super.setUp() Defaults[.test] = defaultValue @@ -27,7 +27,7 @@ final class ObservableDefaultsTests: XCTestCase { @Observable final class TestModelWithMemberSyntax { - @ObservableDefaults(Defaults.Keys.test) + @Default(Defaults.Keys.test) @ObservationIgnored var testValue: String } @@ -47,7 +47,7 @@ final class ObservableDefaultsTests: XCTestCase { @Observable final class TestModelWithDotSyntax { - @ObservableDefaults(.test) + @Default(.test) @ObservationIgnored var testValue: String } @@ -71,7 +71,7 @@ final class ObservableDefaultsTests: XCTestCase { @Observable final class TestModelWithFunctionCall { - @ObservableDefaults(getKey()) + @Default(getKey()) @ObservationIgnored var testValue: String } @@ -93,7 +93,7 @@ final class ObservableDefaultsTests: XCTestCase { @Observable final class TestModelWithProperty { - @ObservableDefaults(key) + @Default(key) @ObservationIgnored var testValue: String } From 6cc37e0a521b3b570b975d4e91ceb4a62c08f9a6 Mon Sep 17 00:00:00 2001 From: Kevin Romero Peces-Barba Date: Wed, 18 Sep 2024 13:39:01 -0400 Subject: [PATCH 06/11] Fix lint warnings In the macro tests, I had to move the @Observable classes out of the test because linter was asking to mark them as private but doing so was causing the @Observable macro to error. --- Package.swift | 4 +- .../DefaultMacroTests.swift | 2 +- Tests/DefaultsMacrosTests/DefaultTests.swift | 82 +++++++++---------- 3 files changed, 42 insertions(+), 46 deletions(-) diff --git a/Package.swift b/Package.swift index 500ee0b..e1cb708 100644 --- a/Package.swift +++ b/Package.swift @@ -57,14 +57,14 @@ let package = Package( "DefaultsMacros", "DefaultsMacrosDeclarations", .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), - .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), + .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax") ] ), .testTarget( name: "DefaultsMacrosTests", dependencies: [ "Defaults", - "DefaultsMacros", + "DefaultsMacros" ] ) ] diff --git a/Tests/DefaultsMacrosDeclarationsTests/DefaultMacroTests.swift b/Tests/DefaultsMacrosDeclarationsTests/DefaultMacroTests.swift index ab71566..2922e01 100644 --- a/Tests/DefaultsMacrosDeclarationsTests/DefaultMacroTests.swift +++ b/Tests/DefaultsMacrosDeclarationsTests/DefaultMacroTests.swift @@ -9,7 +9,7 @@ import XCTest @testable import DefaultsMacrosDeclarations let testMacros: [String: Macro.Type] = [ - "Default": DefaultMacro.self, + "Default": DefaultMacro.self ] #endif diff --git a/Tests/DefaultsMacrosTests/DefaultTests.swift b/Tests/DefaultsMacrosTests/DefaultTests.swift index 00ed9a4..c73f3d8 100644 --- a/Tests/DefaultsMacrosTests/DefaultTests.swift +++ b/Tests/DefaultsMacrosTests/DefaultTests.swift @@ -11,9 +11,47 @@ extension Defaults.Keys { static let test = Defaults.Key(testKey, default: defaultValue) } +func getKey() -> Defaults.Key { + .test +} + +let keyProperty = Defaults.Keys.test + +@available(macOS 14, iOS 17, tvOS 17, watchOS 10, visionOS 1, *) +@Observable +private final class TestModelWithMemberSyntax { + @Default(Defaults.Keys.test) + @ObservationIgnored + var testValue: String +} + +@available(macOS 14, iOS 17, tvOS 17, watchOS 10, visionOS 1, *) +@Observable +private final class TestModelWithDotSyntax { + @Default(.test) + @ObservationIgnored + var testValue: String +} + +@available(macOS 14, iOS 17, tvOS 17, watchOS 10, visionOS 1, *) +@Observable +private final class TestModelWithFunctionCall { + @Default(getKey()) + @ObservationIgnored + var testValue: String +} + +@available(macOS 14, iOS 17, tvOS 17, watchOS 10, visionOS 1, *) +@Observable +final class TestModelWithProperty { + @Default(keyProperty) + @ObservationIgnored + var testValue: String +} + @available(macOS 14, iOS 17, tvOS 17, watchOS 10, visionOS 1, *) final class DefaultTests: XCTestCase { - override class func setUp() { + override func setUp() { super.setUp() Defaults[.test] = defaultValue } @@ -23,15 +61,6 @@ final class DefaultTests: XCTestCase { Defaults.removeAll() } - // MARK: Member Syntax - - @Observable - final class TestModelWithMemberSyntax { - @Default(Defaults.Keys.test) - @ObservationIgnored - var testValue: String - } - func testMacroWithMemberSyntax() { let model = TestModelWithMemberSyntax() XCTAssertEqual(model.testValue, defaultValue) @@ -43,15 +72,6 @@ final class DefaultTests: XCTestCase { XCTAssertEqual(model.testValue, newValue) } - // MARK: Dot syntax - - @Observable - final class TestModelWithDotSyntax { - @Default(.test) - @ObservationIgnored - var testValue: String - } - func testMacroWithDotSyntax() { let model = TestModelWithDotSyntax() XCTAssertEqual(model.testValue, defaultValue) @@ -63,19 +83,6 @@ final class DefaultTests: XCTestCase { XCTAssertEqual(model.testValue, newValue) } - // MARK: Function call - - static func getKey() -> Defaults.Key { - return .test - } - - @Observable - final class TestModelWithFunctionCall { - @Default(getKey()) - @ObservationIgnored - var testValue: String - } - func testMacroWithFunctionCall() { let model = TestModelWithFunctionCall() XCTAssertEqual(model.testValue, defaultValue) @@ -87,17 +94,6 @@ final class DefaultTests: XCTestCase { XCTAssertEqual(model.testValue, newValue) } - // MARK: Property - - private static var key = Defaults.Keys.test - - @Observable - final class TestModelWithProperty { - @Default(key) - @ObservationIgnored - var testValue: String - } - func testMacroWithProperty() { let model = TestModelWithProperty() XCTAssertEqual(model.testValue, defaultValue) From 147892d9d8f4a70930b201e322fac2651bd55380 Mon Sep 17 00:00:00 2001 From: Kevin Romero Peces-Barba Date: Wed, 18 Sep 2024 13:48:53 -0400 Subject: [PATCH 07/11] Update readme to include `@Default` macro --- readme.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 3dafd90..1b2300c 100644 --- a/readme.md +++ b/readme.md @@ -181,7 +181,7 @@ Defaults[isUnicorn] ### SwiftUI support -#### `@Default` +#### `@Default` in `View` You can use the `@Default` property wrapper to get/set a `Defaults` item and also have the view be updated when the value changes. This is similar to `@State`. @@ -207,6 +207,22 @@ Note that it's `@Default`, not `@Defaults`. You cannot use `@Default` in an `ObservableObject`. It's meant to be used in a `View`. +#### `@Default` in `@Observable` + +With the `@Default` macro, you can use `Defaults` inside `@Observable` classes that use the [Observation](https://developer.apple.com/documentation/observation) framework. Doing so is as simple as importing `DefaultsMacros` as adding two lines to a property (note that adding `@ObservationIgnored` is needed to prevent clashes with `@Observable`): + +```swift +import Defaults +import DefailtsMacros + +@Observable +final class UnicornManager { + @Default(.hasUnicorn) + @ObservationIgnored + var hasUnicorn: Bool +} +``` + #### `Toggle` There's also a `SwiftUI.Toggle` wrapper that makes it easier to create a toggle based on a `Defaults` key with a `Bool` value. From 8708fd5ab52e8d4b85379809ac993d1cc3e20d93 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Wed, 25 Sep 2024 16:35:27 +0700 Subject: [PATCH 08/11] Update readme.md --- readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 1b2300c..14177fc 100644 --- a/readme.md +++ b/readme.md @@ -209,11 +209,11 @@ You cannot use `@Default` in an `ObservableObject`. It's meant to be used in a ` #### `@Default` in `@Observable` -With the `@Default` macro, you can use `Defaults` inside `@Observable` classes that use the [Observation](https://developer.apple.com/documentation/observation) framework. Doing so is as simple as importing `DefaultsMacros` as adding two lines to a property (note that adding `@ObservationIgnored` is needed to prevent clashes with `@Observable`): +With the `@Default` macro, you can use `Defaults` inside `@Observable` classes that use the [Observation](https://developer.apple.com/documentation/observation) framework. Doing so is as simple as importing `DefaultsMacros` and adding two lines to a property (note that adding `@ObservationIgnored` is needed to prevent clashes with `@Observable`): ```swift import Defaults -import DefailtsMacros +import DefaultsMacros @Observable final class UnicornManager { From 1c9262c9d11ba9db2967fba4f53c704957c340ec Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Wed, 25 Sep 2024 16:45:09 +0700 Subject: [PATCH 09/11] Update Default.swift --- Sources/DefaultsMacros/Default.swift | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/Sources/DefaultsMacros/Default.swift b/Sources/DefaultsMacros/Default.swift index 35edf95..bdcf09b 100644 --- a/Sources/DefaultsMacros/Default.swift +++ b/Sources/DefaultsMacros/Default.swift @@ -2,19 +2,20 @@ import Defaults import Foundation /** -Attached macro that adds support for using ``Defaults`` in ``@Observable`` classes. **Important**: to -prevent issues with ``@Observable``, you'll need to also add ``@ObservationIgnored`` to the attached -property. +Attached macro that adds support for using ``Defaults`` in ``@Observable`` classes. + +- Important: To prevent issues with ``@Observable``, you need to also add ``@ObservationIgnored`` to the attached property. This macro adds accessor blocks to the attached property similar to those added by `@Observable`. + For example, given the following source: ```swift @Observable final class CatModel { - @Default(Defaults.Keys.cat) + @Default(.cat) @ObservationIgnored - private var catName: String + var catName: String } ``` @@ -24,14 +25,14 @@ The macro will generate the following expansion: @Observable final class CatModel { @ObservationIgnored - private var catName: String { + var catName: String { get { access(keypath: \.catName) - return Defaults[Defaults.Keys.cat] + return Defaults[.cat] } set { withMutation(keyPath: \catName) { - Defaults[Defaults.Keys.cat] = newValue + Defaults[.cat] = newValue } } } From 017a42da905ad3bd218b680c437c09477d47d4fb Mon Sep 17 00:00:00 2001 From: Kevin Romero Peces-Barba Date: Thu, 26 Sep 2024 11:35:13 -0400 Subject: [PATCH 10/11] Rename @Default macro back to @ObservableDefault We cannot overload @Default, otherwise it is impossible to use the property wrapper version in the same source file as the macro as the later takes precedence. --- ...{Default.swift => ObservableDefault.swift} | 6 +-- .../DefaultsMacrosPlugin.swift | 2 +- ...cro.swift => ObservableDefaultMacro.swift} | 38 +++++++++---------- ...wift => ObservableDefaultMacroTests.swift} | 12 +++--- ...sts.swift => ObservableDefaultTests.swift} | 10 ++--- readme.md | 6 +-- 6 files changed, 37 insertions(+), 37 deletions(-) rename Sources/DefaultsMacros/{Default.swift => ObservableDefault.swift} (87%) rename Sources/DefaultsMacrosDeclarations/{DefaultMacro.swift => ObservableDefaultMacro.swift} (67%) rename Tests/DefaultsMacrosDeclarationsTests/{DefaultMacroTests.swift => ObservableDefaultMacroTests.swift} (92%) rename Tests/DefaultsMacrosTests/{DefaultTests.swift => ObservableDefaultTests.swift} (93%) diff --git a/Sources/DefaultsMacros/Default.swift b/Sources/DefaultsMacros/ObservableDefault.swift similarity index 87% rename from Sources/DefaultsMacros/Default.swift rename to Sources/DefaultsMacros/ObservableDefault.swift index bdcf09b..ed3e1db 100644 --- a/Sources/DefaultsMacros/Default.swift +++ b/Sources/DefaultsMacros/ObservableDefault.swift @@ -13,7 +13,7 @@ For example, given the following source: ```swift @Observable final class CatModel { - @Default(.cat) + @ObservableDefault(.cat) @ObservationIgnored var catName: String } @@ -40,8 +40,8 @@ final class CatModel { ``` */ @attached(accessor, names: named(get), named(set)) -public macro Default(_ key: Defaults.Key) = +public macro ObservableDefault(_ key: Defaults.Key) = #externalMacro( module: "DefaultsMacrosDeclarations", - type: "DefaultMacro" + type: "ObservableDefaultMacro" ) diff --git a/Sources/DefaultsMacrosDeclarations/DefaultsMacrosPlugin.swift b/Sources/DefaultsMacrosDeclarations/DefaultsMacrosPlugin.swift index e257ef7..689f41d 100644 --- a/Sources/DefaultsMacrosDeclarations/DefaultsMacrosPlugin.swift +++ b/Sources/DefaultsMacrosDeclarations/DefaultsMacrosPlugin.swift @@ -4,6 +4,6 @@ import SwiftSyntaxMacros @main struct DefaultsMacrosPlugin: CompilerPlugin { let providingMacros: [Macro.Type] = [ - DefaultMacro.self + ObservableDefaultMacro.self ] } diff --git a/Sources/DefaultsMacrosDeclarations/DefaultMacro.swift b/Sources/DefaultsMacrosDeclarations/ObservableDefaultMacro.swift similarity index 67% rename from Sources/DefaultsMacrosDeclarations/DefaultMacro.swift rename to Sources/DefaultsMacrosDeclarations/ObservableDefaultMacro.swift index f9cb71e..720254a 100644 --- a/Sources/DefaultsMacrosDeclarations/DefaultMacro.swift +++ b/Sources/DefaultsMacrosDeclarations/ObservableDefaultMacro.swift @@ -4,7 +4,7 @@ import SwiftSyntax import SwiftSyntaxBuilder import SwiftSyntaxMacros -public struct DefaultMacro: AccessorMacro { +public struct ObservableDefaultMacro: AccessorMacro { public static func expansion( of node: AttributeSyntax, providingAccessorsOf declaration: some DeclSyntaxProtocol, @@ -12,49 +12,49 @@ public struct DefaultMacro: AccessorMacro { ) throws -> [AccessorDeclSyntax] { // Must be attached to a property declaration. guard let variableDeclaration = declaration.as(VariableDeclSyntax.self) else { - throw DefaultMacroError.notAttachedToProperty + throw ObservableDefaultMacroError.notAttachedToProperty } // Must be attached to a variable property (i.e. `var` and not `let`). guard variableDeclaration.bindingSpecifier.tokenKind == .keyword(.var) else { - throw DefaultMacroError.notAttachedToVariable + throw ObservableDefaultMacroError.notAttachedToVariable } // Must be attached to a single property. guard variableDeclaration.bindings.count == 1, let binding = variableDeclaration.bindings.first else { - throw DefaultMacroError.notAttachedToSingleProperty + throw ObservableDefaultMacroError.notAttachedToSingleProperty } // Must not provide an initializer for the property (i.e. not assign a value). guard binding.initializer == nil else { - throw DefaultMacroError.attachedToPropertyWithInitializer + throw ObservableDefaultMacroError.attachedToPropertyWithInitializer } // Must not be attached to property with existing accessor block. guard binding.accessorBlock == nil else { - throw DefaultMacroError.attachedToPropertyWithAccessorBlock + throw ObservableDefaultMacroError.attachedToPropertyWithAccessorBlock } // Must use Identifier Pattern. // See https://swiftinit.org/docs/swift-syntax/swiftsyntax/identifierpatternsyntax guard let pattern = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier else { - throw DefaultMacroError.attachedToPropertyWithoutIdentifierProperty + throw ObservableDefaultMacroError.attachedToPropertyWithoutIdentifierProperty } // Must receive arguments guard let arguments = node.arguments else { - throw DefaultMacroError.calledWithoutArguments + throw ObservableDefaultMacroError.calledWithoutArguments } // Must be called with Labeled Expression. // See https://swiftinit.org/docs/swift-syntax/swiftsyntax/labeledexprlistsyntax guard let expressionList = arguments.as(LabeledExprListSyntax.self) else { - throw DefaultMacroError.calledWithoutLabeledExpression + throw ObservableDefaultMacroError.calledWithoutLabeledExpression } // Must only receive one argument. guard expressionList.count == 1, let expression = expressionList.first?.expression else { - throw DefaultMacroError.calledWithMultipleArguments + throw ObservableDefaultMacroError.calledWithMultipleArguments } return [ @@ -75,7 +75,7 @@ public struct DefaultMacro: AccessorMacro { } } -enum DefaultMacroError: Error { +enum ObservableDefaultMacroError: Error { case notAttachedToProperty case notAttachedToVariable case notAttachedToSingleProperty @@ -90,28 +90,28 @@ enum DefaultMacroError: Error { case calledWithUnsupportedExpression } -extension DefaultMacroError: CustomStringConvertible { +extension ObservableDefaultMacroError: CustomStringConvertible { var description: String { switch self { case .notAttachedToProperty: - "@Default must be attached to a property." + "@ObservableDefault must be attached to a property." case .notAttachedToVariable: - "@Default must be attached to a `var` property." + "@ObservableDefault must be attached to a `var` property." case .notAttachedToSingleProperty: - "@Default can only be attached to a single property." + "@ObservableDefault can only be attached to a single property." case .attachedToPropertyWithInitializer: - "@Default must not be attached with a property with a value assigned. To create set default value, provide it in the `Defaults.Key` definition." + "@ObservableDefault must not be attached with a property with a value assigned. To create set default value, provide it in the `Defaults.Key` definition." case .attachedToPropertyWithAccessorBlock: - "@Default must not be attached to a property with accessor block." + "@ObservableDefault must not be attached to a property with accessor block." case .attachedToPropertyWithoutIdentifierProperty: - "@Default could not identify the attached property." + "@ObservableDefault could not identify the attached property." case .calledWithoutArguments, .calledWithoutLabeledExpression, .calledWithMultipleArguments, .calledWithoutFunctionSyntax, .calledWithoutKeyArgument, .calledWithUnsupportedExpression: - "@Default must be called with (1) argument of type `Defaults.Key`" + "@ObservableDefault must be called with (1) argument of type `Defaults.Key`" } } } diff --git a/Tests/DefaultsMacrosDeclarationsTests/DefaultMacroTests.swift b/Tests/DefaultsMacrosDeclarationsTests/ObservableDefaultMacroTests.swift similarity index 92% rename from Tests/DefaultsMacrosDeclarationsTests/DefaultMacroTests.swift rename to Tests/DefaultsMacrosDeclarationsTests/ObservableDefaultMacroTests.swift index 2922e01..2dce238 100644 --- a/Tests/DefaultsMacrosDeclarationsTests/DefaultMacroTests.swift +++ b/Tests/DefaultsMacrosDeclarationsTests/ObservableDefaultMacroTests.swift @@ -9,18 +9,18 @@ import XCTest @testable import DefaultsMacrosDeclarations let testMacros: [String: Macro.Type] = [ - "Default": DefaultMacro.self + "ObservableDefault": ObservableDefaultMacro.self ] #endif -final class DefaultMacroTests: XCTestCase { +final class ObservableDefaultMacroTests: XCTestCase { func testExpansionWithMemberSyntax() throws { #if canImport(DefaultsMacrosDeclarations) assertMacroExpansion( #""" @Observable class ObservableClass { - @Default(Defaults.Keys.name) + @ObservableDefault(Defaults.Keys.name) @ObservationIgnored var name: String } @@ -57,7 +57,7 @@ final class DefaultMacroTests: XCTestCase { #""" @Observable class ObservableClass { - @Default(.name) + @ObservableDefault(.name) @ObservationIgnored var name: String } @@ -94,7 +94,7 @@ final class DefaultMacroTests: XCTestCase { #""" @Observable class ObservableClass { - @Default(getName()) + @ObservableDefault(getName()) @ObservationIgnored var name: String } @@ -131,7 +131,7 @@ final class DefaultMacroTests: XCTestCase { #""" @Observable class ObservableClass { - @Default(propertyName) + @ObservableDefault(propertyName) @ObservationIgnored var name: String } diff --git a/Tests/DefaultsMacrosTests/DefaultTests.swift b/Tests/DefaultsMacrosTests/ObservableDefaultTests.swift similarity index 93% rename from Tests/DefaultsMacrosTests/DefaultTests.swift rename to Tests/DefaultsMacrosTests/ObservableDefaultTests.swift index c73f3d8..a1243e7 100644 --- a/Tests/DefaultsMacrosTests/DefaultTests.swift +++ b/Tests/DefaultsMacrosTests/ObservableDefaultTests.swift @@ -20,7 +20,7 @@ let keyProperty = Defaults.Keys.test @available(macOS 14, iOS 17, tvOS 17, watchOS 10, visionOS 1, *) @Observable private final class TestModelWithMemberSyntax { - @Default(Defaults.Keys.test) + @ObservableDefault(Defaults.Keys.test) @ObservationIgnored var testValue: String } @@ -28,7 +28,7 @@ private final class TestModelWithMemberSyntax { @available(macOS 14, iOS 17, tvOS 17, watchOS 10, visionOS 1, *) @Observable private final class TestModelWithDotSyntax { - @Default(.test) + @ObservableDefault(.test) @ObservationIgnored var testValue: String } @@ -36,7 +36,7 @@ private final class TestModelWithDotSyntax { @available(macOS 14, iOS 17, tvOS 17, watchOS 10, visionOS 1, *) @Observable private final class TestModelWithFunctionCall { - @Default(getKey()) + @ObservableDefault(getKey()) @ObservationIgnored var testValue: String } @@ -44,13 +44,13 @@ private final class TestModelWithFunctionCall { @available(macOS 14, iOS 17, tvOS 17, watchOS 10, visionOS 1, *) @Observable final class TestModelWithProperty { - @Default(keyProperty) + @ObservableDefault(keyProperty) @ObservationIgnored var testValue: String } @available(macOS 14, iOS 17, tvOS 17, watchOS 10, visionOS 1, *) -final class DefaultTests: XCTestCase { +final class ObservableDefaultTests: XCTestCase { override func setUp() { super.setUp() Defaults[.test] = defaultValue diff --git a/readme.md b/readme.md index 14177fc..073146e 100644 --- a/readme.md +++ b/readme.md @@ -207,9 +207,9 @@ Note that it's `@Default`, not `@Defaults`. You cannot use `@Default` in an `ObservableObject`. It's meant to be used in a `View`. -#### `@Default` in `@Observable` +#### `@ObservableDefault` in `@Observable` -With the `@Default` macro, you can use `Defaults` inside `@Observable` classes that use the [Observation](https://developer.apple.com/documentation/observation) framework. Doing so is as simple as importing `DefaultsMacros` and adding two lines to a property (note that adding `@ObservationIgnored` is needed to prevent clashes with `@Observable`): +With the `@ObservableDefault` macro, you can use `Defaults` inside `@Observable` classes that use the [Observation](https://developer.apple.com/documentation/observation) framework. Doing so is as simple as importing `DefaultsMacros` and adding two lines to a property (note that adding `@ObservationIgnored` is needed to prevent clashes with `@Observable`): ```swift import Defaults @@ -217,7 +217,7 @@ import DefaultsMacros @Observable final class UnicornManager { - @Default(.hasUnicorn) + @ObservableDefault(.hasUnicorn) @ObservationIgnored var hasUnicorn: Bool } From 242aba5c48545ed1c157bf90edb3d27ade7cceb4 Mon Sep 17 00:00:00 2001 From: Kevin Romero Peces-Barba Date: Thu, 26 Sep 2024 11:55:05 -0400 Subject: [PATCH 11/11] Add build time disclaimer in @ObservableDefault section of readme --- readme.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/readme.md b/readme.md index 073146e..03022eb 100644 --- a/readme.md +++ b/readme.md @@ -211,6 +211,11 @@ You cannot use `@Default` in an `ObservableObject`. It's meant to be used in a ` With the `@ObservableDefault` macro, you can use `Defaults` inside `@Observable` classes that use the [Observation](https://developer.apple.com/documentation/observation) framework. Doing so is as simple as importing `DefaultsMacros` and adding two lines to a property (note that adding `@ObservationIgnored` is needed to prevent clashes with `@Observable`): +> [!IMPORTANT] +> Build times will increase when using macros. +> +> Swift macros depend on the [`swift-syntax`](https://github.com/swiftlang/swift-syntax) package. This means that when you compile code that includes macros as dependencies, you also have to compile `swift-syntax`. It is widely known that doing so has serious impact in build time and, while it is an issue that is being tracked (see [`swift-syntax`#2421](https://github.com/swiftlang/swift-syntax/issues/2421)), there's currently no solution implemented. + ```swift import Defaults import DefaultsMacros