jessesquires / Foil

A lightweight property wrapper for UserDefaults done right
https://jessesquires.github.io/Foil/
MIT License
459 stars 26 forks source link

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

Closed Larry-Gensch closed 2 years ago

Larry-Gensch commented 3 years ago

Have you read the Contributing Guidelines?

General Information

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

jessesquires commented 3 years ago

Thanks so much for filing this issue, @Larry-Gensch ! šŸ‘šŸ¼

You bring up valid concerns. However, these omissions were actually intentional, but I'll be happy to add support (or accept PRs) if there is demand from people who are using this library. šŸ˜Š

My reasoning below:

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.

The Swift programming guide explicitly discourages the use specific integer sizes, and instead encourages using "plain" Int. Based on these general language conventions, and the specific context of UserDefaults, I think it will be very rare that anyone wants to explicitly store something other than Int.

However, I think there is a case to be made for supporting "plain" UInt. We should probably do that.

Of course, supporting Int8, Int16, etc. is trivial -- I'm curious how many people are actually storing these types in UserDefaults instead of plain Int.

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

This isn't true, actually. It's not JSON-based, it's PropertyList-based. Non-string keyed dictionaries are not property list types.

From the Property List Programming Guide, emphasis mine:

If an array or dictionary contains objects that are not property-list objects, then you cannot save and restore the hierarchy of data using the various property-list methods and functions. And although NSDictionary and CFDictionary objects allow their keys to be objects of any type, if the keys are not string objects, the collections are not property-list objects.

Attempting to store a dictionary with non-string keys will throw an exception. I tried. šŸ˜Š

Objective-C values that are directly supported by UserDefaults are all missing: NSString, NSNumber, NSDictionary, NSArray, etc.

This was also intentional, under the assumption that most people are almost always using Swift types. Again, specifically in the context of UserDefaults -- if you have a Swift project (even if it's mixed with ObjC) and you want to use a Swift-only feature (this property wrapper), why would you opt for NSNumber or NSDictionary when the Swift alternatives are typically a much better choice?

Again, if there's a demand here, I'm happy to add this. But I don't expect there to be a high demand for this...

Larry-Gensch commented 3 years ago

My mistake regarding Dictionary. I also accept your comments.

Using NSDictionary or NSArray instead of Dictionary and Array is sometimes necessary for class semantics for optimal resource storage/retrieval.

jessesquires commented 3 years ago

@Larry-Gensch no problem! šŸ˜„

sometimes necessary for class semantics for optimal resource storage/retrieval.

I suspect if you are having this problem, the data you are trying to store in UserDefaults is too large and you should probably be using a database, right?

yujinqiu commented 3 years ago

@jessesquires Can you add support UserDefaultsSerializable to NSColor or can you provide an example of user define type that conform to UserDefaultsSerializable protocol? The doc "Adding support for custom types is possible by conforming to UserDefaultsSerializable" is too little information to me. My use case is : I want to store use customized color settings in UserDefaults.

Thanks.

jessesquires commented 3 years ago

hey @yujinqiu -- all you need to do is declare conformance to the protocol and implement the methods on your custom type

https://github.com/jessesquires/Foil/blob/main/Sources/UserDefaultsSerializable.swift#L32-L44

struct MyModel: UserDefaultsSerializable {

    var storedValue: [Float] { 
         // todo
    }

    init(storedValue: [Float]) {
         // todo
    }
}
yujinqiu commented 3 years ago

Hi @jessesquires thanks for your example for custom type. It works when the MyModel is Struct. But when it comes to NSColor which is Cocoa Class, it failed. I try to give a POC below.

// we cannot add code inside MyModel here directly,  NSColor is Cocoa Class
class MyModel {
    init() {}
}

extension MyModel: UserDefaultsSerializable {
    public var storedValue: MyModel {
        return MyModel()
    }

    public required convenience init(storedValue _: MyModel) {
        self.init()
    }
}

We'll get Xcode error

'required' initializer must be declared directly in class 'MyModel' (not in an extension)

My current working around is convert NSColor variable to Data Type and save it with Foil, but it's not elegant :-( . Is there any suggestion to store Class type in UserDefaults ?

jessesquires commented 2 years ago

Hey folks šŸ‘‹šŸ¼

I feel like the initial, main concern here has been sufficiently discussed and addressed. I'm going to close this issue.

Feel free to open a new issue for follow-ups! šŸ˜„