├── .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 | --------------------------------------------------------------------------------