Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix usage of failable inits. Update docs for RawRepresentable. #95

Merged
merged 3 commits into from
Jan 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading