(2022/04/01: updated and expanded)
NSUserDefaults → UserDefaults
In the beginning, when Cocoa was hot (circa 2001), there was already NSUserDefaults, a thoughtful API for the seemingly simple task of persisting preference settings.
Swift (since 2014) gave it a cleaner name (UserDefaults, without the NS prefix), but it also created new expectations that the plain old class was not delivering:
- strong type checking
- easy value types usage
- modern Swiftness, etc.
This is where Swiftified UsersDefaults libraries come in. Here we look at a few of them:
- SwiftyUserDefaults: "Modern Swift API for NSUserDefaults"
- Defaults: "Swifty and modern UserDefaults"
- Foil: "A lightweight property wrapper for UserDefaults done right"
(TLTR? Jump to the summary table.)
usage
declarations & definitions
- 1. SwiftyUserDefaults
- 2. Defaults
- 3. Foil
import SwiftyUserDefaults
extension DefaultsKeys {
var userName: DefaultsKey<String> { .init("userName", defaultValue: "") }
var quality: DefaultsKey<Double> { .init("quality", defaultValue: 0.8) }
var launchCount: DefaultsKey<Int?> { .init("launchCount") } // optional
}
import Defaults
extension Defaults.Keys {
static let userName = Key<String>("userName", default: "")
static let quality = Key<Double>("quality", default: 0.8)
static let launchCount: Key<Int?>("launchCount") // optional
}
import Foil
class Settings {
@WrappedDefault(key: "userName") var userName: String = ""
@WrappedDefault(key: "quality") var quality: Double = 0.8
@WrappedDefaultOptional(key: "launchCount") var launchCount: Int? // optional
}
The first two libraries both define Keys in an extension; Defaults' approach looks a bit cleaner.
Foil, being the newest kid on the block, takes advantage of Swift's Property Wrappers. Using it involves defining actual usable variables (instead of just keys) in your own class (or struct).
access: get & set
- 1. SwiftyUserDefaults
- 2. Defaults
- 3. Foil
import SwiftyUserDefaults
let q1 = Defaults[key: quality] // or,
let q2 = Defaults[\.quality] // with shortcut dot syntax, or,
let q3 = Defaults.quality // with Swift 5.1 dynamicMemberLookup
Defaults[\.quality] = 0.5 // or,
Defaults.quality = 0.5
import Defaults
let q = Defaults[.quality]
Defaults[.quality] = 0.5
// import Foil
let q = settings.quality
settings.quality = 0.5
The first two libraries involve similar steps: Import the module, and then access the values through the class and a subscript. SwiftyUserDefaults also allows access without a subscript by using Swift 5.1's dynamicMemberLookup.
In Foil, importing is not needed for merely accessing the values, since they are declared under your own class (or struct). And subscripting is not necessary.
change observations
- 1. SwiftyUserDefaults
- 2. Defaults
- 3. Foil
import SwiftyUserDefaults
// KVO
Defaults.observe(\.namedKey, options: [ .initial, .old, .new ]) { change in
// default options are just [ .old, .new ]
}
import Defaults
// 1. KVO
Defaults.observe(.namedKey, options: [ .initial, .old, .new ]) { change in
// default options include .initial (?)
}
// 2. Combine
let cancellable = Defaults.publisher(.namedKey)
.sink { change in
// ...
}
import Foil
// 1. Swift property observers: `willSet` and `didSet`
@WrappedDefault(key: "quality") var quality: Double = 0.8 {
didSet {
// `.initial` option not available
}
}
// import Foil
// 2. KVO
settings.observe(\.namedKey, options: [ .initial, .old, .new ]) { settings, change in
// ...
}
// 3. Combine #1 with KVO
let cancellable = settings.publisher(for: \.namedKey, options: [ .new ])
.sink { newValue in
// `@objc dynamic` required on property
}
// 4. Combine #2
let cancellable = settings.$namedKey
.sink { newValue in
// `@objc dynamic` NOT required on property
// `.initial` option is included
}
issues in compiling
Xcode 13.2 and before
Even before upgrading to Xcode 13.3, the first two libraries were often problematic, sensitive to the Swift compiler's inner changes. I agree that it is due to the over complications of the designs.
Xcode 13.3
13.3 brings both SwiftyUserDefaults and Defaults to the knees – it could very well be due to the newer Swift compiler's bugs. As of this writing, only one of them has a workaround.
This was the point that I decided to remove my apps' reliance of them and
roll my own,
probably some simple wrapper around Foundation
's UserDefaults...
But then the research found me Foil...
wrapping in Foil
Foil has the same basic design as what I was trying
(except that I used the (more succinct?) name @UserDefault
instead of @WrappedDefault
).
Not only was it already done,
it was actually at v3.
(we know the third release is always the perfect ripe)
So instead of removing the UserDefault packages from my Xcode projects, I ended up replacing them.
I am still in the process of converting my projetcs, and will update this post if anything significant happens.
Stay cool.
URL issues
I did encounter an unexpected issue while converting to Foil:
Storing URLs from Core Data's uriRepresentation
.
Specifically, retriving and passing the Foil-saved URL back to Core Data caused a "is not a valid Core Data URI" crash.
The current workaround is to store the URL as a String.
summary table
SwiftyUserDefaults | Defaults | Foil | |
---|---|---|---|
GitHub stats as of 2022/03/16 | |||
latest release | 5.3.0 | 6.2.1 | 3.0.0 |
stars | 4.6k | 994 | 3001 |
enum | ✓ | ✓ | ✓ |
custom types | ✓ | ✓ | ✓ |
properly utilize register(defaults:) | ❌ | ✓ | ✓ |
Combine Publisher | ❌ | ✓ | ✓ |
Codable2 | ✓ | ✓ | ❌ |
DefaultsSerializable | Defaults.Serializable | UserDefaultsSerializable |
references
- the libraries:
- UserDefaults
- Yet Another Swift Blog: The Advanced Guide to UserDefaults in Swift
- Jesse Squires:
- Swift: Property Wrappers:
- NSHipster: Swift Property Wrappers
- Swift by Sundell: Property wrappers in Swift
- Sarun: What is a Property Wrapper in Swift