Codable Struct: Get Value by Key - swift

I have for example codable struct User:
struct User: Codable {
// ...
var psnTag: String?
var xblTag: String?
var steamTag: String?
var nintendoTag: String?
var instagramId: String?
// ...
}
and stringKey as String = "psnTag"
How can I get the value from instance by stringKey?
Like this:
let stringKey = "psnTag"
user.hasKeyForPath(stringKey) //return Bool
user.valueForPath(stringKey) //return AnyObject

Start with extending Encodable protocol and declare methods for hasKey and for value
Using Mirror
extension Encodable {
func hasKey(for path: String) -> Bool {
return Mirror(reflecting: self).children.contains { $0.label == path }
}
func value(for path: String) -> Any? {
return Mirror(reflecting: self).children.first { $0.label == path }?.value
}
}
Using JSON Serialization
extension Encodable {
func hasKey(for path: String) -> Bool {
return dictionary?[path] != nil
}
func value(for path: String) -> Any? {
return dictionary?[path]
}
var dictionary: [String: Any]? {
return (try? JSONSerialization.jsonObject(with: JSONEncoder().encode(self))) as? [String: Any]
}
}
Now you can use it like this:
.hasKey(for: "key") //returns Bool
.value(for: "key") //returns Any?

Related

Creating a expiry date for UserDefaults values

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?
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) {
self.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")
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
}
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)

Skipping empty string with JSONEncoders swift

I a codable serialization extension which I use to turn my Codable struct to dictionaries, the problem I am facing is strings. I get string value from my UITextField at at times this value could be empty and as a result an empty string is decoded. How can I return nil if the value is an empty string.
extension Encodable {
var requestDictionary: [String: Any]? {
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
guard let data = try? encoder.encode(self) else { return nil }
return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { $0 as? [String: Any] }
}
}
if I have a Struct
let example = Exa(age: 10, name: "")
let dict = example.requestDictionary
print(dict)
I want it to just print ["age": 10] and return nil for the empty string
You can implement your own String encoding method extending KeyedEncodingContainer:
extension KeyedEncodingContainer {
mutating func encode(_ value: String, forKey key: K) throws {
guard !value.isEmpty else { return }
try encodeIfPresent(value, forKey: key)
}
}
Btw your request dictionary can be simplified as:
extension Encodable {
var dictionary: [String: Any]? {
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
return try? JSONSerialization.jsonObject(with: encoder.encode(self)) as? [String: Any]
}
}
Playground testing:
struct Exa: Encodable {
let age: Int
let name: String
}
let example = Exa(age: 10, name: "")
let dict = example.dictionary!
print(dict) // "["age": 10]\n"
I'll just another approach using a property wrapper to mark which properties could be skipped.
#propertyWrapper
struct SkipEmpty {
var wrappedValue: String
}
extension SkipEmpty: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self.wrappedValue = try container.decode(String.self)
}
func encode(to encoder: Encoder) throws {
// nothing to do here, see below
}
}
But to actually skip, you'd also need to create a overload for the KeyedEncodingContainer.encode method for the SkipEmpty type:
extension KeyedEncodingContainer {
mutating func encode(_ value: SkipEmpty, forKey key: K) throws {
if !value.wrappedValue.isEmpty {
try encode(value.wrappedValue, forKey: key) // encode the value here
}
}
}
You could possibly try to make it more generic, e.g. SkipEmpty<T: Codable> and provide another argument for the value to skip or a predicate, etc...
The usage is:
struct Exa: Encodable {
var age: Int
#SkipEmpty var name: String
}

Decoding a nullable heterogeneous dictionary

I need to decode a dictionary of the form [(Heterogeneous object): CGFloat]?. I've got working extensions on the KeyedDecodingContainer that allow me to decode lists of and single heterogeneous objects however I'm struggling to convert these to work with a dictionary. Here is the working method for a optional array:
func decodeArrayIfPresent<T : Decodable, U : ClassFamily>(family: U.Type, forKey key: K) throws -> [T]? {
do {
var container = try self.nestedUnkeyedContainer(forKey: key)
var list = [T]()
var tmpContainer = container
while !container.isAtEnd {
let typeContainer = try container.nestedContainer(keyedBy: Discriminator.self)
let family: U? = try typeContainer.decodeIfPresent(U.self, forKey: U.discriminator)
if let type = family?.getType() as? T.Type {
list.append(try tmpContainer.decode(type))
}
}
return list
} catch DecodingError.keyNotFound {
return nil
} catch DecodingError.valueNotFound {
return nil
}
}
And this is where I've got to for a dictionary:
func decodeDictIfPresent<T : Decodable, U : ClassFamily>(family: U.Type, forKey key: K) throws -> [T: Any]? {
do {
var container = try self.nestedUnkeyedContainer(forKey: key)
var dict = [T: Any]()
var tmpContainer = container
while !container.isAtEnd {
let typeContainer = try container.nestedContainer(keyedBy: Discriminator.self)
let family: U? = try typeContainer.decodeIfPresent(U.self, forKey: U.discriminator)
if let type = family?.getType() as? T.Type {
list.append(try tmpContainer.decode(type))
}
}
return dict
} catch DecodingError.keyNotFound {
return nil
} catch DecodingError.valueNotFound {
return nil
}
}
Requirements:
protocol ClassFamily: Decodable {
/// The discriminator key.
static var discriminator: Discriminator { get }
/// Returns the class type of the object coresponding to the value.
func getType() -> AnyObject.Type
}
/// Discriminator key enum used to retrieve discriminator fields in JSON payloads.
enum Discriminator: String, CodingKey {
case type = "type"
}
Any pointers in the right direction would be appreciated! Thanks in advance

