Skip to main content

UserDefaults in Swift

· 6 min read

(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:

  1. SwiftyUserDefaults: "Modern Swift API for NSUserDefaults"
  2. Defaults: "Swifty and modern UserDefaults"
  3. Foil: "A lightweight property wrapper for UserDefaults done right"

(TLTR? Jump to the summary table.)


declarations & definitions

1. SwiftyUserDefaults
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

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
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

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
import SwiftyUserDefaults

// KVO
Defaults.observe(\.namedKey, options: [ .initial, .old, .new ]) { change in
// default options are just [ .old, .new ]

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

GitHub stats as of 2022/03/16
latest release5.
custom types
properly utilize register(defaults:)
Combine Publisher


  1. I gave the 300th star. 8)