Reputation: 281
I am trying to map my data to Model. Where I am using Firestore snapshot listener, to get data. here, I am getting data and mapping to "User" model that;
do{
let user = try User(dictionary: tempUserDic)
print("\(user.firstName)")
}
catch{
print("error occurred")
}
Here is my Model:
struct User {
let firstName: String
// var lon: Double = 0.0
// var refresh:Int = 0
// var createdOn: Timestamp = Timestamp()
}
//Testing Codable
extension User: Codable {
init(dictionary: [String: Any]) throws {
self = try JSONDecoder().decode(User.self, from: JSONSerialization.data(withJSONObject: dictionary))
}
private enum CodingKeys: String, CodingKey {
case firstName = "firstName"
}
}
Correct me if I am wrong.
Crashing because I am getting "Timestamp" in data.
Data getting from listener :
User Dictionary:
[\"firstName\": Ruchira,
\"lastInteraction\": FIRTimestamp: seconds=1576566738 nanoseconds=846000000>]"
How to map "Timestamp" to Model?
Tried "CodableFirstore" https://github.com/alickbass/CodableFirebase
Upvotes: 7
Views: 2099
Reputation: 942
While using the build in Firebase decoder is ideal, a change in the structure of the model class will render it broken. In my case, I catch the error and then correct the incoming record data so it can be decoded. Here is a method that expands upon @mig_loren answer but fixes all properties including recursing through arrays and child dictionaries.
private func fixFirTimestamps(_ jsonDict: [String: Any]) -> [String: Any] {
var fixedDict = jsonDict
// Find all fields where value is a `FIRTimestamp` and replace it with `Double`.
let indices = fixedDict.keys.indices.filter { index in
let key = fixedDict.keys[index]
return jsonDict[key] is Timestamp
}
// replace `Timestamp` with `Double`
for index in indices {
let key = fixedDict.keys[index]
guard let timestamp = fixedDict[key] as? Timestamp else { continue }
fixedDict[key] = timestamp.seconds
}
// find child json and recurse
let childJsonIndices = fixedDict.keys.indices.filter { index in
let key = fixedDict.keys[index]
return jsonDict[key] is [String: Any]
}
// recurse child json
for index in childJsonIndices {
let key = fixedDict.keys[index]
guard let childJson = fixedDict[key] as? [String: Any] else { continue }
fixedDict[key] = fixFirTimestamps(childJson)
}
// find arrays of child json and recurse
let childJsonArrayIndices = fixedDict.keys.indices.filter { index in
let key = fixedDict.keys[index]
return jsonDict[key] is [[String: Any]]
}
// recurse child json
for index in childJsonArrayIndices {
let key = fixedDict.keys[index]
guard let childJsonArray = fixedDict[key] as? [[String: Any]] else { continue }
let updatedArray = childJsonArray.map { childJson in
return fixFirTimestamps(childJson)
}
fixedDict[key] = updatedArray
}
return fixedDict
}
// call with
let fixedDict = fixFirTimestamps(query.data())
Upvotes: 0
Reputation: 588
I found that Firestore natively handles timestamp conversion via documentReference.data(as: )
; so instead of accessing the data dictionary yourself, you can pass it in such as: documentReference.data(as: User.self)
.
The existing solutions implement additional decoding logic which I believe should already be handled by Timestamp
's conformance to Codable. And they'll run into issues with nested timestamps.
Use documentReference.data(as: ) to map a document reference to a Swift type.
Upvotes: 1
Reputation: 4006
An approach is to create an extension to type Dictionary
that coverts a dictionary to any other type, but automatically modifies Date
and Timestamp
types to writeable JSON strings.
This is the code:
extension Dictionary {
func decodeTo<T>(_ type: T.Type) -> T? where T: Decodable {
var dict = self
// This block will change any Date and Timestamp type to Strings
dict.filter {
$0.value is Date || $0.value is Timestamp
}.forEach {
if $0.value is Date {
let date = $0.value as? Date ?? Date()
dict[$0.key] = date.timestampString as? Value
} else if $0.value is Timestamp {
let date = $0.value as? Timestamp ?? Timestamp()
dict[$0.key] = date.dateValue().timestampString as? Value
}
}
let jsonData = (try? JSONSerialization.data(withJSONObject: dict, options: [])) ?? nil
if let jsonData {
return (try? JSONDecoder().decode(type, from: jsonData)) ?? nil
} else {
return nil
}
}
}
The .timestampString
method is also declared in an extension for type Date
:
extension Date {
var timestampString: String {
Date.timestampFormatter.string(from: self)
}
static private var timestampFormatter: DateFormatter {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
dateFormatter.timeZone = TimeZone(identifier: "UTC")
return dateFormatter
}
}
Usage, like in the case of the question:
let user = tempUserDict.decodeTo(User.self)
Upvotes: 2
Reputation: 194
I solved this by converting the FIRTimestamp
fields to Double
(seconds) so the JSONSerialization
could parse it accordingly.
let items: [T] = documents.compactMap { query in
var data = query.data() // get a copy of the data to be modified.
// If any of the fields value is a `FIRTimestamp` we replace it for a `Double`.
if let index = (data.keys.firstIndex{ data[$0] is FIRTimestamp }) {
// Convert the field to `Timestamp`
let timestamp: Timestamp = data[data.keys[index]] as! Timestamp
// Get the seconds of it and replace it on the `copy` of `data`.
data[data.keys[index]] = timestamp.seconds
}
// This should not complain anymore.
guard let data = try? JSONSerialization.data(
withJSONObject: data,
options: .prettyPrinted
) else { return nil }
// Make sure your decoder setups the decoding strategy to `. secondsSince1970` (see timestamp.seconds documentation).
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
return try? decoder.decode(T.self, from: data)
}
// Use now your beautiful `items`
return .success(items)
Upvotes: 0