Combining UserDefaults with Observation, giving you the ability to easily create Observable UserDefaults-backed classes.
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.
However, @AppStorage
has a few limitations:
- The initialization of it requires a string key and default value, making reuse difficult
- 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.
- Refactoring code can be cumbersome depending on how widespread your usage keys are
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.
UserDefaultsObservation aims to solve these issues by:
- Providing the ability to define any class as both UserDefaults-backed and Observable
- Centralizing the definition of UserDefaults keys and their default values
- Able to be used in both SwiftUI and non-SwiftUI code
- iOS 17.0+ | macOS 14.0+ | tvOS 17.0+ | watchOS 10.0+ | macCatalyst 17.0+
- Xcode 15
This package is built on Observation and Macros that are releasing in iOS 17, macOS 14.
File > Add Package Dependencies. Use this URL in the search box: https://github.com/tgeisse/UserDefaultsObservation
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:
import Foundation
import UserDefaultsObservation
@ObservableUserDefaults
class MySampleClass {
var firstUse = false
var username: String? = nil
}
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.
@ObservableUserDefaults
class MySampleClass {
var firstUse = false
var username: String? = nil
@ObservableUserDefaultsIgnored
var someIgnoredProperty = "hello world"
}
A default key is created for you as {ClassName}.{PropertyName}
. In the example above, the keys would be the following:
- "MySampleClass.firstUse"
- "MySampleClass.username"
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:
@ObservableUserDefaults
class MySampleClass {
var firstUse = false
var username: String? = nil
@ObservableUserDefaultsIgnored
var someIgnoredProperty = "hello world"
@UserDefaultsProperty(key: "myPreviousKey")
var existingUserDefaults: Bool = true
}
To use a custom UserDefaults store, you can use the @UserDefaultsStore
attribute to denote the UserDefaults store variable.
@ObservableUserDefaults
class MySampleClass {
var firstUse = false
var username: String? = nil
@ObservableUserDefaultsIgnored
var someIgnoredProperty = "hello world"
@UserDefaultsProperty(key: "myPreviousKey")
var existingUserDefaults: Bool = true
@UserDefaultsStore
var myStore = UserDefaults(suiteName: "MyStore.WithSuiteName.Example")
}
Should you need to change the store at runtime, one option is to do so with an initializer:
@ObservableUserDefaults
class MySampleClass {
var firstUse = false
var username: String? = nil
@UserDefaultsStore
var myStore: UserDefaults
init(_ store: UserDefaults = .standard) {
self.myStore = store
}
}
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:
@ObservableUserDefaults
class MySampleClass {
var firstUse = false
var username: String? = nil
@ObservableUserDefaultsIgnored
var someIgnoredProperty = "hello world"
@UserDefaultsProperty(key: "myPreviousKey")
var existingUserDefaults: Bool = true
@UserDefaultsStore
var myStore: UserDefaults {
#if DEBUG
return UserDefaults(suiteName: "myDebugStore.example")!
#else
return UserDefaults(suiteName: "myProductionStore.example")!
#endif
}
}
If computing this each time is not desired, then this is another option:
@UserDefaultsStore
var myStore: UserDefaults = {
#if DEBUG
return UserDefaults(suiteName: "myDebugStore.example")!
#else
return UserDefaults(suiteName: "myProductionStore.example")!
#endif
}()
One more option is to put the compiler flag code into the initializer
@UserDefaultsStore
var myStore: UserDefaults
init(_ store: UserDefaults = .standard) {
#if DEBUG
self.myStore = UserDefaults(suiteName: "myDebugStore.example")
#else
self.myStore = store
#endif
}
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 of the class containing the CloudProperty.
Let's take a look at syncing a user's favorite color:
@ObservableUserDefaults
class UsersPreferences {
@CloudProperty(key: "yourCloudKey",
userDefaultKey: "differentUserDefaultKey",
onStoreServerChange: .cloudValue,
onInitialSyncChange: .cloudValue,
onAccountChange: .cachedValue)
var favoriteColor: String = "Green"
}
This displays the parameters the user can or must define. Let's review each:
key
- the key to be used for storing and retrieving values from NSUbiquitousKeyValueStoreuserDefaultKey
- an optionally provided key to be used by UserDefaults for storing values locallyonStoreServerChange
- define the value to use when aNSUbiquitousKeyValueStoreServerChange
external notification is received. Default value:ignore
onInitialSyncChange
- define the value to use when aNSUbiquitousKeyValueStoreInitialSyncChange
external notification is received. Default value:ignore
onAccountChange
- define the value to use when aNSUbiquitousKeyValueStoreAccountChange
external notification is received. Default value:ignore
Tip
Omitting the userDefaultKey
parameter will use the value of the key
parameter for UserDefaults.
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:
defaultValue
- this will use the default value provided. In the favorite color example above, this would be "Green"cachedValue
- the value cached in UserDefaults and is previous value set on the devicecloudValue
- retrieves the cloud value and stores it locallyignore
- ignores the event
Warning
This package does not take any action when the NSUbiquitousKeyValueStoreQuotaViolationChange
external notification is received.
All of the following types are supported, including their optional counterparts:
-
NSData
-
Data
-
NSString
-
String
-
NSURL
-
URL
-
NSDate
-
Date
-
NSNumber
-
Bool
-
Int
-
Int8
-
Int16
-
Int32
-
Int64
-
UInt
-
UInt8
-
UInt16
-
UInt32
-
UInt64
-
Double
-
Float
-
RawRepresentable where RawType is in the list above
-
Array where Element is in the list above
-
Dictionary where Key == String && Value is in the list above
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
.
- CloudProperty now defaults all events to
ignore
- Updates to ReadMe
- Removed a warning that would be presented if using a RawRepresentable with a CloudProperty
- Added extra type conformance for RawRepresentables
New Features and Code Organization
- Added support for Cloud Storage through NSUbiquitousKeyValueStore
- Added new attributes that are shorter. Existing attributes that they complement are still available
- Internal code structure cleanup
- Readme updates
- Updated the Package platform versions to align with Swift Macros platform versions
- Added @available attribute to the protocol used to align with the Observation framework's platform availability
- Added visionOS
- Removed commented code that is no longer needed to keep around
- Updated Readme
- Updated to swift-syntax 509.0.0 minimum
- Small internal code updates
- Small syntax changes
- No longer do you need to import Observation for the macro package to work
- Changes were made to macro declaration; updates were made to match
README updates Included additional examples of how to use the @ObservableUserDefaultsStore property attribute
README updates
README updates
New Features and Code Organization
- Added @ObservableUserDefaultsStore to define a custom UserDefaults suite. No longer tied to just UserDefaults.standard
- Added @ObservableUserDefaultsIgnored to remove reuse of @ObsercationIgnored
- Moved UserDefaultsWrapper out on its own in the library instead of as a nested struct created by the macro
- Organized code structure