Skip to content

Commit

Permalink
Fix usage of failable inits. Update docs for RawRepresentable. (#95)
Browse files Browse the repository at this point in the history
See the changelog for details.
  • Loading branch information
jessesquires committed Jan 15, 2024
1 parent f5d6633 commit 89f4d89
Show file tree
Hide file tree
Showing 4 changed files with 53 additions and 13 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ NEXT

- TBA

5.0.1
-----

This release closes the [5.0.1 milestone](https://github.com/jessesquires/Foil/milestone/8?closed=1).

- Addressed some potential edge cases and issues with optional types and failable initializers. ([#95](https://github.com/jessesquires/Foil/issues/95), [@jessesquires](https://github.com/jessesquires))
- The default implementation of `UserDefaultsSerializable` for Swift built-in types (`Int`, `Double`, `String`, etc.) now provides a **non-failable** initializer because these initializers cannot fail. This still satisfies the protocol requirements.
- Added an `assertionFailure` to the `UserDefaultsSerializable` implementation for `RawRepresentable` to catch potential bugs when storing and fetching data after making changes to a `RawRepresentable` type.
- Documentation has been updated with thorough explanations of edge cases and considerations for `RawRepresentable` types. Please see the `README` for further details.

5.0.0
-----

Expand Down
2 changes: 1 addition & 1 deletion Foil.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'Foil'
s.version = '5.0.0'
s.version = '5.0.1'
s.license = 'MIT'

s.summary = 'A lightweight property wrapper for UserDefaults'
Expand Down
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ AppSettings.shared

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.
>
Expand All @@ -144,6 +149,8 @@ The following types are supported by default for use with `@FoilDefaultStorage`.
- `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.
Expand All @@ -165,6 +172,28 @@ var user: 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

- [NSUserDefaults in Practice](http://dscoder.com/defaults.html), the excellent guide by [David Smith](https://twitter.com/Catfish_Man)
Expand Down
25 changes: 13 additions & 12 deletions Sources/UserDefaultsSerializable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public protocol UserDefaultsSerializable {
extension Bool: UserDefaultsSerializable {
public var storedValue: Self { self }

public init?(storedValue: Self) {
public init(storedValue: Self) {
self = storedValue
}
}
Expand All @@ -57,7 +57,7 @@ extension Bool: UserDefaultsSerializable {
extension Int: UserDefaultsSerializable {
public var storedValue: Self { self }

public init?(storedValue: Self) {
public init(storedValue: Self) {
self = storedValue
}
}
Expand All @@ -66,7 +66,7 @@ extension Int: UserDefaultsSerializable {
extension UInt: UserDefaultsSerializable {
public var storedValue: Self { self }

public init?(storedValue: Self) {
public init(storedValue: Self) {
self = storedValue
}
}
Expand All @@ -75,7 +75,7 @@ extension UInt: UserDefaultsSerializable {
extension Float: UserDefaultsSerializable {
public var storedValue: Self { self }

public init?(storedValue: Self) {
public init(storedValue: Self) {
self = storedValue
}
}
Expand All @@ -84,7 +84,7 @@ extension Float: UserDefaultsSerializable {
extension Double: UserDefaultsSerializable {
public var storedValue: Self { self }

public init?(storedValue: Self) {
public init(storedValue: Self) {
self = storedValue
}
}
Expand All @@ -93,7 +93,7 @@ extension Double: UserDefaultsSerializable {
extension String: UserDefaultsSerializable {
public var storedValue: Self { self }

public init?(storedValue: Self) {
public init(storedValue: Self) {
self = storedValue
}
}
Expand All @@ -102,7 +102,7 @@ extension String: UserDefaultsSerializable {
extension URL: UserDefaultsSerializable {
public var storedValue: Self { self }

public init?(storedValue: Self) {
public init(storedValue: Self) {
self = storedValue
}
}
Expand All @@ -111,7 +111,7 @@ extension URL: UserDefaultsSerializable {
extension Date: UserDefaultsSerializable {
public var storedValue: Self { self }

public init?(storedValue: Self) {
public init(storedValue: Self) {
self = storedValue
}
}
Expand All @@ -120,7 +120,7 @@ extension Date: UserDefaultsSerializable {
extension Data: UserDefaultsSerializable {
public var storedValue: Self { self }

public init?(storedValue: Self) {
public init(storedValue: Self) {
self = storedValue
}
}
Expand All @@ -134,7 +134,7 @@ extension Array: UserDefaultsSerializable where Element: UserDefaultsSerializabl
self.compactMap { $0.storedValue }
}

public init?(storedValue: [Element.StoredValue]) {
public init(storedValue: [Element.StoredValue]) {
self = storedValue.compactMap { Element(storedValue: $0) }
}
}
Expand All @@ -148,7 +148,7 @@ extension Set: UserDefaultsSerializable where Element: UserDefaultsSerializable
self.map { $0.storedValue }
}

public init?(storedValue: [Element.StoredValue]) {
public init(storedValue: [Element.StoredValue]) {
self = Set(storedValue.compactMap { Element(storedValue: $0) })
}
}
Expand All @@ -162,7 +162,7 @@ extension Dictionary: UserDefaultsSerializable where Key == String, Value: UserD
self.compactMapValues { $0.storedValue }
}

public init?(storedValue: [String: Value.StoredValue]) {
public init(storedValue: [String: Value.StoredValue]) {
self = storedValue.compactMapValues { Value(storedValue: $0) }
}
}
Expand All @@ -174,6 +174,7 @@ extension UserDefaultsSerializable where Self: RawRepresentable, Self.RawValue:
public init?(storedValue: RawValue.StoredValue) {
guard let rawValue = Self.RawValue(storedValue: storedValue),
let value = Self(rawValue: rawValue) else {
assertionFailure("[Foil] RawRepresentable error: found unexpected stored value: \(storedValue)")
return nil
}
self = value
Expand Down

0 comments on commit 89f4d89

Please sign in to comment.