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

How to make swifty UserDefaults #972

Open
onmyway133 opened this issue May 28, 2024 · 0 comments
Open

How to make swifty UserDefaults #972

onmyway133 opened this issue May 28, 2024 · 0 comments

Comments

@onmyway133
Copy link
Owner

onmyway133 commented May 28, 2024

We want to have a swifty UserDefaults API that works with subscript and in a type safe manner.

extension Defaults.Keys {
    static let string = Defaults.Key("string", default: "0")
}

XCTAssertEqual(defaults[.string], "0")
defaults[.string] = "1"
XCTAssertEqual(defaults[.string], "1")

UserDefaults plist compatibility

Define Compatible protocol that allows value to be plist compatible

The value parameter can be only property list objects: NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary. For NSArray and NSDictionary objects, their contents must be property list objects.

public protocol Compatible: Equatable {}

extension Int: Compatible {}
extension String: Compatible {}
extension Bool: Compatible {}
extension Date: Compatible {}
extension Array: Compatible where Element: Compatible {}
extension Dictionary: Compatible where Key: Compatible, Value: Compatible {}

Next, define Defaults that accepts UserDefaults as initialize dependency, so that we can swap UserDefaults.

Key with generic type

We define Key with phantom type Value so we know which value this key is pointing to, this makes it easier to reason about the code.

Since Swift has limitation Static stored properties not supported in generic types, we can't extend our Key with static stored properties, we have to do via computed property

extension Defaults.Key {
    static var string:  Defaults.Key<String> { .init("string", default: "0") }
}

This works, but does not look nice. To workaround this, we define class AnyKey and make our Key class as well and inherited this AnyKey class.

Make a typealias typealias Keys = AnyKey so we can refer to Defaults.Keys when we define our keys.

public class Defaults {
    public var suite: UserDefaults

    public init(suite: UserDefaults = .standard) {
        self.suite = suite
    }

    public subscript<Value: Compatible>(key: Key<Value>) -> Value {
        get {
            if let value = suite.object(forKey: key.name) as? Value {
                return value
            }

            return key.defaultValue
        }
        set {
            suite.set(newValue, forKey: key.name)
        }
    }

    public func exists<Value: Compatible>(key: Key<Value>) -> Bool {
        suite.object(forKey: key.name) != nil
    }
}

extension Defaults {
    public typealias Keys = AnyKey

    public class Key<Value: Compatible>: AnyKey {
        var defaultValue: Value

        public init(_ name: String, default defaultValue: Value) {
            self.defaultValue = defaultValue

            super.init(name: name)
        }
    }

    public class AnyKey {
        var name: String

        init(name: String) {
            self.name = name
        }
    }
}

extension Defaults.AnyKey: Equatable {
    public static func == (lhs: Defaults.AnyKey, rhs: Defaults.AnyKey) -> Bool {
        lhs.name == rhs.name
    }
}

extension Defaults.AnyKey: Hashable {
    public func hash(into hasher: inout Hasher) {
        hasher.combine(name)
    }
}

How about Optional

We can support Optional as well, as long as it's underlying value is compatible. Since the type is defined via Key, we can't accidentally use Optional when the Key has non Optional value

extension Optional: Compatible where Wrapped: Compatible {}

extension Defaults {
    public subscript<Value: Compatible>(key: Key<Optional<Value>>) -> Value? {
        get {
            if let value = suite.object(forKey: key.name) as? Value {
                return value
            }
            
            return nil
        }
        set {
            if let newValue {
                suite.set(newValue, forKey: key.name)
            } else {
                suite.removeObject(forKey: key.name)
            }
        }
    }
}

extension Defaults.Keys {
    static let optional = Defaults.Key<Int?>("optional.int", default: nil)
}

func testOptional() {
    XCTAssertNil(defaults[.optional])

    defaults[.optional] = 1
    XCTAssertEqual(defaults[.optional], 1)

    defaults[.optional] = nil
    XCTAssertNil(defaults[.optional])
}
@onmyway133 onmyway133 changed the title How to make swiftty UserDefaults How to make swifty UserDefaults May 28, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant