Foil 
A lightweight property wrapper for UserDefaults done right
About
Read the post: A better approach to writing a UserDefaults Property Wrapper
Why the name?
Foil, as in “let me quickly and easily wrap and store this leftover food in some foil so I can eat it later.” 🌯 😉
Foil:
noun
North America
A very thin, pliable, easily torn sheet of aluminum used for cooking, packaging, cosmetics, and insulation.
Usage
You can use @FoilDefaultStorage for non-optional values and @FoilDefaultStorageOptional for optional ones.
You may wish to store all your user defaults in one place, however, that is not necessary. Any property on any type can use this wrapper.
final class AppSettings {
    static let shared = AppSettings()
    @FoilDefaultStorage(key: "flagEnabled")
    var flagEnabled = true
    @FoilDefaultStorage(key: "totalCount")
    var totalCount = 0
    @FoilDefaultStorageOptional(key: "timestamp")
    var timestamp: Date?
}
// Usage
func userDidToggleSetting(_ sender: UISwitch) {
    AppSettings.shared.flagEnabled = sender.isOn
}
There is also an included example app project.
Using enum keys
If you prefer using an enum for the keys, writing an extension specific to your app is easy. However, this is not required. In fact, unless you have a specific reason to reference the keys, this is completely unnecessary.
enum AppSettingsKey: String, CaseIterable {
    case flagEnabled
    case totalCount
    case timestamp
}
extension FoilDefaultStorage {
    init(wrappedValue: T, _ key: AppSettingsKey) {
        self.init(wrappedValue: wrappedValue, key: key.rawValue)
    }
}
extension FoilDefaultStorageOptional {
    init(_ key: AppSettingsKey) {
        self.init(key: key.rawValue)
    }
}
Observing changes
There are many ways to observe property changes. The most common are by using Key-Value Observing or a Combine Publisher. KVO observing requires the object with the property to inherit from NSObject and the property must be declared as @objc dynamic.
final class AppSettings: NSObject {
    static let shared = AppSettings()
    @FoilDefaultStorageOptional(key: "userId")
    @objc dynamic var userId: String?
    @FoilDefaultStorageOptional(key: "average")
    var average: Double?
}
Using KVO
let observer = AppSettings.shared.observe(\.userId, options: [.new]) { settings, change in
    print(change)
}
Using Combine
[!NOTE] The
averagedoes not need the@objc dynamicannotation,.receiveValuewill fire immediately with the current value ofaverageand on every change after.
AppSettings.shared.$average
    .sink {
        print($0)
    }
    .store(in: &cancellable)
Combine Alternative with KVO
[!NOTE] In this case,
userIdneeds the@objc dynamicannotation andAppSettingsneeds to inherit fromNSObject. ThenreceiveValuewill fire only on changes to wrapped object’s value. It will not publish the initial value as in the example above.
AppSettings.shared
    .publisher(for: \.userId, options: [.new])
    .sink {
        print($0)
    }
    .store(in: &cancellable)
Supported types
The following types are supported by default for use with @FoilDefaultStorage.
[!NOTE] While the
UserDefaultsSerializableprotocol defines a failable initializer,init?(storedValue:), it is possible to provide a custom implementation with a non-failable initializer, which still satisfies the protocol requirements.For all of Swift’s built-in types (
Bool,Int,Double,String, etc.), the default implementation ofUserDefaultsSerializableis non-failable.[!IMPORTANT] Adding support for custom types is possible by conforming to
UserDefaultsSerializable. However, this is highly discouraged as allplisttypes are supported by default.UserDefaultsis not intended for storing complex data structures and object graphs. You should probably be using a proper database (or serializing to disk viaCodable) instead.While
Foilsupports storingCodabletypes by default, you should use this sparingly and only for small objects with few properties.
- Bool
- Int
- UInt
- Float
- Double
- String
- URL
- Date
- Data
- Array
- Set
- Dictionary
- RawRepresentabletypes
- Codabletypes
Notes on Codable types
[!WARNING] If you are storing custom
Codabletypes and using the default implementation ofUserDefaultsSerializableprovided byFoil, then you must use the optional variant of the property wrapper,@FoilDefaultStorageOptional. This will allow you to make breaking changes to yourCodabletype (e.g., adding or removing a property). Alternatively, you can provide a custom implementation ofCodablethat supports migration, or provide a custom implementation ofUserDefaultsSerializablethat handles encoding/decoding failures. See the example below.
Codable Example:
// Note: uses the default implementation of UserDefaultsSerializable
struct User: Codable, UserDefaultsSerializable {
    let id: UUID
    let name: String
}
// Yes, do this
@FoilDefaultStorageOptional(key: "user")
var user: User?
// NO, do NOT this
// This will crash if you change User by adding/removing properties
@FoilDefaultStorage(key: "user")
var user = User()
Notes on RawRepresentable types
Using RawRepresentable types, especially as properties of a Codable type require special considerations. As mentioned above, Codable types must use @FoilDefaultStorageOptional out-of-the-box, unless you provide a custom implementation of UserDefaultsSerializable. The same is true for RawRepresentable types.
[!WARNING]
RawRepresentabletypes must use@FoilDefaultStorageOptionalin case you modify the cases of yourenum(or otherwise modify yourRawRepresentablewith a breaking change). Additionally,RawRepresentabletypes have a designated initializer that is failable,init?(rawValue:), and thus could returnnil.Or, if you are storing a
Codabletype that hasRawRepresentableproperties, by default those properties should be optional to accommodate the optionality described above.
If you wish to avoid these edge cases with RawRepresentable types, you can provide a non-failable initializer:
extension MyStringEnum: UserDefaultsSerializable {
    // Default init provided by Foil
    // public init?(storedValue: RawValue.StoredValue) { ... }
    // New, non-failable init using force-unwrap.
    // Only do this if you know you will not make breaking changes.
    public init(storedValue: String) { self.init(rawValue: storedValue)! }
}
Additional Resources
- NSUserDefaults in Practice, the excellent guide by David Smith
- UserDefaults documentation
- Preferences and Settings Programming Guide
- Property List Programming Guide
Supported Platforms
- iOS 13.0+
- tvOS 13.0+
- watchOS 6.0+
- macOS 11.0+
- visionOS 1.0+
Requirements
- Swift 6.0+
- Xcode 16.0+
- SwiftLint
Installation
Swift Package Manager
dependencies: [
    .package(url: "https://github.com/jessesquires/Foil.git", from: "6.0.0")
]
Alternatively, you can add the package directly via Xcode.
Documentation
You can read the documentation here. Generated with jazzy. Hosted by GitHub Pages.
Documentation is also available on the Swift Package Index.
Contributing
Interested in making contributions to this project? Please review the guides below.
Also consider sponsoring this project or buying my apps! ✌️
Credits
Created and maintained by Jesse Squires.
License
Released under the MIT License. See LICENSE for details.
Copyright © 2021-present Jesse Squires.
 View on GitHub
View on GitHub