Skip to content

Commit

Permalink
add observeFirst mode, and update doc
Browse files Browse the repository at this point in the history
  • Loading branch information
fatbobman committed Oct 10, 2024
1 parent 65fe9d1 commit c401204
Show file tree
Hide file tree
Showing 4 changed files with 49 additions and 19 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ The library provides additional macros for finer control:
- `@ObservableOnly`: The property is observable but not stored in `UserDefaults`.
- `@Ignore`: The property is neither observable nor stored in `UserDefaults`.
- `@DefaultsKey`: Specifies a custom `UserDefaults` key for the property.
- `@DefaultsBacked`: The property is stored in `UserDefaults` and observable.

#### Example

Expand Down Expand Up @@ -125,6 +126,7 @@ public init(
- `userDefaults`: The `UserDefaults` instance to use (default is `.standard`).
- `ignoreExternalChanges`: If `true`, the instance ignores external `UserDefaults` changes (default is `false`).
- `prefix`: A prefix for all `UserDefaults` keys associated with this class. The prefix must not contain '.' characters.
- `observeFirst`: Observation priority mode. When enabled (set to true), only properties explicitly marked with `@DefaultsBacked` will correspond to UserDefaults, while others will be treated as ObservableOnly. The default value is false

#### Example Usage

Expand Down Expand Up @@ -196,6 +198,16 @@ struct ContentView: View {
}
```

## Observe First Mode

You can enable this mode by setting the observeFirst parameter in the `@ObservableDefaults` macro:

```swift
@ObservableDefaults(observeFirst: true)
```

When this mode is enabled, only properties explicitly marked with `@DefaultsBacked` will be persisted to UserDefaults. All other properties will automatically have the `@ObservableOnly` macro applied, making them observable but not persisted. Think of this as the inverse of the standard mode, focusing on observability while adding persistence capabilities to individual properties as needed.

## Important Notes

- **External Changes**: By default, `ObservableDefaults` instances respond to external changes in `UserDefaults`. You can disable this by setting `ignoreExternalChanges` to `true`.
Expand Down
3 changes: 2 additions & 1 deletion Sources/ObservableDefaults/Macros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ public macro ObservableOnly() = #externalMacro(module: "ObservableDefaultsMacros
// - ignoreExternalChanges: Ignore notifications from external modifications. When true, the instance will only respond to its own data modifications
// - suiteName: The suite name for UserDefaults
// - prefix: Prefix for UserDefaults keys. Note: It cannot contain the '.' character
// - observeFirst: Observation priority mode. When enabled (set to true), only properties explicitly marked with `@DefaultsBacked` will correspond to UserDefaults, while others will be treated as ObservableOnly. The default value is false
// If a parameter is set in both the macro and the initializer, the value in the initializer takes precedence
@attached(member, names: named(_$observationRegistrar), named(_userDefaults), named(_isExternalNotificationDisabled), named(access), named(withMutation), named(getValue), named(setValue), named(UserDefaultsWrapper), named(init), named(_prefix), named(cancellables), named(setupUserDefaultsObservation),
named(checkForChanges), named(observer), named(DefaultsObservation), named(observerStarter))
@attached(extension, conformances: Observable)
@attached(memberAttribute)
public macro ObservableDefaults(autoInit: Bool = true, ignoreExternalChanges: Bool = false, suiteName: String? = nil, prefix: String? = nil) = #externalMacro(module: "ObservableDefaultsMacros", type: "ObservableDefaultsMacros")
public macro ObservableDefaults(autoInit: Bool = true, ignoreExternalChanges: Bool = false, suiteName: String? = nil, prefix: String? = nil, observeFirst: Bool = false) = #externalMacro(module: "ObservableDefaultsMacros", type: "ObservableDefaultsMacros")
6 changes: 4 additions & 2 deletions Sources/ObservableDefaultsClient/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@
import Foundation
import ObservableDefaults

@ObservableDefaults
@ObservableDefaults(observeFirst: true)
public class Test {
@DefaultsKey(userDefaultsKey: "firstName")
@DefaultsBacked
public var name: String = "abc"

@ObservableOnly
var age: String?

var sex: Bool = false
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public enum ObservableDefaultsMacros {
static let ignoreExternalChanges: String = "ignoreExternalChanges"
static let suiteName: String = "suiteName"
static let prefix: String = "prefix"
static let observeFirst: String = "observeFirst"
}

extension ObservableDefaultsMacros: MemberMacro {
Expand All @@ -30,7 +31,7 @@ extension ObservableDefaultsMacros: MemberMacro {

let className = IdentifierPatternSyntax(identifier: .init(stringLiteral: "\(identifier.name.trimmed)"))

let (autoInit, suiteName, prefix, ignoreExternalChanges) = extractProperty(node)
let (autoInit, suiteName, prefix, ignoreExternalChanges, _) = extractProperty(node)

// Traverse all members and get all members with isPersistent set to true
guard let classDecl = declaration as? ClassDeclSyntax else {
Expand Down Expand Up @@ -189,12 +190,12 @@ extension ObservableDefaultsMacros: MemberMacro {
}
"""
let observerStarterSyntax: DeclSyntax =
"""
private func observerStarter() {
observer = DefaultsObservation(host: self, userDefaults: _userDefaults, prefix: _prefix)
}
"""
"""
private func observerStarter() {
observer = DefaultsObservation(host: self, userDefaults: _userDefaults, prefix: _prefix)
}
"""

return [
registrarSyntax,
accessFunctionSyntax,
Expand Down Expand Up @@ -228,21 +229,29 @@ extension ObservableDefaultsMacros: ExtensionMacro {

extension ObservableDefaultsMacros: MemberAttributeMacro {
public static func expansion(
of _: AttributeSyntax,
of node: AttributeSyntax,
attachedTo _: some DeclGroupSyntax,
providingAttributesFor member: some DeclSyntaxProtocol,
in _: some MacroExpansionContext
) throws -> [SwiftSyntax.AttributeSyntax] {
let (_, _, _, _, observeFirst) = extractProperty(node)
guard let varDecl = member.as(VariableDeclSyntax.self),
varDecl.isPersistent
varDecl.isObservable
else {
return []
}
return [
"""
@\(raw: DefaultsBackedMacro.name)
""",
]

if observeFirst {
if !varDecl.hasAttribute(named: DefaultsBackedMacro.name) && !varDecl.hasAttribute(named: ObservableOnlyMacro.name){
return [ "@\(raw: ObservableOnlyMacro.name)"]
}
} else {
if varDecl.isPersistent {
return [ "@\(raw: DefaultsBackedMacro.name)" ]
}
}

return []
}
}

Expand All @@ -255,12 +264,14 @@ extension ObservableDefaultsMacros {
autoInit: Bool,
suiteName: String?,
prefix: String?,
ignoreExternalChanges: Bool
ignoreExternalChanges: Bool,
observeFirst: Bool
) {
var autoInit = true
var suiteName: String?
var prefix: String?
var ignoreExternalChanges = false
var observeFirst = false

if let argumentList = node.arguments?.as(LabeledExprListSyntax.self) {
for argument in argumentList {
Expand All @@ -280,9 +291,13 @@ extension ObservableDefaultsMacros {
let stringLiteral = argument.expression.as(StringLiteralExprSyntax.self)
{
prefix = stringLiteral.trimmedDescription
} else if argument.label?.text == ObservableDefaultsMacros.observeFirst,
let booleanLiteral = argument.expression.as(BooleanLiteralExprSyntax.self)
{
observeFirst = booleanLiteral.literal.text == "true"
}
}
}
return (autoInit, suiteName, prefix, ignoreExternalChanges)
return (autoInit, suiteName, prefix, ignoreExternalChanges, observeFirst)
}
}

0 comments on commit c401204

Please sign in to comment.