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
, UserDefaultsObservation
, and use 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 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 the case of refactoring or migrating existing keys, you can mark a property with the @ObservableUserDefaultsProperty
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"
@ObservableUserDefaultsProperty("myPreviousKey")
var existingUserDefaults: Bool = true
}
To use a custom UserDefaults store, you can use the @ObservableUserDefaultsStore
attribute to denote the UserDefaults variable.
@ObservableUserDefaults
class MySampleClass {
var firstUse = false
var username: String? = nil
@ObservableUserDefaultsIgnored
var someIgnoredProperty = "hello world"
@ObservableUserDefaultsProperty("myPreviousKey")
var existingUserDefaults: Bool = true
@ObservableUserDefaultsStore
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
@ObservableUserDefaultsStore
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"
@ObservableUserDefaultsProperty("myPreviousKey")
var existingUserDefaults: Bool = true
@ObservableUserDefaultsStore
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:
@ObservableUserDefaultsStore
var myStore: UserDefaults = {
#if DEBUG
return UserDefaults(suiteName: "myDebugStore.example")!
#else
return UserDefaults(suiteName: "myProductionStore.example")!
#endif
}()
The last option is to put the compiler flag code into the initializer
@ObservableUserDefaultsStore
var myStore: UserDefaults
init(_ store: UserDefaults = .standard) {
#if DEBUG
self.myStore = UserDefaults(suiteName: "myDebugStore.example")
#else
self.myStore = store
#endif
}
All of the following types are supported, including their optional counterparts:
-
RawRepresentable
-
NSData
-
Data
-
NSString
-
String
-
NSURL
-
URL
-
NSDate
-
Date
-
NSNumber
-
Bool
-
Int
-
Int8
-
Int16
-
Int32
-
Int64
-
UInt
-
UInt8
-
UInt16
-
UInt32
-
UInt64
-
Double
-
Float
-
Array where Element is in the above list
-
Dictionary where Key == String && Value is in the above list
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 @ObservationIgnored
.
- 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