I have a Firebase Firestore document, containing String, Number, and GeoPoint values. Here is a sample console output printed by print() function.
[
"name": "Test",
"location": <FIRGeoPoint: (37.165300, 27.590800)>,
"aNumber": 123123
]
Now I want to create a struct for this document, conforming Codableprotocol.
struct TestStruct: Codable {
let name: String
let aNumber: Double
let location: GeoPoint
struct CodingKeys: CodingKey {
case name, location, aNumber
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: CodingKeys.name)
aNumber = try container.decode(Double.self, forKey: CodingKeys.aNumber)
location = try container.decode(GeoPoint.self, forKey: CodingKeys.location)
}
}
// encode is not implemented yet.
This code will show an error since GeoPoint is not conforming Codable protocol.
So I tried to make GeoPoint conform Codable:
extension GeoPoint: Codable {
enum CodingKeys: CodingKey {
case latitute, longitude
}
public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let latitude = try container.decode(Double.self, forKey: CodingKeys.latitute)
let longitude = try container.decode(Double.self, forKey: CodingKeys.latitute)
super.init(latitude: latitude, longitude: longitude)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(latitude, forKey: CodingKeys.latitute)
try container.encode(longitude, forKey: CodingKeys.longitude)
}
}
Now, IDE is mad at me!
The initializer init(from:) should be required, but extensions cannot have required initializers. Also, extensions cannot have designated initializers, so initializer should be convenience too. A stupid dead-end.
To bypass it, I subclassed GeoPoint:
class ANGeoPoint: GeoPoint, Codable {
enum CodingKeys: CodingKey {
case latitute, longitude
}
public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let latitude = try container.decode(Double.self, forKey: CodingKeys.latitute)
let longitude = try container.decode(Double.self, forKey: CodingKeys.latitute)
super.init(latitude: latitude, longitude: longitude)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(latitude, forKey: CodingKeys.latitute)
try container.encode(longitude, forKey: CodingKeys.longitude)
}
}
and changed
location = try container.decode(GeoPoint.self, forKey: CodingKeys.location)
line to
location = try container.decode(ANGeoPoint.self, forKey: CodingKeys.location)
Now code is clear of IDE warnings. Let's test it:
Firestore.firestore()
.collection("testCollection")
.document("qweasdzxc")
.getDocument { (snap, error) in
if let data = snap?.data() {
let jsonData = JSONSerialization.data(withJSONObject: data, options: .prettyPrinted)
let myStruct = try? JSONDecoder().decode(TestStruct.self, from: jsonData)
}
}
When we run our test code, it will break! Here is my brand-new baby runtime error:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Invalid type in JSON write (FIRGeoPoint)'
let's look back to console output of our example data:
...
"location": <FIRGeoPoint: (37.165300, 27.590800)>
...
Even I try to decode location into ANGeoPoint while decoding the data into TestStruct, the location data coming from Firestore is still GeoPoint. And JSONDecode cannot decode a non-Codable object.
More, as you remember Xcode don't let me create a Codable GeoPoint.
Now I'm stuck! Any suggestions? Thank you.
EDIT: I've found this in the Firebase iOS SDK: https://github.com/firebase/firebase-ios-sdk/commit/13e366738463739f0c21d4cedab4bafbfdb57c6f
But even I'm using the latest version, my code doesn't have this. So I've manually added:
protocol CodableGeoPoint: Codable {
var latitude: Double { get }
var longitude: Double { get }
init(latitude: Double, longitude: Double)
}
extension CodableGeoPoint {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let latitude = try container.decode(Double.self, forKey: CodingKeys.makeKey(name: "latitude"))
let longitude = try container.decode(Double.self, forKey: CodingKeys.makeKey(name: "longitude"))
self.init(latitude: latitude, longitude: longitude)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(latitude, forKey: CodingKeys.makeKey(name: "latitude"))
try container.encode(longitude, forKey: CodingKeys.makeKey(name: "longitude"))
}
}
extension GeoPoint: CodableGeoPoint {}
Now GeoPoint is Codable. But I still cannot decode it with JSONDecoder.
Here is the way to make GeoPoint codable.
I've found this in the Firebase iOS SDK: https://github.com/firebase/firebase-ios-sdk/commit/13e366738463739f0c21d4cedab4bafbfdb57c6f
But even I'm using the latest version, my code doesn't have this I don't know why. So I've manually added:
protocol CodableGeoPoint: Codable {
var latitude: Double { get }
var longitude: Double { get }
init(latitude: Double, longitude: Double)
}
enum CodableGeoPointCodingKeys: CodingKey {
case latitude, longitude
}
extension CodableGeoPoint {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodableGeoPointCodingKeys.self)
let latitude = try container.decode(Double.self, forKey: .latitude)
let longitude = try container.decode(Double.self, forKey: .longitude)
self.init(latitude: latitude, longitude: longitude)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodableGeoPointCodingKeys.self)
try container.encode(latitude, forKey: .latitude)
try container.encode(longitude, forKey: .longitude)
}
}
extension GeoPoint: CodableGeoPoint {}
Since GeoPoint is held in [String: Any]? as an object, and JSONSerilalization can only hold strings and numbers, you cannot serialize data into json.
First, you have to encode GeoPoint then, using JSONSerialization create and jsonObject (aka Dictionary) from it and replace the GeoPoint with this jsonObject then you are ready for decoding the data into Struct. This is applicable for Timestamp object too.
Here is the code representation of what I say:
Firestore.firestore()
.collection("testCollection")
.document("qweasdzxc")
.getDocument { (snap, error) in
if var data = snap?.data() {
// check every key's value if it is a GeoPoint. If it is, convert it into Dictionary. You have to do these for inner values too.
for key in data.keys {
if let val = data[key] as? GeoPoint {
let locData = try JSONEncoder().encode(val)
data[key] = try JSONSerialization.jsonObject(with: locData, options: .allowFragments)
}
}
let jsonData = JSONSerialization.data(withJSONObject: data, options: .prettyPrinted)
let myStruct = try? JSONDecoder().decode(TestStruct.self, from: jsonData)
}
}
Another solution is holding the GeoPoint in a variable and deleting it from data. After completion of serialization and decoding, you can manually set struct's GeoPoint data to the one you hold.
This is my best solution after 2 days of headache. Hope someone can find a better one and share it here.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With