jessesquires/foil
*A lightweight [property wrapper](https://docs.swift.org/swift-book/ReferenceManual/Attributes.html#ID348) 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:<br> noun<br> North America<br> 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.
```swift
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.
```swift
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](https://www.jessesquires.com/blog/2021/08/08/different-ways-to-observe-properties-in-swift/). 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`.
```swift
final class AppSettings: NSObject {
static let shared = AppSettings()
@FoilDefaultStorageOptional(key: "userId")
@objc dynamic var userId: String?
@FoilDefaultStorageOptional(key: "average")
var average: Double?
}
```
#### Using KVO
```swift
let observer = AppSettings.shared.observe(\.userId, options: [.new]) { settings, change in
print(change)
}
```
#### Using Combine
> [!NOTE]
> The `average` does not need the `@objc dynamic` annotation, `.receiveValue` will fire immediately with the current value of `average` and on every change after.
```swift
AppSettings.shared.$average
.sink {
print($0)
}
.store(in: &cancellable)
```
#### Combine Alternative with KVO
> [!NOTE]
> In this case, `userId` needs the `@objc dynamic` annotation and `AppSettings` needs to inherit from `NSObject`. Then `receiveValue` will fire only on changes to wrapped object's value. It will not publish the initial value as in the example above.
```swift
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 `UserDefaultsSerializable` protocol 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 of `UserDefaultsSerializable` is **non-failable**.
> [!IMPORTANT]
> Adding support for custom types is possible by conforming to `UserDefaultsSerializable`. However, **this is highly discouraged** as all `plist` types are supported by default. `UserDefaults` is not intended for storing complex data structures and object graphs. You should probably be using a proper database (or serializing to disk via `Codable`) instead.
>
> While `Foil` supports storing `Codable` types 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`
- `RawRepresentable` types
- `Codable` types
#### Notes on [`Codable`](https://developer.apple.com/documentation/swift/codable) types
> [!WARNING]
> If you are storing custom `Codable` types and using the default implementation of `UserDefaultsSerializable` provided by `Foil`, then **you must use the optional variant of the property wrapper**, `@FoilDefaultStorageOptional`. This will allow you to make breaking changes to your `Codable` type (e.g., adding or removing a property). Alternatively, you can provide a custom implementation of `Codable` that supports migration, or provide a custom implementation of `UserDefaultsSerializable` that handles encoding/decoding failures. See the example below.
**Codable Example:**
```swift
// 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`](https://developer.apple.com/documentation/swift/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]
> `RawRepresentable` types must use `@FoilDefaultStorageOptional` in case you modify the cases of your `enum` (or otherwise modify your `RawRepresentable` with a breaking change). Additionally, `RawRepresentable` types have a designated initializer that is failable, `init?(rawValue:)`, and thus could return `nil`.
>
> Or, if you are storing a `Codable` type that has `RawRepresentable` properties, 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:
```swift
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
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.
Package Metadata
Repository: jessesquires/foil
Default branch: main
README: README.md