Add `@ObservableDefault` macro (#189)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
This commit is contained in:
parent
a89f799930
commit
ef1b2318fb
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"originHash" : "ab2612a1595aa1a4d9bb3f076279fda1b1b3d17525d1f97e45ce22c697728978",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "swift-syntax",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/swiftlang/swift-syntax",
|
||||
"state" : {
|
||||
"revision" : "0687f71944021d616d34d922343dcef086855920",
|
||||
"version" : "600.0.1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 3
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
// swift-tools-version:5.11
|
||||
import PackageDescription
|
||||
import CompilerPluginSupport
|
||||
|
||||
let package = Package(
|
||||
name: "Defaults",
|
||||
|
@ -16,8 +17,17 @@ let package = Package(
|
|||
targets: [
|
||||
"Defaults"
|
||||
]
|
||||
),
|
||||
.library(
|
||||
name: "DefaultsMacros",
|
||||
targets: [
|
||||
"DefaultsMacros"
|
||||
]
|
||||
)
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/swiftlang/swift-syntax", from: "600.0.1")
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "Defaults",
|
||||
|
@ -28,6 +38,18 @@ let package = Package(
|
|||
// .swiftLanguageMode(.v5)
|
||||
// ]
|
||||
),
|
||||
.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: [
|
||||
|
@ -36,6 +58,22 @@ let package = Package(
|
|||
// swiftSettings: [
|
||||
// .swiftLanguageMode(.v5)
|
||||
// ]
|
||||
),
|
||||
.testTarget(
|
||||
name: "DefaultsMacrosDeclarationsTests",
|
||||
dependencies: [
|
||||
"DefaultsMacros",
|
||||
"DefaultsMacrosDeclarations",
|
||||
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
|
||||
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax")
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "DefaultsMacrosTests",
|
||||
dependencies: [
|
||||
"Defaults",
|
||||
"DefaultsMacros"
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
import Defaults
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
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 {
|
||||
@ObservableDefault(.cat)
|
||||
@ObservationIgnored
|
||||
var catName: String
|
||||
}
|
||||
```
|
||||
|
||||
The macro will generate the following expansion:
|
||||
|
||||
```swift
|
||||
@Observable
|
||||
final class CatModel {
|
||||
@ObservationIgnored
|
||||
var catName: String {
|
||||
get {
|
||||
access(keypath: \.catName)
|
||||
return Defaults[.cat]
|
||||
}
|
||||
set {
|
||||
withMutation(keyPath: \catName) {
|
||||
Defaults[.cat] = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
*/
|
||||
@attached(accessor, names: named(get), named(set))
|
||||
@attached(peer, names: prefixed(`_objcAssociatedKey_`))
|
||||
public macro ObservableDefault<Value>(_ key: Defaults.Key<Value>) =
|
||||
#externalMacro(
|
||||
module: "DefaultsMacrosDeclarations",
|
||||
type: "ObservableDefaultMacro"
|
||||
)
|
|
@ -0,0 +1,9 @@
|
|||
import SwiftCompilerPlugin
|
||||
import SwiftSyntaxMacros
|
||||
|
||||
@main
|
||||
struct DefaultsMacrosPlugin: CompilerPlugin {
|
||||
let providingMacros: [Macro.Type] = [
|
||||
ObservableDefaultMacro.self
|
||||
]
|
||||
}
|
|
@ -0,0 +1,191 @@
|
|||
import SwiftCompilerPlugin
|
||||
import SwiftDiagnostics
|
||||
import SwiftSyntax
|
||||
import SwiftSyntaxBuilder
|
||||
import SwiftSyntaxMacros
|
||||
|
||||
/**
|
||||
Macro declaration for the ``ObservableDefault`` macro.
|
||||
*/
|
||||
public struct ObservableDefaultMacro {}
|
||||
|
||||
/**
|
||||
Conforming to ``AccessorMacro`` allows us to add the property accessors (get/set) that integrate with ``Observable``.
|
||||
*/
|
||||
extension ObservableDefaultMacro: AccessorMacro {
|
||||
public static func expansion(
|
||||
of node: AttributeSyntax,
|
||||
providingAccessorsOf declaration: some DeclSyntaxProtocol,
|
||||
in context: some MacroExpansionContext
|
||||
) throws(ObservableDefaultMacroError) -> [AccessorDeclSyntax] {
|
||||
let property = try propertyPattern(of: declaration)
|
||||
let expression = try keyExpression(of: node)
|
||||
let associatedKey = associatedKeyToken(for: property)
|
||||
|
||||
// The get/set accessors follow the same pattern that @Observable uses to handle the mutations.
|
||||
//
|
||||
// The get accessor also sets up an observation to update the value when the UserDefaults
|
||||
// changes from elsewhere. Doing so requires attaching it as an Objective-C associated
|
||||
// object due to limitations with current macro capabilities and Swift concurrency.
|
||||
return [
|
||||
#"""
|
||||
get {
|
||||
if objc_getAssociatedObject(self, &Self.\#(associatedKey)) == nil {
|
||||
let cancellable = Defaults.publisher(\#(expression))
|
||||
.sink { [weak self] in
|
||||
self?.\#(property) = $0.newValue
|
||||
}
|
||||
objc_setAssociatedObject(self, &Self.\#(associatedKey), cancellable, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
access(keyPath: \.\#(property))
|
||||
return Defaults[\#(expression)]
|
||||
}
|
||||
"""#,
|
||||
#"""
|
||||
set {
|
||||
withMutation(keyPath: \.\#(property)) {
|
||||
Defaults[\#(expression)] = newValue
|
||||
}
|
||||
}
|
||||
"""#
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Conforming to ``PeerMacro`` we can add a new property of type Defaults.Observation that will update the original property whenever
|
||||
the UserDefaults value changes outside the class.
|
||||
*/
|
||||
extension ObservableDefaultMacro: PeerMacro {
|
||||
public static func expansion(
|
||||
of node: SwiftSyntax.AttributeSyntax,
|
||||
providingPeersOf declaration: some SwiftSyntax.DeclSyntaxProtocol,
|
||||
in context: some SwiftSyntaxMacros.MacroExpansionContext
|
||||
) throws -> [SwiftSyntax.DeclSyntax] {
|
||||
let property = try propertyPattern(of: declaration)
|
||||
let associatedKey = associatedKeyToken(for: property)
|
||||
|
||||
return [
|
||||
"private nonisolated(unsafe) static var \(associatedKey): Void?"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// Logic used by both macro implementations
|
||||
extension ObservableDefaultMacro {
|
||||
/**
|
||||
Extracts the pattern (i.e. the name) of the attached property.
|
||||
*/
|
||||
private static func propertyPattern(
|
||||
of declaration: some SwiftSyntax.DeclSyntaxProtocol
|
||||
) throws(ObservableDefaultMacroError) -> TokenSyntax {
|
||||
// Must be attached to a property declaration.
|
||||
guard let variableDeclaration = declaration.as(VariableDeclSyntax.self) else {
|
||||
throw .notAttachedToProperty
|
||||
}
|
||||
|
||||
// Must be attached to a variable property (i.e. `var` and not `let`).
|
||||
guard variableDeclaration.bindingSpecifier.tokenKind == .keyword(.var) else {
|
||||
throw .notAttachedToVariable
|
||||
}
|
||||
|
||||
// Must be attached to a single property.
|
||||
guard variableDeclaration.bindings.count == 1, let binding = variableDeclaration.bindings.first else {
|
||||
throw .notAttachedToSingleProperty
|
||||
}
|
||||
|
||||
// Must not provide an initializer for the property (i.e. not assign a value).
|
||||
guard binding.initializer == nil else {
|
||||
throw .attachedToPropertyWithInitializer
|
||||
}
|
||||
|
||||
// Must not be attached to property with existing accessor block.
|
||||
guard binding.accessorBlock == nil else {
|
||||
throw .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 .attachedToPropertyWithoutIdentifierProperty
|
||||
}
|
||||
|
||||
return pattern
|
||||
}
|
||||
|
||||
/**
|
||||
Extracts the expression used to define the Defaults.Key in the macro call.
|
||||
*/
|
||||
private static func keyExpression(
|
||||
of node: AttributeSyntax
|
||||
) throws(ObservableDefaultMacroError) -> ExprSyntax {
|
||||
// Must receive arguments
|
||||
guard let arguments = node.arguments else {
|
||||
throw .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 .calledWithoutLabeledExpression
|
||||
}
|
||||
|
||||
// Must only receive one argument.
|
||||
guard expressionList.count == 1, let expression = expressionList.first?.expression else {
|
||||
throw .calledWithMultipleArguments
|
||||
}
|
||||
|
||||
return expression
|
||||
}
|
||||
|
||||
/**
|
||||
Generates the token to use as key for the associated object used to hold the UserDefaults observation.
|
||||
*/
|
||||
private static func associatedKeyToken(for property: TokenSyntax) -> TokenSyntax {
|
||||
"_objcAssociatedKey_\(property)"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Error handling for ``ObservableDefaultMacro``.
|
||||
*/
|
||||
public enum ObservableDefaultMacroError: 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 ObservableDefaultMacroError: CustomStringConvertible {
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .notAttachedToProperty:
|
||||
"@ObservableDefault must be attached to a property."
|
||||
case .notAttachedToVariable:
|
||||
"@ObservableDefault must be attached to a `var` property."
|
||||
case .notAttachedToSingleProperty:
|
||||
"@ObservableDefault can only be attached to a single property."
|
||||
case .attachedToPropertyWithInitializer:
|
||||
"@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:
|
||||
"@ObservableDefault must not be attached to a property with accessor block."
|
||||
case .attachedToPropertyWithoutIdentifierProperty:
|
||||
"@ObservableDefault could not identify the attached property."
|
||||
case .calledWithoutArguments,
|
||||
.calledWithoutLabeledExpression,
|
||||
.calledWithMultipleArguments,
|
||||
.calledWithoutFunctionSyntax,
|
||||
.calledWithoutKeyArgument,
|
||||
.calledWithUnsupportedExpression:
|
||||
"@ObservableDefault must be called with (1) argument of type `Defaults.Key`"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
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] = [
|
||||
"ObservableDefault": ObservableDefaultMacro.self
|
||||
]
|
||||
#else
|
||||
let testMacros: [String: Macro.Type] = [:]
|
||||
#endif
|
||||
|
||||
final class ObservableDefaultMacroTests: XCTestCase {
|
||||
func testExpansionWithMemberSyntax() throws {
|
||||
#if canImport(DefaultsMacrosDeclarations)
|
||||
assertMacroExpansion(
|
||||
declaration(for: "Defaults.Keys.name"),
|
||||
expandedSource: expectedExpansion(for: "Defaults.Keys.name"),
|
||||
macros: testMacros,
|
||||
indentationWidth: .tabs(1)
|
||||
)
|
||||
#else
|
||||
throw XCTSkip("Macros are only supported when running tests for the host platform")
|
||||
#endif
|
||||
}
|
||||
|
||||
func testExpansionWithDotSyntax() throws {
|
||||
#if canImport(DefaultsMacrosDeclarations)
|
||||
assertMacroExpansion(
|
||||
declaration(for: ".name"),
|
||||
expandedSource: expectedExpansion(for: ".name"),
|
||||
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(
|
||||
declaration(for: "getName()"),
|
||||
expandedSource: expectedExpansion(for: "getName()"),
|
||||
macros: testMacros,
|
||||
indentationWidth: .tabs(1)
|
||||
)
|
||||
#else
|
||||
throw XCTSkip("Macros are only supported when running tests for the host platform")
|
||||
#endif
|
||||
}
|
||||
|
||||
func testExpansionWithProperty() throws {
|
||||
#if canImport(DefaultsMacrosDeclarations)
|
||||
assertMacroExpansion(
|
||||
declaration(for: "propertyName"),
|
||||
expandedSource: expectedExpansion(for: "propertyName"),
|
||||
macros: testMacros,
|
||||
indentationWidth: .tabs(1)
|
||||
)
|
||||
#else
|
||||
throw XCTSkip("Macros are only supported when running tests for the host platform")
|
||||
#endif
|
||||
}
|
||||
|
||||
private func declaration(for keyExpression: String) -> String {
|
||||
#"""
|
||||
@Observable
|
||||
class ObservableClass {
|
||||
@ObservableDefault(\#(keyExpression))
|
||||
@ObservationIgnored
|
||||
var name: String
|
||||
}
|
||||
"""#
|
||||
}
|
||||
|
||||
private func expectedExpansion(for keyExpression: String) -> String {
|
||||
#"""
|
||||
@Observable
|
||||
class ObservableClass {
|
||||
@ObservationIgnored
|
||||
var name: String {
|
||||
get {
|
||||
if objc_getAssociatedObject(self, &Self._objcAssociatedKey_name) == nil {
|
||||
let cancellable = Defaults.publisher(\#(keyExpression))
|
||||
.sink { [weak self] in
|
||||
self?.name = $0.newValue
|
||||
}
|
||||
objc_setAssociatedObject(self, &Self._objcAssociatedKey_name, cancellable, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
access(keyPath: \.name)
|
||||
return Defaults[\#(keyExpression)]
|
||||
}
|
||||
set {
|
||||
withMutation(keyPath: \.name) {
|
||||
Defaults[\#(keyExpression)] = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private nonisolated(unsafe) static var _objcAssociatedKey_name: Void?
|
||||
}
|
||||
"""#
|
||||
}
|
||||
}
|
|
@ -0,0 +1,197 @@
|
|||
import Defaults
|
||||
import DefaultsMacros
|
||||
import Foundation
|
||||
import Observation
|
||||
import Testing
|
||||
|
||||
private let animalKey = "animalKey"
|
||||
private let defaultAnimal = "cat"
|
||||
private let newAnimal = "unicorn"
|
||||
|
||||
private let colorKey = "colorKey"
|
||||
private let defaultColor = "blue"
|
||||
private let newColor = "purple"
|
||||
|
||||
extension Defaults.Keys {
|
||||
static let animal = Defaults.Key(animalKey, default: defaultAnimal)
|
||||
static let color = Defaults.Key(colorKey, default: defaultColor)
|
||||
}
|
||||
|
||||
func getKey() -> Defaults.Key<String> {
|
||||
.animal
|
||||
}
|
||||
|
||||
let keyProperty = Defaults.Keys.animal
|
||||
|
||||
@available(macOS 14, iOS 17, tvOS 17, watchOS 10, visionOS 1, *)
|
||||
@Observable
|
||||
private final class TestModelWithDotSyntax: Sendable {
|
||||
@ObservableDefault(.animal)
|
||||
@ObservationIgnored
|
||||
var animal: String
|
||||
}
|
||||
|
||||
@available(macOS 14, iOS 17, tvOS 17, watchOS 10, visionOS 1, *)
|
||||
@Observable
|
||||
private final class TestModelWithFunctionCall {
|
||||
@ObservableDefault(getKey())
|
||||
@ObservationIgnored
|
||||
var animal: String
|
||||
}
|
||||
|
||||
@available(macOS 14, iOS 17, tvOS 17, watchOS 10, visionOS 1, *)
|
||||
@Observable
|
||||
final class TestModelWithProperty {
|
||||
@ObservableDefault(keyProperty)
|
||||
@ObservationIgnored
|
||||
var animal: String
|
||||
}
|
||||
|
||||
@available(macOS 14, iOS 17, tvOS 17, watchOS 10, visionOS 1, *)
|
||||
@Observable
|
||||
private final class TestModelWithMemberSyntax {
|
||||
@ObservableDefault(Defaults.Keys.animal)
|
||||
@ObservationIgnored
|
||||
var animal: String
|
||||
}
|
||||
|
||||
@available(macOS 14, iOS 17, tvOS 17, watchOS 10, visionOS 1, *)
|
||||
@Observable
|
||||
private final class TestModelWithMultipleValues {
|
||||
@ObservableDefault(.animal)
|
||||
@ObservationIgnored
|
||||
var animal: String
|
||||
|
||||
@ObservableDefault(.color)
|
||||
@ObservationIgnored
|
||||
var color: String
|
||||
}
|
||||
|
||||
@Suite(.serialized)
|
||||
final class ObservableDefaultTests {
|
||||
init() {
|
||||
Defaults.removeAll()
|
||||
Defaults[.animal] = defaultAnimal
|
||||
Defaults[.color] = defaultColor
|
||||
}
|
||||
|
||||
deinit {
|
||||
Defaults.removeAll()
|
||||
}
|
||||
|
||||
@available(macOS 14, iOS 17, tvOS 17, watchOS 10, visionOS 1, *)
|
||||
@Test
|
||||
func testMacroWithMemberSyntax() async {
|
||||
let model = TestModelWithMemberSyntax()
|
||||
#expect(model.animal == defaultAnimal)
|
||||
|
||||
let userDefaultsValue = UserDefaults.standard.string(forKey: animalKey)
|
||||
#expect(userDefaultsValue == defaultAnimal)
|
||||
|
||||
await confirmation { confirmation in
|
||||
_ = withObservationTracking {
|
||||
model.animal
|
||||
} onChange: {
|
||||
confirmation()
|
||||
}
|
||||
|
||||
UserDefaults.standard.set(newAnimal, forKey: animalKey)
|
||||
}
|
||||
|
||||
#expect(model.animal == newAnimal)
|
||||
}
|
||||
|
||||
@available(macOS 14, iOS 17, tvOS 17, watchOS 10, visionOS 1, *)
|
||||
@Test
|
||||
func testMacroWithDotSyntax() async {
|
||||
let model = TestModelWithDotSyntax()
|
||||
#expect(model.animal == defaultAnimal)
|
||||
|
||||
let userDefaultsValue = UserDefaults.standard.string(forKey: animalKey)
|
||||
#expect(userDefaultsValue == defaultAnimal)
|
||||
|
||||
await confirmation { confirmation in
|
||||
_ = withObservationTracking {
|
||||
model.animal
|
||||
} onChange: {
|
||||
confirmation()
|
||||
}
|
||||
|
||||
UserDefaults.standard.set(newAnimal, forKey: animalKey)
|
||||
}
|
||||
|
||||
#expect(model.animal == newAnimal)
|
||||
}
|
||||
|
||||
@available(macOS 14, iOS 17, tvOS 17, watchOS 10, visionOS 1, *)
|
||||
@Test
|
||||
func testMacroWithFunctionCall() async {
|
||||
let model = TestModelWithFunctionCall()
|
||||
#expect(model.animal == defaultAnimal)
|
||||
|
||||
let userDefaultsValue = UserDefaults.standard.string(forKey: animalKey)
|
||||
#expect(userDefaultsValue == defaultAnimal)
|
||||
|
||||
await confirmation { confirmation in
|
||||
_ = withObservationTracking {
|
||||
model.animal
|
||||
} onChange: {
|
||||
confirmation()
|
||||
}
|
||||
|
||||
UserDefaults.standard.set(newAnimal, forKey: animalKey)
|
||||
}
|
||||
|
||||
#expect(model.animal == newAnimal)
|
||||
}
|
||||
|
||||
@available(macOS 14, iOS 17, tvOS 17, watchOS 10, visionOS 1, *)
|
||||
@Test
|
||||
func testMacroWithProperty() async {
|
||||
let model = TestModelWithProperty()
|
||||
#expect(model.animal == defaultAnimal)
|
||||
|
||||
let userDefaultsValue = UserDefaults.standard.string(forKey: animalKey)
|
||||
#expect(userDefaultsValue == defaultAnimal)
|
||||
|
||||
await confirmation { confirmation in
|
||||
_ = withObservationTracking {
|
||||
model.animal
|
||||
} onChange: {
|
||||
confirmation()
|
||||
}
|
||||
|
||||
UserDefaults.standard.set(newAnimal, forKey: animalKey)
|
||||
}
|
||||
|
||||
#expect(model.animal == newAnimal)
|
||||
}
|
||||
|
||||
@available(macOS 14, iOS 17, tvOS 17, watchOS 10, visionOS 1, *)
|
||||
@Test
|
||||
func testMacroWithMultipleValues() async {
|
||||
let model = TestModelWithMultipleValues()
|
||||
#expect(model.animal == defaultAnimal)
|
||||
#expect(model.color == defaultColor)
|
||||
|
||||
await confirmation(expectedCount: 2) { confirmation in
|
||||
_ = withObservationTracking {
|
||||
model.animal
|
||||
} onChange: {
|
||||
confirmation()
|
||||
}
|
||||
|
||||
_ = withObservationTracking {
|
||||
model.color
|
||||
} onChange: {
|
||||
confirmation()
|
||||
}
|
||||
|
||||
UserDefaults.standard.set(newAnimal, forKey: animalKey)
|
||||
UserDefaults.standard.set(newColor, forKey: colorKey)
|
||||
}
|
||||
|
||||
#expect(model.animal == newAnimal)
|
||||
#expect(model.color == newColor)
|
||||
}
|
||||
}
|
23
readme.md
23
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,27 @@ Note that it's `@Default`, not `@Defaults`.
|
|||
|
||||
You cannot use `@Default` in an `ObservableObject`. It's meant to be used in a `View`.
|
||||
|
||||
#### `@ObservableDefault` in `@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`):
|
||||
|
||||
> [!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
|
||||
|
||||
@Observable
|
||||
final class UnicornManager {
|
||||
@ObservableDefault(.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.
|
||||
|
|
Loading…
Reference in New Issue