Home > Back-end >  Creating a expiry date for UserDefaults values
Creating a expiry date for UserDefaults values

Time:09-13

So I had this idea of creating an expiry for UserDefaults. This is the approach I was starting to take but I'm stuck.

struct TimedObject<T: Codable>: Codable {
    let object: T
    let expireDate: Date
}

and then:

extension UserDefaults {
    
    func set<T: Codable>(_ value: T, forKey key: String, expireDate: Date) {
        let timedObject = TimedObject<T>(object: value, expireDate: expireDate)
        let encoder = JSONEncoder()
        if let encoded = try? encoder.encode(timedObject) {
            UserDefaults.standard.set(encoded, forKey: key)
    }

    override open class func value(forKey key: String) -> Any? {
        guard let value = self.value(forKey: key) else {
            return nil
        }
        if TimedObject<???>.self == type(of: value) { // This is where I'm stuck
            ...
        }
    }

So if I would name the type and not use generics I would easily solve this. But naturally I prefer to use generics. Can this be done?

CodePudding user response:

I know OP is using a struct to wrap the stored value but I would still like to offer a different protocol based solution where any type that should be stored with an expiration date needs to conform to this protocol.

Here is the protocol for I am using

protocol TimedObject: Codable {
    associatedtype Value: Codable

    var value: Value { get }
    var expirationDate: Date { get }
}

and the functions to store and retrieve from UserDefaults

extension UserDefaults {
    func set<Timed: TimedObject>(_ value: Timed, forKey key: String) {
        if let encoded = try? JSONEncoder().encode(value) {
            UserDefaults.standard.set(encoded, forKey: key)
        }
    }

    func value<Timed: TimedObject>(_ type: Timed.Type, forKey key: String) -> Timed.Value? {
        guard let data = self.value(forKey: key) as? Data, let object = try? JSONDecoder().decode(Timed.self, from: data) else {
            return nil
        }
        return object.expirationDate > .now ? object.value : nil
    }
}

Finally an example

struct MyStruct: Codable {
    let id: Int
    let name: String
}

extension MyStruct: TimedObject {
    typealias Value = Self

    var value: MyStruct { self }

    var expirationDate: Date {
        .now.addingTimeInterval(24 * 60 * 60)
    }
}

let my = MyStruct(id: 12, name: "abc")

UserDefaults.standard.set(my, forKey: "my")

let my2 = UserDefaults.standard.value(MyStruct.self, forKey: "my")

CodePudding user response:

Since you're returning Any?, it is best to create another struct to point to TimedObject as you don't need the object property when returning Any?:

struct Expiry: Codable {
    var expireDate: Date
}

struct TimedObject<T: Codable>: Timable {
    let object: T
    var expireDate: Date
}

override open class func value(forKey key: String) -> Any? {
    guard let value = self.value(forKey: key) as? Data else {
        return nil
    }
    if let timed = try? JSONDecoder().decode(Expiry.self, from: value) {
    //do anything with timed
    }
    //make sure to return value
}

And here's a method to access TimedObject:

func timedObject<T: Codable>(forKey key: String) -> TimedObject<T>? {
    guard let value = self.data(forKey: key) as? Data, let timed = try? JSONDecoder().decode(TimedObject<T>.self, from: value) else {
        return nil
    }
    return value
}

CodePudding user response:

If you make the value() method generic then you can reverse the process done in the set() method: retrieve the data and decode it as a TimedObject<T>.

However, I would choose a different name to avoid possible ambiguities with the exisiting value(forKey:) method. Also I see no reason why this should be a class method.

Note also that your generic set() method should call the non-generic version on the same instance.

extension UserDefaults {
    
    func set<T: Codable>(_ value: T, forKey key: String, expireDate: Date) {
        let timedObject = TimedObject(object: value, expireDate: expireDate)
        let encoder = JSONEncoder()
        if let encoded = try? encoder.encode(timedObject) {
            set(encoded, forKey: key)
        }
    }
    
    func expiringValue<T: Codable>(forKey key: String) -> T? {
        guard let data = self.data(forKey: key) else {
            return nil
        }
        let decoder = JSONDecoder()
        guard let decoded = try? decoder.decode(TimedObject<T>.self, from: data) else {
            return nil
        }
        // check expire date ...
        return decoded.object
    }
}

Example usage:

let val1 = UserDefaults.standard.expiringValue(forKey: "foo") as String?
let val2: String? = UserDefaults.standard.expiringValue(forKey: "bar")

In both cases, expiringValue(forKey:) is called with the inferred type.

Or in combination with optional binding:

if let val: String = UserDefaults.standard.expiringValue(forKey: "test") {
    print(val)
}

Another option is to pass the desired type as an additional argument:

    func value<T: Codable>(forKey key: String, as: T.Type) -> T? {
        guard let data = self.data(forKey: key) else {
            return nil
        }
        let decoder = JSONDecoder()
        guard let decoded = try? decoder.decode(TimedObject<T>.self, from: data) else {
            return nil
        }
        // check expire date ...
        return decoded.object
    }

which is then used as

let val = UserDefaults.standard.value(forKey: "foo", as: String.self)
  • Related