jessesquires / foil Goto Github PK
View Code? Open in Web Editor NEWA lightweight property wrapper for UserDefaults done right
Home Page: https://jessesquires.github.io/Foil/
License: MIT License
A lightweight property wrapper for UserDefaults done right
Home Page: https://jessesquires.github.io/Foil/
License: MIT License
Have you read the Contributing Guidelines?
Project version:
1.0.0
Platform/OS version:
iOS 13+
Any related GitHub issues:
The current implementation of UserDefaultsSerializable
is too simplistic because it only defines a subset of the values that can be automatically stored in UserDefaults
:
Bool
, Int
, Float
, and Double
are currently supported numeric types, although UserDefaults
can store ANY numeric type that is supported by NSNumber
. This includes Int8
, Int16
, UInt8
, UInt16
, etc.String
:: Value.StoredValue
]) where UserDefaults
actually allows any supported UserDefaults value for a key ([Key.StoredValue: Value.StoredValue])UserDefaults
are all missing: NSString
, NSNumber
, NSDictionary
, NSArray
, etc.NA
Clearly and concisely describe what you expected to happen.
public protocol UDS_NSNumber: UserDefaultsSerializable where StoredValue == NSNumber { }
extension Bool: UDS_NSNumber {
public var storedValue: NSNumber { return self as NSNumber }
public init(storedValue: NSNumber) {
self = storedValue.boolValue
}
}
extension Int: UDS_NSNumber {
public var storedValue: NSNumber { return self as NSNumber }
public init(storedValue: NSNumber) {
self = storedValue.intValue
}
}
extension Int8: UDS_NSNumber {
public var storedValue: NSNumber { return self as NSNumber }
public init(storedValue: NSNumber) {
self = storedValue.int8Value
}
}
extension Int16: UDS_NSNumber {
public var storedValue: NSNumber { return self as NSNumber }
public init(storedValue: NSNumber) {
self = storedValue.int16Value
}
}
extension Int32: UDS_NSNumber {
public var storedValue: NSNumber { return self as NSNumber }
public init(storedValue: NSNumber) {
self = storedValue.int32Value
}
}
extension Int64: UDS_NSNumber {
public var storedValue: NSNumber { return self as NSNumber }
public init(storedValue: NSNumber) {
self = storedValue.int64Value
}
}
extension UInt: UDS_NSNumber {
public var storedValue: NSNumber { return self as NSNumber }
public init(storedValue: NSNumber) {
self = storedValue.uintValue
}
}
extension UInt8: UDS_NSNumber {
public var storedValue: NSNumber { return self as NSNumber }
public init(storedValue: NSNumber) {
self = storedValue.uint8Value
}
}
extension UInt16: UDS_NSNumber {
public var storedValue: NSNumber { return self as NSNumber }
public init(storedValue: NSNumber) {
self = storedValue.uint16Value
}
}
extension UInt32: UDS_NSNumber {
public var storedValue: NSNumber { return self as NSNumber }
public init(storedValue: NSNumber) {
self = storedValue.uint32Value
}
}
extension UInt64: UDS_NSNumber {
public var storedValue: NSNumber { return self as NSNumber }
public init(storedValue: NSNumber) {
self = storedValue.uint64Value
}
}
extension Float: UDS_NSNumber {
public var storedValue: NSNumber { return self as NSNumber }
public init(storedValue: NSNumber) {
self = storedValue.floatValue
}
}
extension Double: UDS_NSNumber {
public var storedValue: NSNumber { return self as NSNumber }
public init(storedValue: NSNumber) {
self = storedValue.doubleValue
}
}
extension Dictionary: UserDefaultsSerializable where
Key: UserDefaultsSerializable,
Key.StoredValue: Hashable,
Value: UserDefaultsSerializable {
public var storedValue: [Key.StoredValue: Value.StoredValue] {
reduce(into: [:]) {
$0[$1.0.storedValue] = $1.1.storedValue
}
}
public init(storedValue: [Key.StoredValue : Value.StoredValue]) {
self = storedValue.reduce(into: [:]) {
$0[Key(storedValue: $1.0)] = Value(storedValue: $1.1)
}
}
}
N/A
N/A
Hello! Big Foil fan here. :)
I'd like to be able to save a Codable
struct in UserDefaults using Foil. I know this is somewhat controversial, and "highly discouraged" but I'm aware of the tradeoffs and I'm OK with it. I'm open to any possible way of doing that, even if it's not a feature that's added to Foil itself.
Be able to save and retrieve small Codable structs from UserDefaults.
The problem I'm running into is that I want the decoding to fail gracefully if the struct can't be decoded, but I can't figure out how to do that.
I have a MyCodableStruct
the conforms to Codable
. I have exposed it in my AppSettings
like so:
@WrappedDefaultOptional(key: "myCodableStructKey")
public var savedCodableStruct: MyCodableStruct? {
willSet {
objectWillChange.send()
}
}
I've tried to have my type conform to UserDefaultsSerializeable
via the following approach:
extension MyCodableStruct: UserDefaultsSerializable {
public var storedValue: String? {
guard
let data = try? JSONEncoder().encode(self),
let string = String(data: data, encoding: .utf8)
else {
return nil
}
return string
}
public init(storedValue: String?) {
guard
let storedValue,
let data = storedValue.data(using: .utf8),
let decoded = try? JSONDecoder().decode(Self.self, from: data)
else {
fatalError("Didn't expect something we couldn't decode!")
}
self = decoded
}
}
This actually works, and I can save/restore the struct. However, I want it to be able to fail gracefully if there is some garbage stored in that string. Currently it hits the fatal error above. I would like it to just return nil
if the decoding fails, but that's not an option in this initializer.
I'm a little out of my depth with property wrappers and generics, so I'm not sure how to do this.
What causes it to crash is the last line of fetchOptional<T>(_ key: String) -> T?
where it does return T(storedValue: storedValue)
.
func fetchOptional<T: UserDefaultsSerializable>(_ key: String) -> T? {
let fetched: Any?
if T.self == URL.self {
// Hack for URL, which is special
// See: http://dscoder.com/defaults.html
// Error: Could not cast value of type '_NSInlineData' to 'NSURL'
fetched = self.url(forKey: key)
} else {
fetched = self.object(forKey: key)
}
if fetched == nil {
return nil
}
guard let storedValue = fetched as? T.StoredValue else {
return nil
}
return T(storedValue: storedValue)
}
If the protocol instead had a failable initializer like init?(storedValue:)
it would work fine. It could return nil, which I would be happy with. Maybe if there was an optional init?(storedValue: StoredValue)
in the protocol? Or a UserDefaultsSerializableOptional
?
I'm open to any other ideas as well. Thanks for your help and your great work!
Is there a way to use the native UserDefaults.publisher
added by Combine? It seems this only works if the properties were added as extensions to UserDefaults
instead of a separate AppSettings
type. For example, this test passes today:
func test_Integration_Publisher() {
let promise = expectation(description: #function)
var publishedValue: String?
TestSettings.store
.publisher(for: \.username, options: [.new])
.sink {
publishedValue = $0
promise.fulfill()
}
.store(in: &cancellable)
settings.username = "abc123"
wait(for: [promise], timeout: 5)
XCTAssertEqual(settings.username, publishedValue)
}
// This has to be added to get the publisher but of course duplicates the property already added to `TestSettings`
extension UserDefaults {
@objc var username: String? {
get { string(forKey: "username") }
set { set(newValue, forKey: "username") }
}
}
Since you cannot have more than one property wrapper on a field (I think), I'm wondering if WrappedDefault
can offer a publisher on the property as well so instead of adding the property as an extension to UserDefaults
, to be exposed from the same property that was created in TestSettings
:
func test_Integration_Publisher() {
let promise = expectation(description: #function)
var publishedValue: String?
settings.$username
.publisher(options: [.new])
.sink {
publishedValue = $0
promise.fulfill()
}
.store(in: &cancellable)
settings.username = "abc123"
wait(for: [promise], timeout: 5)
XCTAssertEqual(settings.username, publishedValue)
}
5.0.1
macOS 14.3.1
MBP
No response
Hello!
I've been trying to set a Bool
to default to false
, however it was defaulting to true
in UserDefaults (when I inspected the .plist -- I attempted deleting the entire Container, still no dice.) In the meantime, I figured I'd try setting a String to a value to see where it was set, and got a crash.
class AppStorage: ObservableObject {
static let shared = AppStorage()
// Notice: not optional!
@FoilDefaultStorage(key: "didMigrateFromv_0_2_to_0_3_VersionedSchema")
var didMigrateFromv0_2Schema: String = "blah"
@FoilDefaultStorage(key: "defaultSidebarItem")
var defaultSidebarItem: SidebarItem = .albumGrid {
willSet {
objectWillChange.send()
}
}
// ...
Observe this crash:
Expected it to not use the Optional variant for fetch if the value is not there.
No response
No response
No response
How to integrated Foil with SwiftUI project ?
I try to integrated Foil in my SwiftUI project.
UI binding:
struct GeneralSettings: View {
@ObservedObject var settings: AppSettings = AppSettings()
var body: some View {
Stepper(value: $settings.fontSize, in: 0 ... 42) {
Text("**Size:** \(settings.fontSize)")
}
}
}
class AppSettings: ObservableObject {
@WrappedDefault(key: "fontSize")
var fontSize: Int = 30
}
The problem is we can't not define @Published
here when we have @WrappedDefault, so Stepper UI fontSize value is not update.
and My workaround is
class AppSettings: ObservableObject {
private var cancellables = Set<AnyCancellable>()
@Published var needUpdate: Bool = false
@WrappedDefault(key: "fontSize")
var fontSize: Int = 30
init() {
$fontSize
.sink { _ in
self.needUpdate.toggle()
}
.store(in: &cancellables)
}
}
I'm not sure if this is correct way to integrated Foil with SwiftUI.
N/A
N/A
I hope I don't get shunned 😅, but curious on how many feel about dropping CocoaPods support and just go pure Swift Package? The minimum Xcode version to submit to the App Store is Xcode 12 which is well above the minimum required to use SPM so I don't think supporting legacy is an issue.
Not only will this be less maintenance, but also we could get rid of the .xcodeproj
and use only Package.swift
(but I suspect there's more reasons than CocoaPods the Xcode project is needed).
Have you read the Contributing Guidelines?
yes
1.1.0 / the latest state
doesn't matter
doesn't matter
doesn't matter
Clearly and concisely describe the bug.
Podspec declares deployment targets as 12.0 / 10.13 while Package.swift says 13 / 10.15.
Provide numbered steps to follow that reproduce the bug.
No steps.
Clearly and concisely describe what you expected to happen.
Deployment targets are identical.
Paste the full output of any stack trace or compiler error.
Include a code snippet that reproduces the described behavior, if applicable
https://github.com/jessesquires/Foil/blob/main/Foil.podspec#L17
https://github.com/jessesquires/Foil/blob/main/Package.swift#L23
If applicable, add screenshots, gifs, or videos to help explain your problem.
Add any other useful information about the problem here.
I’d suggest using something like this:
@WrappedDefault("flagEnabled") var flagEnabled = true
Instead of this:
@WrappedDefault(keyName: "flagEnabled", defaultValue: true)
var flagEnabled: Bool
I think it’s clear from the declaration that the assigned value is a default/initial compile-time value. And this avoids duplicating the type declaration (true
and Bool
).
The trick is to use init(wrappedValue: T, _ keyName: String
instead of init(keyName: String, defaultValue: T
.
I used SwiftyUserDefaults before, but when property wrappers got available I came up with my own simpler solution. I’ve recently read your article to make sure I did not miss any caveat.
Here’s basically what I’ve been doing:
@propertyWrapper
struct Default<T> {
private let key: String
private let suite: UserDefaults
init(wrappedValue: T, _ key: String, suite: UserDefaults = .standard) {
suite.register(defaults: [key: wrappedValue])
self.key = key
self.suite = suite
}
...
Initially I struggled to come up with a good name here and I honestly don't like this name at all.
class NetworkManagerClass { }
.AppStorage
, so we can't use that.Right now, I'm thinking FoilStorage
and FoilStorageOptional
?
Open to ideas here.
Not sure if it should be treated as a feature or bug.
Have you read the Contributing Guidelines?
Yes.
Lower deployment targets.
Yes, I can't depend on this library because my library supports lower deployment target.
Simply lower them.
None.
For example, lint succeeds with setting iOS to 9.0:
❯ bundle exec pod lib lint --platforms=ios
-> Foil (1.1.0)
- NOTE | xcodebuild: note: Using new build system
- NOTE | xcodebuild: note: Building targets in parallel
- NOTE | xcodebuild: note: Using codesigning identity override: -
- NOTE | [iOS] xcodebuild: note: Planning build
- NOTE | [iOS] xcodebuild: note: Constructing build description
- NOTE | [iOS] xcodebuild: warning: Skipping code signing because the target does not have an Info.plist file and one is not being generated automatically. (in target 'App' from project 'App')
Foil passed validation.
2.0.0
iOS 15, iOS 13.5
Tested on various Simulators
No response
When using the example project and updating one of the values like e.g. flagEnabled
, after rebuilding the app the flagEnabled value is reset to the old value that has been saved previously before the last change.
Since those values are backed by UserDefaults, I would expect them to be preserved between app launches. So that when we update a key with a new value, when starting the app again we should see the previously updated value instead of the one from before.
No response
Issue can be seen quite good in the video. This also happens if we safely move the app in the background first and then rebuild the project.
Also when stepping in the code via debugger it shows that the issue is not caused by the example UI code, but the flag value returned by AppSettings.shared.flagEnabled
is in fact the outdated value.
One really nice aspect of @AppStorage
is that I can do something like this:
struct ContentView: View {
@AppStorage("debugMode") var debugMode: Bool = false
var body: some View {
Toggle("Debug Mode", isOn: $debugMode)
.padding()
}
}
I was hoping to be able to do something like this with Foil, e.g.:
Toggle("Debug Mode", isOn: AppSettings.shared.$debugMode)
However, with this I get the error: Cannot convert value of type 'AnyPublisher<Bool, Never>' to expected argument type 'Binding<Bool>'
N/A
One way to get it to work is to add this to the SwiftUI view that is using it:
let debugMode: Binding<Bool> = Binding(
get: { AppSettings.shared.debugMode },
set: { AppSettings.shared.debugMode = $0 }
)
// Usage:
Toggle("Debug Mode", isOn: debugMode)
This works, but I'd rather not have to define those Bindings myself for every setting.
Ideally something can be implemented within the @WrappedDefault
property wrapper to do this automatically, using the same syntax as @AppStorage
. I'm not sure if it's possible or not. Even an var/func would work for me but couldn't get those to work, my Combine/Generics-fu wasn't strong enough. Ideas:
// Doesn't compile because you can't use self yet
public var binding: Binding<T> = Binding(
get: { self._userDefaults.fetch(self.key) },
set: { self._userDefaults.save($0, for: self.key) }
)
// Compiles but the call site doesn't compile, or maybe I'm doing something wrong
public func binding() -> Binding<T> {
return Binding<T>(
get: { self._userDefaults.fetch(self.key) },
set: { self._userDefaults.save($0, for: self.key) }
)
}
I've seen the solution in the sample SwiftUI project (@State
var and onChangeOf/onReceive
), but I find it similarly inelegant.
One other idea, if you are using the enum version of AppSettings
is to just use @AppStorage
directly:
struct ContentView: View {
@AppStorage(AppSettingsKey.debugMode.rawValue) var debugMode: Bool = false
var body: some View {
Toggle("Debug Mode", isOn: $debugMode)
.padding()
}
}
I assume this suffers the same problem you mention in the blog post where @AppStorage
still gets default values wrong.
@jessesquires will take care of this. Please do not submit PRs. 😄
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.