diff --git a/CHANGELOG.md b/CHANGELOG.md index 553a9c7..a1149dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 ----- diff --git a/Foil.podspec b/Foil.podspec index 928a6ae..2c596a6 100644 --- a/Foil.podspec +++ b/Foil.podspec @@ -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' diff --git a/README.md b/README.md index a2cde81..dd847c8 100644 --- a/README.md +++ b/README.md @@ -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. > @@ -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. @@ -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) diff --git a/Sources/UserDefaultsSerializable.swift b/Sources/UserDefaultsSerializable.swift index 33fbfbb..a1af883 100644 --- a/Sources/UserDefaultsSerializable.swift +++ b/Sources/UserDefaultsSerializable.swift @@ -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 } } @@ -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 } } @@ -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 } } @@ -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 } } @@ -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 } } @@ -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 } } @@ -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 } } @@ -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 } } @@ -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 } } @@ -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) } } } @@ -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) }) } } @@ -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) } } } @@ -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