Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

RealmSwift and Codable when using optional types

I've been using RealmSwift and Codable for a long time in a project, however, my api dev just gave me two new properties that only exist on some return calls of an object. If I call getUserInfo, I receive a user model without these two properties. In such an instance, you would use decodeIfPresent in codable and set the data type to optional. However, these two fields are epoch time values, making them some sort of number. Realm requires your datatypes to be prefixed with @objc.

@objc dynamic var scanIn:Double = 0

Of course, all number primitives work like this, but NONE of them work as optionals. You must use NSNumber or similar to use optionals with ObjC, but lucky me, Codable doesn't work with NSNumber. I know I have a lot of different options here, but I was really looking for something simple and quick that wouldn't require me to rebuild the entire model with mapping or convert everything when I receive it. I'll be writing that out a workaround for now, but I would really like to keep things simple and clean.

I tried setting a value if none return and just using a non-optional type like this

scanIn = try container.decodeIfPresent(Double.self, forKey:.scanIn) ?? 0

However, this sets ALL values to 0 for some reason. I have no idea why it does that, but another dev has advised that it doesn't work in codable like that and I had to set the double to optional. I would like to clarify that this number exists immediately before conversion, but is 0 after.

Any ideas on an easy fix? Maybe I'm doing something wrong?

like image 741
Nox Avatar asked Feb 06 '26 19:02

Nox


2 Answers

You could use the RealmOptional<Double> type.

As stated in the documentation:

Optional numeric types are declared using the RealmOptional type:

let age = RealmOptional<Int>()

RealmOptional supports Int, Float, Double, Bool, and all of the sized versions of Int (Int8, Int16, Int32, Int64).

like image 133
Vin Gazoil Avatar answered Feb 09 '26 10:02

Vin Gazoil


Vin Gazoli gave me the missing key.

First, RealmOptional needs to be declared as a let, so in the init you need to set the value by using myObject.myVariable.value = newValue. Then, everywhere you use it you must use it as obj.variable.value as well. RealmOptional does not conform to codable though, so you must write an extension. You can find it below as well as a link to where I received it.

ex of object:

class Attendee: Object,Codable {

@objc dynamic var id = 0
@objc dynamic var attendeeId = 0
@objc dynamic var lanId = ""
@objc dynamic var firstName = ""
@objc dynamic var lastName = ""
@objc dynamic var email = ""
@objc dynamic var employeeId = ""
@objc dynamic var badgeId = ""
@objc dynamic var department = ""
@objc dynamic var picture:String? = nil
let scanIn = RealmOptional<Double>()
let scanOut = RealmOptional<Double>()

override static func primaryKey () -> String? {
    return  "id"
}

private enum CodingKeys: String, CodingKey {
    case id, attendeeId, lanId, firstName, lastName, email, employeeId, badgeId, department, picture, scanIn, scanOut
}

required convenience init(from decoder: Decoder) throws {
    self.init()
    let container = try decoder.container(keyedBy: CodingKeys.self)
    id = try container.decode(Int.self, forKey:.id)
    attendeeId = try container.decodeIfPresent(Int.self, forKey:.attendeeId) ?? 0
    lanId = try container.decode(String.self, forKey:.lanId)
    firstName = try container.decode(String.self, forKey:.firstName)
    lastName = try container.decode(String.self, forKey:.lastName)
    email = try container.decode(String.self, forKey:.email)
    employeeId = try container.decode(String.self, forKey:.employeeId)
    badgeId = try container.decode(String.self, forKey:.badgeId)
    department = try container.decode(String.self, forKey:.department)
    picture = try container.decodeIfPresent(String.self, forKey:.picture)
    self.scanIn.value = try container.decodeIfPresent(Double.self, forKey:.scanIn) ?? 0
    self.scanOut.value = try container.decodeIfPresent(Double.self, forKey:.scanOut) ?? 0
}

The below code is REQUIRED to make the above object function. Retrieved from this link in conjunction with h1m5's fix in the comments of that page. The below is for Double. The link has other primitives.

func assertTypeIsEncodable<T>(_ type: T.Type, in wrappingType: Any.Type) {
guard T.self is Encodable.Type else {
    if T.self == Encodable.self || T.self == Codable.self {
        preconditionFailure("\(wrappingType) does not conform to Encodable because Encodable does not conform to itself. You must use a concrete type to encode or decode.")
    } else {
        preconditionFailure("\(wrappingType) does not conform to Encodable because \(T.self) does not conform to Encodable.")
    }
}
}

func assertTypeIsDecodable<T>(_ type: T.Type, in wrappingType: Any.Type) {
guard T.self is Decodable.Type else {
    if T.self == Decodable.self || T.self == Codable.self {
        preconditionFailure("\(wrappingType) does not conform to Decodable because Decodable does not conform to itself. You must use a concrete type to encode or decode.")
    } else {
        preconditionFailure("\(wrappingType) does not conform to Decodable because \(T.self) does not conform to Decodable.")
    }
}
}

extension RealmOptional : Encodable where Value : Encodable {
public func encode(to encoder: Encoder) throws {
    assertTypeIsEncodable(Value.self, in: type(of: self))

    var container = encoder.singleValueContainer()
    if let v = self.value {
        try (v as Encodable).encode(to: encoder)  // swiftlint:disable:this force_cast
    } else {
        try container.encodeNil()
    }
}
}

extension RealmOptional : Decodable where Value : Decodable {
public convenience init(from decoder: Decoder) throws {
    // Initialize self here so we can get type(of: self).
    self.init()
    assertTypeIsDecodable(Value.self, in: type(of: self))

    let container = try decoder.singleValueContainer()
    if !container.decodeNil() {
        let metaType = (Value.self as Decodable.Type) // swiftlint:disable:this force_cast
        let element = try metaType.init(from: decoder)
        self.value = (element as! Value)  // swiftlint:disable:this force_cast
    }
}
}
like image 28
Nox Avatar answered Feb 09 '26 11:02

Nox



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!