Coder Social home page Coder Social logo

jessesquires / foil Goto Github PK

View Code? Open in Web Editor NEW
424.0 6.0 24.0 484 KB

A lightweight property wrapper for UserDefaults done right

Home Page: https://jessesquires.github.io/Foil/

License: MIT License

Swift 83.02% Shell 5.18% Ruby 11.80%
swift ios macos watchos tvos property-wrapper userdefaults foil

foil's People

Contributors

basememara avatar dependabot[bot] avatar ejensen avatar jessesquires avatar jonnybeegod avatar jordanekay avatar kansaichris avatar nolanw avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

foil's Issues

Support more types by default. Expand UserDefaultsSerializable to support Int8, Int16, NSArray, NSNumber, etc.

Have you read the Contributing Guidelines?

General Information

  • Project version:
    1.0.0

  • Platform/OS version:
    iOS 13+

  • Any related GitHub issues:

Describe the bug

The current implementation of UserDefaultsSerializable is too simplistic because it only defines a subset of the values that can be automatically stored in UserDefaults:

  1. Only 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.
  2. The Dictionary support seems to be JSON based [String:: Value.StoredValue]) where UserDefaults actually allows any supported UserDefaults value for a key ([Key.StoredValue: Value.StoredValue])
  3. Oobjective-C values that are directly s supported by UserDefaults are all missing: NSString, NSNumber, NSDictionary, NSArray, etc.

Steps to reproduce

NA

Expected behavior

Clearly and concisely describe what you expected to happen.

Stack trace, compiler error, code snippets

  1. Possible solution to (1): See example one below.
  2. Possible solution to (2): See example two below
  3. Exercise left to the reader

Example 1 (NSNumber values)

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
    }
}

Example 2 (Dictionary)

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)
        }
    }
}

Screenshots

N/A

Additional context

N/A

[Feature]: Be able to save `Codable` structs (or a workaround?)

Guidelines

  • I agree to follow this project's Contributing Guidelines.

Description

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.

Problem

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.

Proposed Solution

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?

Alternatives Considered

I'm open to any other ideas as well. Thanks for your help and your great work!

Using UserDefaults Publisher?

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)
}

Setting non-nil String value crashes on force-unwrapping fetchOptional?

Guidelines

  • I agree to follow this project's Contributing Guidelines.

Project Version

5.0.1

Platform and OS Version

macOS 14.3.1

Affected Devices

MBP

Existing Issues

No response

What happened?

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.

Steps to reproduce

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:

CleanShot 2024-04-03 at 18 46 32@2x

Expected behavior

Expected it to not use the Optional variant for fetch if the value is not there.

Attachments

No response

Screenshots or Videos

No response

Additional Information

No response

[Feature]: A demo for SwiftUI project

Guidelines

  • I agree to follow this project's Contributing Guidelines.

Description

How to integrated Foil with SwiftUI project ?

Problem

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.

Proposed Solution

N/A

Alternatives Considered

N/A

Drop CocoaPods support?

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).

Deployment targets inconsistency between Pod and SPM metafiles

Have you read the Contributing Guidelines?
yes

General Information

  • Project version:

1.1.0 / the latest state

  • Platform/OS version:

doesn't matter

  • IDE version:

doesn't matter

  • Devices:

doesn't matter

  • Any related GitHub issues:

Describe the bug

Clearly and concisely describe the bug.
Podspec declares deployment targets as 12.0 / 10.13 while Package.swift says 13 / 10.15.

Steps to reproduce

Provide numbered steps to follow that reproduce the bug.
No steps.

Expected behavior

Clearly and concisely describe what you expected to happen.
Deployment targets are identical.

Stack trace, compiler error, code snippets

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

Screenshots

If applicable, add screenshots, gifs, or videos to help explain your problem.

Additional context

Add any other useful information about the problem here.

  • SPM doesn't support tvOS and watchOS (dunno if that's possible at all though)
  • also make sure that deployment targets in Xcode projects are synced

[Suggestion] Succinct implicit initialization

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
    }
    
    ...

Rename "WrappedDefault" and "WrappedDefaultOptional"?

Initially I struggled to come up with a good name here and I honestly don't like this name at all.

  • No property wrappers use "wrapped" in the name. It's kinda lame, like writing class NetworkManagerClass { }.
  • SwiftUI took AppStorage, so we can't use that.

Right now, I'm thinking FoilStorage and FoilStorageOptional ?

Open to ideas here.

Lower deployment target

Not sure if it should be treated as a feature or bug.

Have you read the Contributing Guidelines?
Yes.

Describe the feature

Lower deployment targets.

Is your feature request related to a problem?

Yes, I can't depend on this library because my library supports lower deployment target.

Proposed solution

Simply lower them.

Alternatives considered

None.

Additional context

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.

[Bug]: user defaults returns outdated value after rebuilding project

Guidelines

  • I agree to follow this project's Contributing Guidelines.

Project Version

2.0.0

Platform and OS Version

iOS 15, iOS 13.5

Affected Devices

Tested on various Simulators

Existing Issues

No response

What happened?

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.

Steps to reproduce

  1. Build and run the example project
  2. Toggle the flagEnabled value (can also be reproduced with the other example keys)
  3. rebuild and run the project again
  4. Observe the flag value displays the old value again

Expected behavior

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.

Attachments

No response

Screenshots or Videos

bug-report.mp4

Additional Information

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.

Prepare and release 3.0

  • verify changelog and readme
  • veryify podspec and package.swift
  • address test failures from #38
  • bump GH actions CI to latest Xcode? (might fix failures)
  • re-gen docs
  • tag release
  • push to cocoapods

[Feature]: Expose a `Binding` for SwiftUI if possible

Guidelines

  • I agree to follow this project's Contributing Guidelines.

Description

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>'

Problem

N/A

Proposed Solution

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) }
    )
}

Alternatives Considered

  1. I've seen the solution in the sample SwiftUI project (@State var and onChangeOf/onReceive), but I find it similarly inelegant.

  2. 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.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.