App Crashing with error: generic parameter 'T' could not be inferred

I'm trying to get custom object which is hashable from UserDefault.
My custom model is defined below:
class WorkerProfileResponse: Mappable, Hashable{
static func == (lhs: WorkerProfileResponse, rhs: WorkerProfileResponse) -> Bool {
return lhs.id == rhs.id
}
var hashValue: Int{
return self.id!
}
var id, loginStatus, lastLogin, lastActive: Int?
var username, email, mobileNumber: String?
var userCategories: [String]?
var userSubCategories: [String]?
var biometricToken: String?
var accessToken: AccessToken?
var userStatus: UserStatus?
var userProfile: UserProfile?
required init(map: Map) {
}
func mapping(map: Map) {
id <- map["id"]
loginStatus <- map["is_logged_in"]
lastLogin <- map["last_login"]
lastActive <- map["last_active"]
biometricToken <- map["biometricToken"]
username <- map["username"]
email <- map["email"]
mobileNumber <- map["mobile_number"]
accessToken <- map["accessToken"]
userStatus <- map["userStatus"]
userCategories <- map["userCategories"]
userSubCategories <- map["userSubCategories"]
userProfile <- map["userProfile"]
}
}
My userdefault method is:
class func getModel<T: Hashable>(key: String) -> T {
let decoded = UserDefaults.standard.data(forKey: key)
let decodedModel = NSKeyedUnarchiver.unarchiveObject(with: decoded!) as! T
return decodedModel
}
And I'm calling it like this:
UserDefault.getModel(key: "workerProfile")
App is crashing when I'm calling this method I don't understand the reason, error is:
error: generic parameter 'T' could not be inferred
I'm answering my own question, if it helps anyone in the future.
It was crashing while decoding because there was no value present in userdefault.
This line had the issue because of force casting:
let decodedModel = NSKeyedUnarchiver.unarchiveObject(with: decoded!) as! T
I've changes this method:
class func getModel<T: Hashable>(key: String) -> T {
let decoded = UserDefaults.standard.data(forKey: key)
let decodedModel = NSKeyedUnarchiver.unarchiveObject(with: decoded!) as! T
return decodedModel
}
To this:
class func getModel<T: Hashable>(key: String) -> T? {
let decoded = UserDefaults.standard.data(forKey: key)
if decoded != nil{
let decodedModel = NSKeyedUnarchiver.unarchiveObject(with: decoded!) as! T
return decodedModel
}
else
{
return nil
}
}

swift - UserDefaults wrapper

I want to make userDefaults easier to use. I write this code.
But I don't want to create a allKeys enum(cases voipToken, userId, userName, displayName,...) as the key path for my property.
If I want add a var accessToken to struct LoginInfo, I must add a case accessToken to enum AllKeys. I use CoreStore.key(.accessToken) get a keyPath for CoreStore.LoginInfo.accessToken.
import Foundation
struct CoreStore {
// static let sharedInstance = CoreStore()
// private init(){}
private static let keyPrefix = "_shared_store_"
enum allKeys: String {
case accessToken
}
static func key(_ key: allKeys) -> String {
return keyPrefix + key.rawValue
}
struct LoginInfo: CoreStoreSettable,CoreStoreGettable { }
}
extension CoreStore.LoginInfo {
static var accessToken: String? {
set {
set(value: newValue, forKey: CoreStore.key(.accessToken))
}
get {
return getString(forKey: CoreStore.key(.accessToken))
}
}
// ...
}
protocol CoreStoreSettable {}
extension CoreStoreSettable {
static func set(value: String?, forKey key: String) {
UserDefaults.standard.set(value, forKey: key)
}
}
protocol CoreStoreGettable {}
extension CoreStoreGettable {
static func getString(forKey key: String) -> String? {
return UserDefaults.standard.string(forKey: key)
}
}
Questions:
Is there a way to remove "enum allKeys"?
Should I use sharedInstance? Why?
I tried :
static var accessToken: String? {
set {
let keyPath = \CoreStore.Loginfo.accessToken
let keyPathString = keyPath.string
set(value: newValue, forKey: keyPathString)
}
get {
return getString(forKey: CoreStore.key(.accessToken))
}
}
I get error "Type of expression is ambiguous without more context"