Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make the RealmSwift RealmOptional compatible with Swift Codable?

Im facing an issue where I can't make the RealmOptional compatible with swift new Codable feature with json decoder.

Cosider the following Realm object.

class School: Object, Codable {

    @objc dynamic var id: Int64 = 0

    @objc dynamic var name: String?
    var numberOfStudents = RealmOptional<Int64>()
    var classes = List<Class>()

    enum CodingKeys: String, CodingKey {
       case id
       case name
       case numberOfStudents
       case classes
    }
}

class Class: Object, Codable {
    var name: String?
    var numberOfStudents = RealmOptional<Int64>()
}

Here we can declare the class as Codable because I wrote an extension for RealmOptinal with the help of this gist. But the problem is when the decoder decodes the json.

Consider this json

let jsonData = """
[
    "id": 1234,
    "name": "Shreesha",
    "numberOfStudents": nil,
    "classes": {
       "name": "Class V",
       "numberOfStudents": 12
    }
]
""".data(using: .utf8)!

In this json all the data are passed and this decodes perfectly with the code.

let decoder = JSONDecoder()

let decoded = try! decoder.decode(School.self, from: jsonData)

But if I remove the numberOfStudents key from the json data which supposed to be a RealmOptional object it will throw an error and it will not decode because RealmOptional is not a swift optional so the decoder thinks that there should be a key in the json data. In JSONDecoder it doesn't try to decode if the key is not there in the json and the property is declared as optional. It simply skips to other keys.

Until now I didn't override the initialiser because we had all the supporting extensions for RealmOptional Realm Lists etc. But now I have to override the init(from decoder: Decoder) to decode it manually and the Realm model has more than 50 properties in it (You know what I mean).

If we override the initialiser I feel there is not point in using JSONDecoder because there is more manual work than using JSONDecoder.

required convenience init(from decoder: Decoder) throws {
    self.init()
    let container = try decoder.container(keyedBy: CodingKeys.self)

    id = try container.decodeIfPresent(Int64.self, forKey: .id) ?? 0
    name = try container.decodeIfPresent(String?.self, forKey: .name) ?? ""
    numberOfStudents = try container.decodeIfPresent(RealmOptional<Int64>.self, forKey: .numberOfStudents) ?? RealmOptional<Int64>()

    let classesArray = try container.decode([Class].self, forKey: .classes)
    classes.append(objectsIn: classesArray)
}

So can someone suggest me the alternate solution to make the RealmOptional compatible with JSONDecoder so that we don't have to override the initialisers.

like image 549
Shreesha Kedlaya Avatar asked Jul 12 '18 09:07

Shreesha Kedlaya


People also ask

Why use realm Swift?

Many applications use Realm Swift – from chart-topping apps in the App Store to the world’s smallest cloud-hosted, SwiftUI chat app. Learn how Realm helps teams build better apps, faster. “ Realm makes the data persistence layer so easy on iOS. We have a big advantage in terms of developer speed and general feature development.

What are the advantages of codable protocol in Swift?

Here are the advantages of Codable protocol: 1) Type safety. You don’t need typecasting or parsing the strings read from the file. Swift does for you all the low-level reading and parsing only returning you a ready to use object of a concrete type. 2) The Simplicity of usage.

Can a swift iOS app work with multiple data sources?

It’s hard to imagine modern Swift iOS application that doesn’t work with multiple data sources like servers, local cache DB, etc, or doesn’t parse/convert data between different formats.


2 Answers

Here is something you can do to work around the problem. Create a new class which supports decoding and has RealmOptional as its property.

class OptionalInt64: Object, Decodable {
    private var numeric = RealmOptional<Int64>()

    required public convenience init(from decoder: Decoder) throws {
        self.init()

        let singleValueContainer = try decoder.singleValueContainer()
        if singleValueContainer.decodeNil() == false {
            let value = try singleValueContainer.decode(Int64.self)
            numeric = RealmOptional(value)
        }
    }

    var value: Int64? {
        return numeric.value
    }

    var zeroOrValue: Int64 {
        return numeric.value ?? 0
    }
}

Then, instead of using RealmOptional in your school class, use this new OptionalInt64 class,

class School: Object, Codable {

    @objc dynamic var id: Int64 = 0

    @objc dynamic var name: String?
    @objc dynamic  var numberOfStudents: OptionalInt64?
    var classes = List<Class>()

    enum CodingKeys: String, CodingKey {
       case id
       case name
       case numberOfStudents
       case classes
    }
}

Note that now instead of using RealmOptional, you are using RealmNumeric? which is of type Optional. Since, it is optional, automatic decoding uses decodeIfPresent method to decode the optional value. And if it is not present in json the value will simply become nil.

like image 54
Sandeep Avatar answered Sep 23 '22 14:09

Sandeep


I have modified the solution of Sandeep to be more generic:

class RealmOptionalCodable<Value: Codable>: Object, Codable where Value: RealmSwift.RealmOptionalType {

    private var numeric = RealmOptional<Value>()

    var value: Value? {
        get {
            numeric.value
        }
        set {
            numeric.value = newValue
        }
    }


    required public convenience init(from decoder: Decoder) throws {
        self.init()

        let singleValueContainer = try decoder.singleValueContainer()
        if singleValueContainer.decodeNil() == false {
            let value = try singleValueContainer.decode(Value.self)
            numeric = RealmOptional(value)
        }
    }

}

Using

@objc dynamic  var numberOfStudents: RealmOptionalCodable<Int>?
like image 45
Mickael Belhassen Avatar answered Sep 20 '22 14:09

Mickael Belhassen