├── .gitignore
├── Sources
├── UserDefaultsObservation
│ ├── Errors
│ │ └── UbiquitousKeyValueStoreError.swift
│ ├── Protocols
│ │ ├── UserDefaultsObservable.swift
│ │ └── UserDefaultsPropertyListValue.swift
│ ├── UbiquitousKeyValueStore
│ │ ├── UbiquitousKeyValueStoreChangeReasonAction.swift
│ │ ├── UbiquitousKeyValueStoreWrapper.swift
│ │ └── UbiquitousKeyValueStoreUtility.swift
│ ├── UserDefaultsObservationMacroDefinitions.swift
│ └── UserDefaults
│ │ └── UserDefaultsWrapper.swift
├── UserDefaultsObservationMacros
│ ├── Shared
│ │ └── MacroIdentifiers.swift
│ ├── Extensions
│ │ ├── PatternBindingListSyntaxExtensions.swift
│ │ ├── CollectionExtensions.swift
│ │ ├── LabeledExprListSyntaxElementExtension.swift
│ │ ├── MemberBlockItemSyntaxExtensions.swift
│ │ └── DeclSyntaxProtocolExtensions.swift
│ ├── Macros
│ │ ├── ObservableUserDefaultsIgnored.swift
│ │ ├── UserDefaults
│ │ │ ├── UserDefaultsStoreMacros.swift
│ │ │ └── ObservableUserDefaultsPropertyMacros.swift
│ │ ├── Class Macros
│ │ │ └── ObservableUserDefaultsMacros.swift
│ │ └── UbiquitousKeyValueStore
│ │ │ └── UbiquitousKeyValueStoreBackedMacro.swift
│ └── UserDefaultsObservationMacro.swift
└── UserDefaultsObservationClient
│ └── main.swift
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── Package.resolved
├── Tests
└── UserDefaultsObservationTests
│ └── UserDefaultsObservationTests.swift
├── LICENSE
├── Package.swift
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | xcuserdata/
5 | DerivedData/
6 | .swiftpm/configuration/registries.json
7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8 | .netrc
9 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsObservation/Errors/UbiquitousKeyValueStoreError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Taylor Geisse on 3/25/24.
6 | //
7 |
8 | import Foundation
9 |
10 | public enum UbiquitousKeyValueStoreError: Error {
11 | case SelfReferenceRemoved
12 | }
13 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsObservation/Protocols/UserDefaultsObservable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Taylor Geisse on 8/21/23.
6 | //
7 |
8 | import Observation
9 |
10 | @available(macOS 14.0, iOS 17.0, tvOS 17.0, macCatalyst 17.0, watchOS 10.0, visionOS 1.0, *)
11 | public protocol UserDefaultsObservable: Observable {
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "swift-syntax",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/swiftlang/swift-syntax",
7 | "state" : {
8 | "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d",
9 | "version" : "509.1.1"
10 | }
11 | }
12 | ],
13 | "version" : 2
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsObservation/UbiquitousKeyValueStore/UbiquitousKeyValueStoreChangeReasonAction.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Taylor Geisse on 3/15/24.
6 | //
7 |
8 | import Foundation
9 |
10 | public enum UbiquitousKeyValueStoreChangeReasonAction: String {
11 | case defaultValue
12 | case cachedValue
13 | case cloudValue
14 | case ignore
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsObservationMacros/Shared/MacroIdentifiers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Taylor Geisse on 3/23/24.
6 | //
7 |
8 | import Foundation
9 |
10 | internal enum MacroIdentifiers: String, CaseIterable {
11 | case ObservableUserDefaults
12 | case ObservableUserDefaultsProperty
13 | case UserDefaultsProperty
14 | case ObservableUserDefaultsIgnored
15 | case ObservableUserDefaultsStore
16 | case UserDefaultsStore
17 | case CloudProperty
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsObservationMacros/Extensions/PatternBindingListSyntaxExtensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Taylor Geisse on 3/24/24.
6 | //
7 |
8 | import SwiftSyntax
9 |
10 | extension PatternBindingListSyntax.Element {
11 | var asDefaultValue: DeclSyntax {
12 | """
13 | let defaultValue\(raw: self.typeAnnotation == nil ? "" : "\(self.typeAnnotation!)")\(raw: self.initializer == nil ? " = nil" : "\(self.initializer!)")
14 | """
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsObservationMacros/Extensions/CollectionExtensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Taylor Geisse on 3/23/24.
6 | //
7 |
8 | import Foundation
9 |
10 | internal extension Collection where Element: RawRepresentable & Equatable {
11 | func contains(_ element: RawType) -> Bool where RawType == Element.RawValue {
12 | guard let element = Element(rawValue: element) else {
13 | return false
14 | }
15 | return self.contains(element)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsObservationMacros/Macros/ObservableUserDefaultsIgnored.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Taylor Geisse on 7/9/23.
6 | //
7 |
8 | import SwiftSyntax
9 | import SwiftSyntaxMacros
10 |
11 | public struct ObservableUserDefaultsIgnoredMacros: PeerMacro {
12 | public static func expansion(of node: SwiftSyntax.AttributeSyntax, providingPeersOf declaration: some SwiftSyntax.DeclSyntaxProtocol, in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.DeclSyntax] {
13 | return []
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsObservationMacros/Macros/UserDefaults/UserDefaultsStoreMacros.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Taylor Geisse on 7/9/23.
6 | //
7 |
8 | import SwiftSyntax
9 | import SwiftSyntaxMacros
10 |
11 | public struct UserDefaultsStoreMacros: PeerMacro {
12 | public static func expansion(of node: SwiftSyntax.AttributeSyntax, providingPeersOf declaration: some SwiftSyntax.DeclSyntaxProtocol, in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.DeclSyntax] {
13 | return []
14 | }
15 |
16 | }
17 |
18 |
19 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsObservationMacros/Extensions/LabeledExprListSyntaxElementExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Taylor Geisse on 3/25/24.
6 | //
7 |
8 | import SwiftSyntax
9 |
10 | extension LabeledExprListSyntax.Element {
11 | func getString() -> TokenSyntax? {
12 | self.expression.as(StringLiteralExprSyntax.self)?
13 | .segments.as(StringLiteralSegmentListSyntax.self)?
14 | .first?.as(StringSegmentSyntax.self)?
15 | .content
16 | }
17 |
18 | func getMemberIdentifier() -> String? {
19 | self.expression.as(MemberAccessExprSyntax.self)?
20 | .declName.as(DeclReferenceExprSyntax.self)?
21 | .baseName
22 | .text
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsObservationMacros/Extensions/MemberBlockItemSyntaxExtensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Taylor Geisse on 3/23/24.
6 | //
7 |
8 | import SwiftSyntax
9 |
10 | internal extension MemberBlockItemSyntax {
11 | var isUserDefaultsStoreVariable: Bool {
12 | guard let attributeName = decl.as(VariableDeclSyntax.self)?
13 | .attributes.first?.as(AttributeSyntax.self)?
14 | .attributeName.as(IdentifierTypeSyntax.self)?
15 | .name.trimmedDescription
16 | else { return false }
17 |
18 | return [MacroIdentifiers.ObservableUserDefaultsStore, .UserDefaultsStore].contains(attributeName)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Tests/UserDefaultsObservationTests/UserDefaultsObservationTests.swift:
--------------------------------------------------------------------------------
1 | import SwiftSyntaxMacros
2 | import SwiftSyntaxMacrosTestSupport
3 | import XCTest
4 | import UserDefaultsObservationMacros
5 |
6 | let testMacros: [String: Macro.Type] = [:
7 | //"stringify": StringifyMacro.self,
8 | ]
9 |
10 | final class UserDefaultsObservationTests: XCTestCase {
11 | func testMacro() {
12 | /* assertMacroExpansion(
13 | """
14 | #stringify(a + b)
15 | """,
16 | expandedSource: """
17 | (a + b, "a + b")
18 | """,
19 | macros: testMacros
20 | ) */
21 | }
22 |
23 | func testMacroWithStringLiteral() {
24 | /* assertMacroExpansion(
25 | #"""
26 | #stringify("Hello, \(name)")
27 | """#,
28 | expandedSource: #"""
29 | ("Hello, \(name)", #""Hello, \(name)""#)
30 | """#,
31 | macros: testMacros
32 | ) */
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsObservationMacros/UserDefaultsObservationMacro.swift:
--------------------------------------------------------------------------------
1 | import SwiftCompilerPlugin
2 | import SwiftSyntax
3 | import SwiftSyntaxBuilder
4 | import SwiftSyntaxMacros
5 | import Foundation
6 |
7 | // MARK: - Error Enumerator
8 | public enum ObservableUserDefaultsError: Error, LocalizedError {
9 | case valueTypeNotSupported(String)
10 | case varValueRequired
11 |
12 | public var errorDescription: String? {
13 | switch self {
14 | case .varValueRequired: return "A 'var' declaration is required"
15 | case .valueTypeNotSupported(let type): return "\(type) is not supported"
16 | }
17 | }
18 | }
19 |
20 | @main
21 | struct UserDefaultsObservationPlugin: CompilerPlugin {
22 | let providingMacros: [Macro.Type] = [
23 | ObservableUserDefaultsMacros.self,
24 | ObservableUserDefaultsPropertyMacros.self,
25 | ObservableUserDefaultsIgnoredMacros.self,
26 | UserDefaultsStoreMacros.self,
27 | UbiquitousKeyValueStoreBackedMacro.self,
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Taylor Geisse
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsObservationMacros/Extensions/DeclSyntaxProtocolExtensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Taylor Geisse on 3/23/24.
6 | //
7 |
8 | import SwiftSyntax
9 |
10 | internal extension DeclSyntaxProtocol {
11 | var shouldAddObservableAttribute: Bool {
12 | guard let property = self.as(VariableDeclSyntax.self),
13 | let binding = property.bindings.first,
14 | let identifer = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier
15 | else {
16 | return false
17 | }
18 |
19 | if let hasAttribute = property.attributes.as(AttributeListSyntax.self)?
20 | .first?.as(AttributeSyntax.self)?.attributeName.trimmedDescription {
21 |
22 | let skipAttributes = MacroIdentifiers.allCases
23 |
24 | if skipAttributes.contains(hasAttribute) {
25 | return false
26 | }
27 | }
28 |
29 | return binding.accessorBlock == nil && identifer.text != "_$observationRegistrar" && identifer.text != "_$userDefaultStore"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsObservationClient/main.swift:
--------------------------------------------------------------------------------
1 | import UserDefaultsObservation
2 | import Foundation
3 |
4 | @available(macOS 14.0, iOS 17.0, tvOS 17.0, macCatalyst 17.0, watchOS 10.0, visionOS 1.0, *)
5 | @ObservableUserDefaults
6 | class MySampleClass {
7 | var firstUse = false
8 |
9 | @ObservableUserDefaultsProperty("OldInterface")
10 | var oldInterface = true
11 |
12 | @CloudProperty(key: "username_key",
13 | userDefaultKey: "differentUserDefaultKey",
14 | onStoreServerChange: .cloudValue,
15 | onInitialSyncChange: .defaultValue,
16 | onAccountChange: .cachedValue)
17 | var username: String?
18 |
19 | @CloudProperty(key: "allEventsIgnored", onInitialSyncChange: .cachedValue)
20 | var allEventsIgnored = true
21 |
22 | @UserDefaultsProperty(key: "myOldUserDefaultsKey")
23 | var olderKey = 50
24 |
25 | var somethingElse: URL = URL(string: "http://google.com")!
26 |
27 | @ObservableUserDefaultsIgnored
28 | var myUntrackedNonUserDefaultsProperty = true
29 |
30 |
31 | @UserDefaultsStore
32 | var myStore: UserDefaults
33 |
34 | init(_ store: UserDefaults = .standard) {
35 | self.myStore = store
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsObservation/Protocols/UserDefaultsPropertyListValue.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Taylor Geisse on 7/9/23.
6 | //
7 |
8 | import Foundation
9 |
10 | // MARK: - Protocol indiciating supported types
11 | public protocol UserDefaultsPropertyListValue {}
12 |
13 | extension NSData: UserDefaultsPropertyListValue {}
14 | extension Data: UserDefaultsPropertyListValue {}
15 |
16 | extension NSString: UserDefaultsPropertyListValue {}
17 | extension String: UserDefaultsPropertyListValue {}
18 |
19 | extension NSURL: UserDefaultsPropertyListValue {}
20 | extension URL: UserDefaultsPropertyListValue {}
21 |
22 | extension NSDate: UserDefaultsPropertyListValue {}
23 | extension Date: UserDefaultsPropertyListValue {}
24 |
25 | extension NSNumber: UserDefaultsPropertyListValue {}
26 | extension Bool: UserDefaultsPropertyListValue {}
27 | extension Int: UserDefaultsPropertyListValue {}
28 | extension Int8: UserDefaultsPropertyListValue {}
29 | extension Int16: UserDefaultsPropertyListValue {}
30 | extension Int32: UserDefaultsPropertyListValue {}
31 | extension Int64: UserDefaultsPropertyListValue {}
32 | extension UInt: UserDefaultsPropertyListValue {}
33 | extension UInt8: UserDefaultsPropertyListValue {}
34 | extension UInt16: UserDefaultsPropertyListValue {}
35 | extension UInt32: UserDefaultsPropertyListValue {}
36 | extension UInt64: UserDefaultsPropertyListValue {}
37 | extension Double: UserDefaultsPropertyListValue {}
38 | extension Float: UserDefaultsPropertyListValue {}
39 |
40 | extension Array: UserDefaultsPropertyListValue where Element: UserDefaultsPropertyListValue {}
41 | extension Dictionary: UserDefaultsPropertyListValue where Key == String, Value: UserDefaultsPropertyListValue {}
42 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsObservationMacros/Macros/UserDefaults/ObservableUserDefaultsPropertyMacros.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Taylor Geisse on 6/22/23.
6 | //
7 |
8 | import SwiftSyntax
9 | import SwiftSyntaxMacros
10 |
11 | public struct ObservableUserDefaultsPropertyMacros: AccessorMacro {
12 | public static func expansion(
13 | of node: AttributeSyntax,
14 | providingAccessorsOf declaration: some DeclSyntaxProtocol,
15 | in context: some MacroExpansionContext
16 | ) throws -> [AccessorDeclSyntax] {
17 | guard let property = declaration.as(VariableDeclSyntax.self),
18 | let binding = property.bindings.first,
19 | let identifier = binding.pattern.as(IdentifierPatternSyntax.self),
20 | binding.accessorBlock == nil,
21 | let key = node.as(AttributeSyntax.self)?
22 | .arguments?.as(LabeledExprListSyntax.self)?.first?
23 | .expression.as(StringLiteralExprSyntax.self)?
24 | .segments.first?.as(StringSegmentSyntax.self)?.content
25 | else { return [] }
26 |
27 | if "\(property.bindingSpecifier)".contains("var") == false {
28 | throw ObservableUserDefaultsError.varValueRequired
29 | }
30 |
31 | let getAccessor: AccessorDeclSyntax =
32 | """
33 | get {
34 | access(keyPath: \\.\(identifier))
35 | \(binding.asDefaultValue)
36 | return UserDefaultsWrapper.getValue(\"\(key)\", defaultValue, _$userDefaultStore)
37 | }
38 | """
39 |
40 | let setAccessor: AccessorDeclSyntax =
41 | """
42 | set {
43 | withMutation(keyPath: \\.\(identifier)) {
44 | UserDefaultsWrapper.setValue(\"\(key)\", newValue, _$userDefaultStore)
45 | }
46 | }
47 | """
48 |
49 | return [
50 | getAccessor,
51 | setAccessor
52 | ]
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsObservation/UserDefaultsObservationMacroDefinitions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Taylor Geisse, June 22nd, 2023
3 | //
4 | //
5 |
6 | import Foundation
7 |
8 | // MARK: - Macro definitions
9 |
10 | /**
11 | A macro that transforms properties to UserDefaults backed variables.
12 | */
13 | @attached(member, names: named(_$observationRegistrar), named(_$userDefaultStore), named(access), named(withMutation), named(getValue), named(setValue), named(UserDefaultsWrapper))
14 | @attached(memberAttribute)
15 | @attached(extension, conformances: Observable)
16 | public macro ObservableUserDefaults() = #externalMacro(module: "UserDefaultsObservationMacros", type: "ObservableUserDefaultsMacros")
17 |
18 | @attached(accessor)
19 | public macro UserDefaultsProperty(key: String) = #externalMacro(module: "UserDefaultsObservationMacros", type: "ObservableUserDefaultsPropertyMacros")
20 |
21 | @attached(accessor)
22 | public macro ObservableUserDefaultsProperty(_ key: String) = #externalMacro(module: "UserDefaultsObservationMacros", type: "ObservableUserDefaultsPropertyMacros")
23 |
24 | @attached(peer)
25 | public macro ObservableUserDefaultsIgnored() = #externalMacro(module: "UserDefaultsObservationMacros", type: "ObservableUserDefaultsIgnoredMacros")
26 |
27 | @attached(peer)
28 | public macro UserDefaultsStore() = #externalMacro(module: "UserDefaultsObservationMacros", type: "UserDefaultsStoreMacros")
29 |
30 | @attached(peer)
31 | public macro ObservableUserDefaultsStore() = #externalMacro(module: "UserDefaultsObservationMacros", type: "UserDefaultsStoreMacros")
32 |
33 | @attached(accessor)
34 | public macro CloudProperty(key: String,
35 | userDefaultKey: String = "defaultsSameAsKey",
36 | onStoreServerChange: UbiquitousKeyValueStoreChangeReasonAction = .ignore,
37 | onInitialSyncChange: UbiquitousKeyValueStoreChangeReasonAction = .ignore,
38 | onAccountChange: UbiquitousKeyValueStoreChangeReasonAction = .ignore)
39 | = #externalMacro(module: "UserDefaultsObservationMacros", type: "UbiquitousKeyValueStoreBackedMacro")
40 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.9
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 | import CompilerPluginSupport
6 |
7 | let package = Package(
8 | name: "UserDefaultsObservation",
9 | platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13), .visionOS(.v1)],
10 | products: [
11 | // Products define the executables and libraries a package produces, making them visible to other packages.
12 | .library(
13 | name: "UserDefaultsObservation",
14 | targets: ["UserDefaultsObservation"]
15 | ),
16 | .executable(
17 | name: "UserDefaultsObservationClient",
18 | targets: ["UserDefaultsObservationClient"]
19 | ),
20 | ],
21 | dependencies: [
22 | // Depend on the latest Swift 5.9 prerelease of SwiftSyntax
23 | .package(url: "https://github.com/swiftlang/swift-syntax", from: "509.0.0"),
24 | ],
25 | targets: [
26 | // Targets are the basic building blocks of a package, defining a module or a test suite.
27 | // Targets can depend on other targets in this package and products from dependencies.
28 | // Macro implementation that performs the source transformation of a macro.
29 | .macro(
30 | name: "UserDefaultsObservationMacros",
31 | dependencies: [
32 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
33 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax")
34 | ]
35 | ),
36 |
37 | // Library that exposes a macro as part of its API, which is used in client programs.
38 | .target(name: "UserDefaultsObservation", dependencies: ["UserDefaultsObservationMacros"]),
39 |
40 | // A client of the library, which is able to use the macro in its own code.
41 | .executableTarget(name: "UserDefaultsObservationClient", dependencies: ["UserDefaultsObservation"]),
42 |
43 | // A test target used to develop the macro implementation.
44 | .testTarget(
45 | name: "UserDefaultsObservationTests",
46 | dependencies: [
47 | "UserDefaultsObservationMacros",
48 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
49 | ]
50 | ),
51 | ]
52 | )
53 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsObservation/UserDefaults/UserDefaultsWrapper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Taylor Geisse on 7/9/23.
6 | //
7 |
8 | import Foundation
9 |
10 | // MARK: - UserDefautls Wrapper
11 | public struct UserDefaultsWrapper {
12 | private init() {}
13 |
14 | // MARK: - Get Values
15 | public static nonisolated func getValue(_ key: String, _ defaultValue: Value, _ store: UserDefaults) -> Value
16 | where Value: RawRepresentable, Value.RawValue: UserDefaultsPropertyListValue
17 | {
18 | guard let rawValue = store.object(forKey: key) as? Value.RawValue else {
19 | return defaultValue
20 | }
21 | return Value(rawValue: rawValue) ?? defaultValue
22 | }
23 |
24 | public static nonisolated func getValue(_ key: String, _ defaultValue: Value, _ store: UserDefaults) -> Value
25 | where Value == R?, R: RawRepresentable, R.RawValue: UserDefaultsPropertyListValue
26 | {
27 | guard let rawValue = store.object(forKey: key) as? R.RawValue else {
28 | return defaultValue
29 | }
30 | return R(rawValue: rawValue) ?? defaultValue
31 | }
32 |
33 | public static nonisolated func getValue(_ key: String, _ defaultValue: Value, _ store: UserDefaults) -> Value
34 | where Value: UserDefaultsPropertyListValue
35 | {
36 | return store.object(forKey: key) as? Value ?? defaultValue
37 | }
38 |
39 | public static nonisolated func getValue(_ key: String, _ defaultValue: Value, _ store: UserDefaults) -> Value
40 | where Value == R?, R: UserDefaultsPropertyListValue
41 | {
42 | return store.object(forKey: key) as? R ?? defaultValue
43 | }
44 |
45 | // MARK: - Set Values
46 | public static nonisolated func setValue(_ key: String, _ newValue: Value, _ store: UserDefaults)
47 | where Value: RawRepresentable, Value.RawValue: UserDefaultsPropertyListValue
48 | {
49 | store.set(newValue.rawValue, forKey: key)
50 | }
51 |
52 | public static nonisolated func setValue(_ key: String, _ newValue: Value, _ store: UserDefaults)
53 | where Value == R?, R: RawRepresentable, R.RawValue: UserDefaultsPropertyListValue
54 | {
55 | store.set(newValue?.rawValue, forKey: key)
56 | }
57 |
58 | public static nonisolated func setValue(_ key: String, _ newValue: Value, _ store: UserDefaults)
59 | where Value: UserDefaultsPropertyListValue
60 | {
61 | store.set(newValue, forKey: key)
62 | }
63 |
64 | public static nonisolated func setValue(_ key: String, _ newValue: Value, _ store: UserDefaults)
65 | where Value == R?, R: UserDefaultsPropertyListValue
66 | {
67 | store.set(newValue, forKey: key)
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsObservation/UbiquitousKeyValueStore/UbiquitousKeyValueStoreWrapper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Taylor Geisse on 3/12/24.
6 | //
7 |
8 | import Foundation
9 |
10 | // MARK: - NSUbiquitousKeyValueStore Wrapper
11 | public struct UbiquitousKeyValueStoreWrapper {
12 | private init() {}
13 |
14 | public static nonisolated func castAnyValue(_ anyValue: Any?, defaultValue: Value) -> Value
15 | where Value: RawRepresentable, Value.RawValue: UserDefaultsPropertyListValue
16 | {
17 | guard let anyValue = anyValue, let rawValue = anyValue as? Value.RawValue else {
18 | return defaultValue
19 | }
20 | return Value(rawValue: rawValue) ?? defaultValue
21 | }
22 |
23 | public static nonisolated func castAnyValue(_ anyValue: Any?, defaultValue: Value) -> Value
24 | where Value == R?, R: RawRepresentable, R.RawValue: UserDefaultsPropertyListValue
25 | {
26 | guard let anyValue = anyValue, let rawValue = anyValue as? R.RawValue else {
27 | return defaultValue
28 | }
29 | return R(rawValue: rawValue) ?? defaultValue
30 | }
31 |
32 | public static nonisolated func castAnyValue(_ anyValue: Any?, defaultValue: Value) -> Value
33 | where Value: UserDefaultsPropertyListValue
34 | {
35 | guard let anyValue = anyValue else { return defaultValue }
36 | return (anyValue as? Value) ?? defaultValue
37 | }
38 |
39 | public static nonisolated func castAnyValue(_ anyValue: Any?, defaultValue: Value) -> Value
40 | where Value == R?, R: UserDefaultsPropertyListValue
41 | {
42 | guard let anyValue = anyValue else { return defaultValue }
43 | return (anyValue as? R) ?? defaultValue
44 | }
45 |
46 |
47 | // MARK: - Set values on NSUbiquitousKeyValueStore
48 | public static nonisolated func set(_ newValue: Value, forKey key: String)
49 | where Value: RawRepresentable, Value.RawValue: UserDefaultsPropertyListValue
50 | {
51 | NSUbiquitousKeyValueStore.default.set(newValue.rawValue, forKey: key)
52 | }
53 |
54 | public static nonisolated func set(_ newValue: Value, forKey key: String)
55 | where Value == R?, R: RawRepresentable, R.RawValue: UserDefaultsPropertyListValue
56 | {
57 | NSUbiquitousKeyValueStore.default.set(newValue?.rawValue, forKey: key)
58 | }
59 |
60 | public static nonisolated func set(_ newValue: Value, forKey key: String)
61 | where Value: UserDefaultsPropertyListValue
62 | {
63 | NSUbiquitousKeyValueStore.default.set(newValue, forKey: key)
64 | }
65 |
66 | public static nonisolated func set(_ newValue: Value, forKey key: String)
67 | where Value == R?, R: UserDefaultsPropertyListValue
68 | {
69 | NSUbiquitousKeyValueStore.default.set(newValue, forKey: key)
70 | }
71 | }
72 |
73 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsObservation/UbiquitousKeyValueStore/UbiquitousKeyValueStoreUtility.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Taylor Geisse on 3/13/24.
6 | //
7 |
8 | import Foundation
9 |
10 | public class UbiquitousKeyValueStoreUtility {
11 | // MARK: - Typealiases
12 | public typealias UbiquitousKVSKey = String
13 | public typealias UbiquitousKVSReasonKey = Int
14 | public typealias UbiquitousKVSUpdateCallback = (UbiquitousKVSReasonKey) throws -> Void
15 |
16 | private var updateCallbacks: [UbiquitousKVSKey: UbiquitousKVSUpdateCallback] = [:]
17 | private var cachedUpdates: [UbiquitousKVSKey: [UbiquitousKVSReasonKey]] = [:]
18 | private var observerAdded = false
19 |
20 | // Make this a singleton
21 | static public let shared = UbiquitousKeyValueStoreUtility()
22 | private init() {
23 | addDidChangeExternallyNotificationObserver()
24 | synchronize()
25 | }
26 |
27 | // MARK: - Notification Registration
28 | internal func addDidChangeExternallyNotificationObserver() {
29 | if observerAdded { return }
30 | NotificationCenter.default.addObserver(self,
31 | selector: #selector(ubiquitousKeyValueStoreDidChange(_:)),
32 | name: NSUbiquitousKeyValueStore.didChangeExternallyNotification,
33 | object: NSUbiquitousKeyValueStore.default)
34 | observerAdded = true
35 | }
36 |
37 | public func synchronize() {
38 | if NSUbiquitousKeyValueStore.default.synchronize() == false {
39 | fatalError("This app was not built with the proper entitlement requests.")
40 | }
41 | }
42 |
43 | // MARK: - Update Callback Registration
44 | public func registerUpdateCallback(forKey key: String, callback: @escaping UbiquitousKVSUpdateCallback) {
45 | updateCallbacks[key] = callback
46 | processCache(forKey: key)
47 | }
48 |
49 | private func processCache(forKey key: String) {
50 | if cachedUpdates[key] == nil { return }
51 | cachedUpdates[key]?.forEach { executeUpdateCallback(forKey: key, reason: $0) }
52 | cachedUpdates[key] = nil
53 | }
54 |
55 | // MARK: - External Notification
56 | @objc func ubiquitousKeyValueStoreDidChange(_ notification: Notification) {
57 | guard let userInfo = notification.userInfo else { return }
58 | guard let reasonForChange = userInfo[NSUbiquitousKeyValueStoreChangeReasonKey] as? Int else { return }
59 | guard let keys = userInfo[NSUbiquitousKeyValueStoreChangedKeysKey] as? [String] else { return }
60 |
61 | keys.forEach { executeUpdateCallback(forKey: $0, reason: reasonForChange) }
62 | }
63 |
64 | private func executeUpdateCallback(forKey key: UbiquitousKVSKey, reason: UbiquitousKVSReasonKey) {
65 | guard let updateCallback = updateCallbacks[key] else {
66 | addKeyReasonToCache(forKey: key, reason: reason)
67 | return
68 | }
69 |
70 | do {
71 | try updateCallback(reason)
72 | } catch {
73 | addKeyReasonToCache(forKey: key, reason: reason)
74 | }
75 | }
76 |
77 | private func addKeyReasonToCache(forKey key: UbiquitousKVSKey, reason: UbiquitousKVSReasonKey) {
78 | cachedUpdates[key, default: []].append(reason)
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsObservationMacros/Macros/Class Macros/ObservableUserDefaultsMacros.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ObservableUserDefaultsMacros.swift
3 | //
4 | //
5 | // Created by Taylor Geisse, June 22nd, 2023
6 | //
7 |
8 | import SwiftSyntax
9 | import SwiftSyntaxMacros
10 |
11 | public struct ObservableUserDefaultsMacros {}
12 |
13 | extension ObservableUserDefaultsMacros: MemberMacro {
14 | public static func expansion(
15 | of node: AttributeSyntax,
16 | providingMembersOf declaration: some DeclGroupSyntax,
17 | in context: some MacroExpansionContext
18 | ) throws -> [DeclSyntax] {
19 |
20 | guard let identifier = declaration.asProtocol(NamedDeclSyntax.self) else { return [] }
21 |
22 | let className = IdentifierPatternSyntax(identifier: .init(stringLiteral: "\(identifier.name.trimmed)"))
23 |
24 | let registrar: DeclSyntax =
25 | """
26 | private let _$observationRegistrar = Observation.ObservationRegistrar()
27 | """
28 |
29 | let accessFunction: DeclSyntax =
30 | """
31 | internal nonisolated func access(keyPath: KeyPath<\(className), Member>) {
32 | _$observationRegistrar.access(self, keyPath: keyPath)
33 | }
34 | """
35 |
36 | let withMutationFunction: DeclSyntax =
37 | """
38 | internal nonisolated func withMutation(keyPath: KeyPath<\(className), Member>, _ mutation: () throws -> T) rethrows -> T {
39 | try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation)
40 | }
41 | """
42 |
43 | let userDefaultStore: DeclSyntax
44 |
45 | if let storeDefined = declaration.memberBlock.members.filter(\.isUserDefaultsStoreVariable).first?.as(MemberBlockItemSyntax.self),
46 | let storeVarIdentifier = storeDefined.decl.as(VariableDeclSyntax.self)?
47 | .bindings.as(PatternBindingListSyntax.self)?
48 | .first?.pattern.as(IdentifierPatternSyntax.self)?
49 | .identifier
50 | {
51 | userDefaultStore =
52 | """
53 | private var _$userDefaultStore: Foundation.UserDefaults { get { \(storeVarIdentifier) } }
54 | """
55 | } else {
56 | userDefaultStore =
57 | """
58 | private let _$userDefaultStore: Foundation.UserDefaults = .standard
59 | """
60 | }
61 |
62 | return [
63 | registrar,
64 | accessFunction,
65 | withMutationFunction,
66 | userDefaultStore
67 | ]
68 | }
69 | }
70 |
71 | extension ObservableUserDefaultsMacros: MemberAttributeMacro {
72 | public static func expansion(
73 | of node: AttributeSyntax,
74 | attachedTo declaration: some DeclGroupSyntax,
75 | providingAttributesFor member: some DeclSyntaxProtocol,
76 | in context: some MacroExpansionContext
77 | ) throws -> [SwiftSyntax.AttributeSyntax] {
78 | guard member.shouldAddObservableAttribute else { return [] }
79 |
80 | guard let className = declaration.as(ClassDeclSyntax.self)?.name.trimmed,
81 | let memberName = member.as(VariableDeclSyntax.self)?
82 | .bindings.as(PatternBindingListSyntax.self)?
83 | .first?.pattern.as(IdentifierPatternSyntax.self)?
84 | .identifier.trimmed
85 | else { return [] }
86 |
87 | return [
88 | AttributeSyntax(
89 | attributeName: IdentifierTypeSyntax(name: .identifier("\(MacroIdentifiers.UserDefaultsProperty.rawValue)(key: \"\(className).\(memberName)\")"))
90 | )
91 | ]
92 | }
93 | }
94 |
95 | extension ObservableUserDefaultsMacros: ExtensionMacro {
96 | public static func expansion(
97 | of node: AttributeSyntax,
98 | attachedTo declaration: some DeclGroupSyntax,
99 | providingExtensionsOf type: some TypeSyntaxProtocol,
100 | conformingTo protocols: [TypeSyntax],
101 | in context: some MacroExpansionContext
102 | ) throws -> [ExtensionDeclSyntax] {
103 | let udObservable: DeclSyntax =
104 | """
105 | extension \(type.trimmed): Observation.Observable {}
106 | """
107 |
108 | guard let ext = udObservable.as(ExtensionDeclSyntax.self) else { return [] }
109 | return [ext]
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/Sources/UserDefaultsObservationMacros/Macros/UbiquitousKeyValueStore/UbiquitousKeyValueStoreBackedMacro.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Taylor Geisse on 3/15/24.
6 | //
7 |
8 | import SwiftSyntax
9 | import SwiftSyntaxMacros
10 |
11 | internal enum ChangeReasonAction: String {
12 | case defaultValue
13 | case cachedValue
14 | case cloudValue
15 | case ignore
16 | }
17 |
18 | public struct UbiquitousKeyValueStoreBackedMacro: AccessorMacro {
19 | public static func expansion(
20 | of node: AttributeSyntax,
21 | providingAccessorsOf declaration: some DeclSyntaxProtocol,
22 | in context: some MacroExpansionContext
23 | ) throws -> [AccessorDeclSyntax] {
24 |
25 | guard let property = declaration.as(VariableDeclSyntax.self),
26 | let binding = property.bindings.first,
27 | let identifier = binding.pattern.as(IdentifierPatternSyntax.self),
28 | binding.accessorBlock == nil,
29 | let arguments = node.arguments?.as(LabeledExprListSyntax.self)
30 | else { return [] }
31 |
32 | var key: TokenSyntax? = nil
33 | var userDefaultKey: TokenSyntax? = nil
34 | var onStoreServerChange: ChangeReasonAction = .ignore
35 | var onInitialSyncChange: ChangeReasonAction = .ignore
36 | var onAccountChange: ChangeReasonAction = .ignore
37 |
38 | for arg in arguments {
39 | switch arg.label?.text {
40 | case "key":
41 | key = arg.getString()
42 | case "userDefaultKey":
43 | userDefaultKey = arg.getString()
44 | case "onStoreServerChange":
45 | guard let rawValue = arg.getMemberIdentifier() else { continue }
46 | guard let parsed = ChangeReasonAction(rawValue: rawValue) else { continue }
47 | onStoreServerChange = parsed
48 | case "onInitialSyncChange":
49 | guard let rawValue = arg.getMemberIdentifier() else { continue }
50 | guard let parsed = ChangeReasonAction(rawValue: rawValue) else { continue }
51 | onInitialSyncChange = parsed
52 | case "onAccountChange":
53 | guard let rawValue = arg.getMemberIdentifier() else { continue }
54 | guard let parsed = ChangeReasonAction(rawValue: rawValue) else { continue }
55 | onAccountChange = parsed
56 | default: continue
57 | }
58 | }
59 |
60 | if userDefaultKey == nil {
61 | userDefaultKey = key
62 | }
63 |
64 | guard let key = key, let userDefaultKey = userDefaultKey else {
65 | // TODO: This should produce an error informing the user of the fix
66 | return []
67 | }
68 |
69 | let ukvsCallback: TokenSyntax
70 |
71 | if onAccountChange == .ignore && onInitialSyncChange == .ignore && onStoreServerChange == .ignore {
72 | ukvsCallback =
73 | """
74 | _ in
75 | return
76 | """
77 | } else {
78 | ukvsCallback =
79 | """
80 | [weak self] event in
81 | guard let self = self else {
82 | throw UbiquitousKeyValueStoreError.SelfReferenceRemoved
83 | }
84 |
85 | switch event {
86 | case NSUbiquitousKeyValueStoreServerChange:
87 | \(changeActionDeclSyntax(identifier: identifier, uKvsKey: key, userDefaultKey: userDefaultKey, action: onStoreServerChange))
88 | case NSUbiquitousKeyValueStoreInitialSyncChange:
89 | \(changeActionDeclSyntax(identifier: identifier, uKvsKey: key, userDefaultKey: userDefaultKey, action: onInitialSyncChange))
90 | case NSUbiquitousKeyValueStoreAccountChange:
91 | \(changeActionDeclSyntax(identifier: identifier, uKvsKey: key, userDefaultKey: userDefaultKey, action: onAccountChange))
92 | default: return
93 | }
94 | """
95 | }
96 |
97 |
98 | let getAccessor: AccessorDeclSyntax =
99 | """
100 | get {
101 | access(keyPath: \\.\(identifier))
102 | \(binding.asDefaultValue)
103 | UbiquitousKeyValueStoreUtility.shared.registerUpdateCallback(forKey: \"\(key)\") { \(ukvsCallback)
104 | }
105 | return UserDefaultsWrapper.getValue(\"\(key)\", defaultValue, _$userDefaultStore)
106 | }
107 | """
108 |
109 | let setAccessor: AccessorDeclSyntax =
110 | """
111 | set {
112 | withMutation(keyPath: \\.\(identifier)) {
113 | UserDefaultsWrapper.setValue(\"\(userDefaultKey)\", newValue, _$userDefaultStore)
114 | UbiquitousKeyValueStoreWrapper.set(newValue, forKey: \"\(key)\")
115 | }
116 | }
117 | """
118 |
119 | return [
120 | getAccessor,
121 | setAccessor
122 | ]
123 | }
124 |
125 |
126 | private static func changeActionDeclSyntax(identifier: IdentifierPatternSyntax ,uKvsKey: TokenSyntax, userDefaultKey: TokenSyntax, action: ChangeReasonAction) -> DeclSyntax {
127 | return switch action {
128 | case .defaultValue:
129 | "self.\(identifier) = defaultValue"
130 | case .cachedValue:
131 | "self.\(identifier) = UserDefaultsWrapper.getValue(\"\(userDefaultKey)\", defaultValue, self._$userDefaultStore)"
132 | case .cloudValue:
133 | """
134 | let cloudValue = NSUbiquitousKeyValueStore.default.object(forKey: \"\(uKvsKey)\")
135 | self.\(identifier) = UbiquitousKeyValueStoreWrapper.castAnyValue(cloudValue, defaultValue: defaultValue)
136 | """
137 | case .ignore:
138 | "return"
139 | }
140 | }
141 | }
142 |
143 |
144 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # UserDefaultsObservation
3 |
4 | Combining UserDefaults with Observation, giving you the ability to easily create Observable UserDefaults-backed classes.
5 |
6 | 1. [Why UserDefaultsObservation?](#why-userdefaultsobservation)
7 | 2. [Requirements](#requirements)
8 | 3. [Installation](#installation)
9 | 4. [Usage](#usage)
10 | - [Creating a class](#creating-a-class)
11 | - [Defining the UserDefaults Key](#defining-the-userdefaults-key)
12 | - [Using a custom UserDefaults suite](#using-a-custom-userdefaults-suite)
13 | - [Compiler Flag Dependent UserDefaults suite](#compiler-flag-dependent-userdefaults-suite)
14 | - [Cloud Storage with NSUbiquitousKeyValueStore](#cloud-storage-with-nsubiquitouskeyvaluestore)
15 | - [Supported Types](#supported-types)
16 | - [What happens with unsupported Types](#unsupported-types)
17 | 5. [Change Log](#change-log)
18 |
19 |
20 | # Why UserDefaultsObservation
21 |
22 | Most applications use UserDefaults to store some user preferences, application defaults, and other pieces of information. In the world of SwiftUI, the `@AppStorage` property wrapper was introduced. This provided access to UserDefaults and a way to invalidate a View triggering a redraw.
23 |
24 | However, `@AppStorage` has a few limitations:
25 | 1. The initialization of it requires a string key and default value, making reuse difficult
26 | 2. It is advised against using it in non-SwiftUI code. While UserDefaults APIs are good, it is now a different method for accessing the same information.
27 | 3. Refactoring code can be cumbersome depending on how widespread your usage keys are
28 |
29 | Wrapping UserDefaults to provide a centralized location of maintaining keys and default values is one solution. However, it does not provide the view invalidating benefits of AppStorage. There are solutions to that as well, but they are sometimes not the most elegant.
30 |
31 | UserDefaultsObservation aims to solve these issues by:
32 | 1. Providing the ability to define any class as both UserDefaults-backed and Observable
33 | 2. Centralizing the definition of UserDefaults keys and their default values
34 | 3. Able to be used in both SwiftUI and non-SwiftUI code
35 |
36 | # Requirements
37 |
38 | * iOS 17.0+ | macOS 14.0+ | tvOS 17.0+ | watchOS 10.0+ | macCatalyst 17.0+
39 | * Xcode 15
40 |
41 |
42 | This package is built on Observation and Macros that are releasing in iOS 17, macOS 14.
43 |
44 | # Installation
45 |
46 | ## SwiftPM
47 |
48 | File > Add Package Dependencies. Use this URL in the search box: https://github.com/tgeisse/UserDefaultsObservation
49 |
50 | # Usage
51 |
52 | ## Creating a Class
53 |
54 | To create a class that is UserDefaults backed, import `Foundation` and `UserDefaultsObservation`. Then mark the class with the `@ObservableUserDefaults` macro. Define variables as you normally would:
55 |
56 | ```swift
57 | import Foundation
58 | import UserDefaultsObservation
59 |
60 | @ObservableUserDefaults
61 | class MySampleClass {
62 | var firstUse = false
63 | var username: String? = nil
64 | }
65 | ```
66 |
67 | Should you need to ignore a variable, use the `@ObservableUserDefaultsIgnored` macro. Note: variables with accessors will be ignored as if they have the `@ObservableUserDefaultsIgnored` macro attribute attached.
68 |
69 | ```swift
70 | @ObservableUserDefaults
71 | class MySampleClass {
72 | var firstUse = false
73 | var username: String? = nil
74 |
75 | @ObservableUserDefaultsIgnored
76 | var someIgnoredProperty = "hello world"
77 | }
78 | ```
79 |
80 | ## Defining the UserDefaults Key
81 |
82 | A default key is created for you as `{ClassName}.{PropertyName}`. In the example above, the keys would be the following:
83 | - "MySampleClass.firstUse"
84 | - "MySampleClass.username"
85 |
86 | In situations you need to control the key - e.g. refactoring or migrating existing keys - you can mark a property with the `@@UserDefaultsProperty` attribute and provide the full UserDefaults key as a parameter. As an example:
87 |
88 | ```swift
89 | @ObservableUserDefaults
90 | class MySampleClass {
91 | var firstUse = false
92 | var username: String? = nil
93 |
94 | @ObservableUserDefaultsIgnored
95 | var someIgnoredProperty = "hello world"
96 |
97 | @UserDefaultsProperty(key: "myPreviousKey")
98 | var existingUserDefaults: Bool = true
99 | }
100 | ```
101 |
102 | ## Using a custom UserDefaults suite
103 |
104 | To use a custom UserDefaults store, you can use the `@UserDefaultsStore` attribute to denote the UserDefaults store variable.
105 |
106 | ```swift
107 | @ObservableUserDefaults
108 | class MySampleClass {
109 | var firstUse = false
110 | var username: String? = nil
111 |
112 | @ObservableUserDefaultsIgnored
113 | var someIgnoredProperty = "hello world"
114 |
115 | @UserDefaultsProperty(key: "myPreviousKey")
116 | var existingUserDefaults: Bool = true
117 |
118 | @UserDefaultsStore
119 | var myStore = UserDefaults(suiteName: "MyStore.WithSuiteName.Example")
120 | }
121 | ```
122 |
123 | Should you need to change the store at runtime, one option is to do so with an initializer:
124 |
125 | ```swift
126 | @ObservableUserDefaults
127 | class MySampleClass {
128 | var firstUse = false
129 | var username: String? = nil
130 |
131 | @UserDefaultsStore
132 | var myStore: UserDefaults
133 |
134 | init(_ store: UserDefaults = .standard) {
135 | self.myStore = store
136 | }
137 | }
138 | ```
139 |
140 | ## Compiler Flag Dependent UserDefaults Suite
141 |
142 | If you would like to define the store using compiler flags, there are a few ways to accomplish this. The first is with a computed property:
143 |
144 | ```swift
145 | @ObservableUserDefaults
146 | class MySampleClass {
147 | var firstUse = false
148 | var username: String? = nil
149 |
150 | @ObservableUserDefaultsIgnored
151 | var someIgnoredProperty = "hello world"
152 |
153 | @UserDefaultsProperty(key: "myPreviousKey")
154 | var existingUserDefaults: Bool = true
155 |
156 | @UserDefaultsStore
157 | var myStore: UserDefaults {
158 | #if DEBUG
159 | return UserDefaults(suiteName: "myDebugStore.example")!
160 | #else
161 | return UserDefaults(suiteName: "myProductionStore.example")!
162 | #endif
163 | }
164 | }
165 | ```
166 |
167 | If computing this each time is not desired, then this is another option:
168 |
169 | ```swift
170 | @UserDefaultsStore
171 | var myStore: UserDefaults = {
172 | #if DEBUG
173 | return UserDefaults(suiteName: "myDebugStore.example")!
174 | #else
175 | return UserDefaults(suiteName: "myProductionStore.example")!
176 | #endif
177 | }()
178 | ```
179 |
180 | One more option is to put the compiler flag code into the initializer
181 |
182 | ```swift
183 | @UserDefaultsStore
184 | var myStore: UserDefaults
185 |
186 | init(_ store: UserDefaults = .standard) {
187 | #if DEBUG
188 | self.myStore = UserDefaults(suiteName: "myDebugStore.example")
189 | #else
190 | self.myStore = store
191 | #endif
192 | }
193 | ```
194 |
195 | ## Cloud Storage with NSUbiquitousKeyValueStore
196 |
197 | A property can be marked for storage in NSUbiquitousKeyValueStore by using the `@CloudProperty` attribute. This attribute will sync write changes to the cloud and use UserDefaults to cache values locally. The values will be cached in the [UserDefaults store](#using-a-custom-userdefaults-suite) of the class containing the CloudProperty.
198 |
199 | Let's take a look at syncing a user's favorite color:
200 |
201 | ```swift
202 | @ObservableUserDefaults
203 | class UsersPreferences {
204 | @CloudProperty(key: "yourCloudKey",
205 | userDefaultKey: "differentUserDefaultKey",
206 | onStoreServerChange: .cloudValue,
207 | onInitialSyncChange: .cloudValue,
208 | onAccountChange: .cachedValue)
209 | var favoriteColor: String = "Green"
210 | }
211 | ```
212 |
213 | This displays the parameters the user can or must define. Let's review each:
214 |
215 | - `key` - the key to be used for storing and retrieving values from NSUbiquitousKeyValueStore
216 | - `userDefaultKey` - an optionally provided key to be used by UserDefaults for storing values locally
217 | - `onStoreServerChange` - define the [value to use](#options-for-values-on-external-notifications) when a `NSUbiquitousKeyValueStoreServerChange` external notification is received. Default value: `ignore`
218 | - `onInitialSyncChange` - define the value to use when a `NSUbiquitousKeyValueStoreInitialSyncChange` external notification is received. Default value: `ignore`
219 | - `onAccountChange` - define the value to use when a `NSUbiquitousKeyValueStoreAccountChange` external notification is received. Default value: `ignore`
220 |
221 | > [!TIP]
222 | > Omitting the `userDefaultKey` parameter will use the value of the `key` parameter for UserDefaults.
223 |
224 | ### Options for Values on External Notifications
225 |
226 | For each external notification event reason, the user can select which value to use to update the cloud property. There are currently four options available:
227 |
228 | - `defaultValue` - this will use the default value provided. In the favorite color example above, this would be "Green"
229 | - `cachedValue` - the value cached in UserDefaults and is previous value set on the device
230 | - `cloudValue` - retrieves the cloud value and stores it locally
231 | - `ignore` - ignores the event
232 |
233 | ### Cloud Storage Quota Violation
234 |
235 | > [!WARNING]
236 | > This package does not take any action when the `NSUbiquitousKeyValueStoreQuotaViolationChange` external notification is received.
237 |
238 |
239 | ## Supported Types
240 |
241 | All of the following types are supported, including their optional counterparts:
242 | * NSData
243 | * Data
244 | * NSString
245 | * String
246 | * NSURL
247 | * URL
248 | * NSDate
249 | * Date
250 | * NSNumber
251 | * Bool
252 | * Int
253 | * Int8
254 | * Int16
255 | * Int32
256 | * Int64
257 | * UInt
258 | * UInt8
259 | * UInt16
260 | * UInt32
261 | * UInt64
262 | * Double
263 | * Float
264 |
265 | * RawRepresentable where RawType is in the list above
266 | * Array where Element is in the list above
267 | * Dictionary where Key == String && Value is in the list above
268 |
269 | ## Unsupported Types
270 |
271 | Unsupported times should throw an error during compile time. The error will be displayed as if it is in the macro, but it is likely the type that is the issue. Should this variable need to be kept on the class, then it may need to be `@ObservableUserDefaultsIgnored`.
272 |
273 | # Change Log
274 |
275 | ## 0.5.2
276 |
277 | * CloudProperty now defaults all events to `ignore`
278 | * Updates to ReadMe
279 |
280 | ## 0.5.1
281 |
282 | * Removed a warning that would be presented if using a RawRepresentable with a CloudProperty
283 | * Added extra type conformance for RawRepresentables
284 |
285 | ## 0.5.0
286 |
287 | **New Features and Code Organization**
288 | * Added support for Cloud Storage through NSUbiquitousKeyValueStore
289 | * Added new attributes that are shorter. Existing attributes that they complement are still available
290 | * Internal code structure cleanup
291 | * Readme updates
292 |
293 | ## 0.4.4
294 |
295 | * Updated the Package platform versions to align with Swift Macros platform versions
296 | * Added @available attribute to the protocol used to align with the Observation framework's platform availability
297 | * Added visionOS
298 |
299 | ## 0.4.3
300 |
301 | * Removed commented code that is no longer needed to keep around
302 | * Updated Readme
303 |
304 | ## 0.4.2
305 |
306 | * Updated to swift-syntax 509.0.0 minimum
307 | * Small internal code updates
308 |
309 | ## 0.4.1
310 |
311 | * Small syntax changes
312 |
313 | ## 0.4.0
314 | * No longer do you need to import Observation for the macro package to work
315 | * Changes were made to macro declaration; updates were made to match
316 |
317 | ## 0.3.3
318 | **README updates**
319 | Included additional examples of how to use the @ObservableUserDefaultsStore property attribute
320 |
321 | ## 0.3.2
322 | README updates
323 |
324 | ## 0.3.1
325 | README updates
326 |
327 | ## 0.3.0
328 |
329 | **New Features and Code Organization**
330 | * Added @ObservableUserDefaultsStore to define a custom UserDefaults suite. No longer tied to just UserDefaults.standard
331 | * Added @ObservableUserDefaultsIgnored to remove reuse of @ObsercationIgnored
332 | * Moved UserDefaultsWrapper out on its own in the library instead of as a nested struct created by the macro
333 | * Organized code structure
334 |
--------------------------------------------------------------------------------