Firebase Firestore Codable is nil - ios

I have this model and I want to fetch results from Firebase Firestore.
struct Notification: Identifiable, Codable {
var id = UUID()
var type: String
var createdAt: Date
enum CodingKeys: String, CodingKey {
case id
case type
case createdAt //Firestore Timestamp
}
}
ViewModel
self.notifications = snapshot.documents.compactMap { queryDocumentSnapshot -> Notification? in
print(try? queryDocumentSnapshot.data(as: Notification.self)) //nil
return try? queryDocumentSnapshot.data(as: Notification.self)
}
The problem is that the model is nil when I try to fill an array.
I have tried it also without the createdAt (timestamp) and still it fails.
Anything wrong here?

The createdAt property of your Notification object is a Date and what's stored in Firestore is a Timestamp
struct Notification: Identifiable, Codable {
var id = UUID()
var type: String
var createdAt: Date <- needs to be a Timestamp
You can either change the property to match, or convert the Firestore Timestamp to a date
let aDate = timestamp.dateValue()
If you want to use the Date property you can include an init in the struct to handle that
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.type = try values.decode(String.self, forKey: .type)
let ts = try values.decode(TimeStamp.self, forKey: .createdAt)
self.myDate = ts.dateValue()

Related

Can I get DocumentSnapshot.data(as:) to use field default values?

I'm fetching a document from Firestore, and I'm marshalling it into a struct using the DocumentSnapshot.data(as:) function from FirebaseFirestoreSwift. I want to be able to set default values for certain fields that may or may not exist on the document (like privateProfile below).
However, when the prop doesn't exist on the document, an error is thrown from DocumentSnapshot.data(as:). Is there a way to get the below example to work or do I have to get creative (e.g. custom decoder initialiser)?
struct MyUser: Codable {
#DocumentID var id: String?
var username: String?
var privateProfile: Bool = true // This is what's causing the issue.
// var privateProfile: Bool? // Would work, but then I have to handle nil values everywhere.
}
userDataListener = db.collection(usersCollection).document(userID)
.addSnapshotListener { (querySnapshot, error) in
if let querySnapshot = querySnapshot {
do {
if let u = try querySnapshot.data(as: MyUser.self) {
self.user = u
}
} catch {
// Handle error...
}
}
}
With help from #Paulw11 we figured that the nicest way while keeping the default values in the field declarations would be writing a custom initialiser like so
struct MyUser: Codable {
#DocumentID var id: String?
var username: String?
var privateProfile: Bool = true
enum CodingKeys: String, CodingKey {
case id = "id"
case username = "username"
case privateProfile = "private_profile"
}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
self.id = try c.decodeIfPresent(String.self, forKey: .id)
self.username = try c.decodeIfPresent(String.self, forKey: .username)
self.privateProfile = try c.decodeIfPresent(Bool.self, forKey: .privateProfile) ?? self.privateProfile
}
}
It's a lot to write when there are many fields though.

Swift: How to convert a struct as dictionary along with hierarchical of keys

I want to convert a CollegeRequest type to a dictionary or data to send it to a server while requesting POST-API.
Here is my CollegeRequest type:
struct CollegeRequest: Codable {
struct Student: Codable {
var id: Int
var name: String
}
var student: Student
var date: Date
}
Once converted (along with the hierarchical keys), it should be like this:
{
"student.id": 3,
"student.name": "Raj",
"date": "2019-10-31T13:59:00+00:00"
}
How can I achieve this ?
Use Encodable protocol to encode your cutom struct type CollegeRequest. Your structure needs a manual encoding
If the structure of your Swift type differs from the structure of its
encoded form, you can provide a custom implementation of Encodable and
Decodable to define your own encoding and decoding logic. source
To encode the CollegeRequest to Data type, use JSONEncoder. An object that encodes instances of a data type as JSON objects.
Concerning the date stored property you you have to use JSONEncoder.DateEncodingStrategy.formatted(_:), the strategy that defers formatting settings to a supplied date formatter.
struct CollegeRequest: Codable {
struct Student: Codable {
var id: Int
var name: String
}
var student: Student
var date: Date
enum CodingKeys: String, CodingKey {
case studentId = "student.id"
case studentName = "student.name"
case date
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(student.id, forKey: .studentId)
try container.encode(student.name, forKey: .studentName)
try container.encode(date, forKey: .date)
}
init(student: CollegeRequest.Student, date: Date) {
self.student = student
self.date = date
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let studentId = try values.decode(Int.self, forKey: .studentId)
let studentName = try values.decode(String.self, forKey: .studentName)
student = Student(id: studentId, name: studentName)
date = try values.decode(Date.self, forKey: .date)
}
}
Usage
let request = CollegeRequest(student: CollegeRequest.Student(id: 1, name: "John"), date: Date())
let jsonEncoder = JSONEncoder()
let df = { let df = DateFormatter(); df.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"; return df }() as DateFormatter
jsonEncoder.dateEncodingStrategy = .formatted(df)
let a: Data? = try? jsonEncoder.encode(request)

Decoding JSON and map it to existing object Swift

I have the following object
struct Properties: Decodable {
var id: String?
var value: String?
var color: String?
}
In the first request to server I get the following response
{
"id":"1",
"color":"red"
}
And after another request I get
{
"id":"1", // the id of the object props is meant for
"props":{
"value":"my value" // I can get any property here
}
}
After the two requests I should have the object with all properties set
By now I decode the second request as following
struct SetAttr: Decodable {
let id: String
let props: [String : Any]
enum SetAttrCodingKeys: String, CodingKey {
case id
case props
}
init(from decoder: Decoder) throws {
let container = try! decoder.container(keyedBy: SetAttrCodingKeys.self)
props = try! container.decode([String : Any].self, forKey: .props)
id = try! container.decode(String.self, forKey: .id)
}
}
But I do not know how to parse props dictionary and set the properties on the first object. I am willing to use a decoding library, but I did not find any that can do this
EDIT:
This is how I tried to set the properties from dictionary, but the solution is not scalable
var myObject: Properties
properties = setAttr.props // [String:Any]
let keys = properties.keys
keys.forEach { key in
if let value = properties[key] {
switch key {
case "value":
myObject.value = value as? String
case "color":
myObject.color = value as? String
default:
break
}
}
}
Just use JSONSerialization which parses whatever you throw at it into arrays and dictionaries. That frees you from all the problems you have with strangely formatted JSON.
For example, the second request will be parsed as a dictionary with two keys "id" and "props", and "props" has a value which is again a dictionary with one key "value" and a value "my value".
And please stop using try! That will cause your app to crash instantly if any input is not expected. Unexpected inputs should be handled, not lead to a crash.
There are various way to do this, but one possible way could be something like this:
struct SecAttr: Decodable {
let id: String
var props: Properties?
private enum CodingKeys: String, CodingKey {
case id
case props
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
if let props = try container.decodeIfPresent(Properties.self, forKey: .props) {
self.props = props
} else {
// decode Properties from the same object
self.props = try Properties(from: decoder)
}
}
}
struct Properties: Decodable {
var value: String?
var color: String?
mutable update(from props: Properties) {
value = props.value ?? value
color = color.value ?? color
}
}
Now you can decode your original object and after getting updated properties, just update them on the original.

How do I properly decode this json string using decodable?

I have the following json string:
{"weight":[{"bmi":24.75,"date":"2020-01-20","logId":1000,"source":"API","time":"23:59:59","weight":200}]}
I want to convert it to a Swift object in order to access the different values. Here is what I am trying to do, I have these structs setup:
struct FitbitResponseModel: Decodable {
let weight: [FitbitResponseData]
}
struct FitbitResponseData: Decodable {
let bmi: Int
let date: String
let logId: Int
let source: String
let time: String
let weight: Int
}
And then I have this method to decode the json string:
func parseJSON(data: Data) -> FitbitResponseModel? {
var returnValue: FitbitResponseModel?
do {
returnValue = try JSONDecoder().decode(FitbitResponseModel.self, from: data)
} catch {
print("Error took place: \(error.localizedDescription).")
}
return returnValue
}
However when I try to run it I get the error that the data couldn’t be read because it isn’t in the correct format. What am I doing wrong? Any help is appreciated.
Thanks in advance!
change
let bmi: Int
to
let bmi: Double
beacuse it's value is coming out to be 24.75 in your response if any variable type doesn't match to JSON response whole model wouldn't map in Codable protocol (Encodable and Decodable)
Talk to your API developer. 000 is not a valid representation of a number for json. It needs to be either 0 or 0.0. You can lint your json at https://jsonlint.com . If you really need to work around this I suggest doing a string replacement on 000, with 0, before you parse the data.
Json is n't valid because logId value in your json is n't valid.
{
"weight": [{
"bmi": 24.75,
"date": "2020-01-20",
"logId": 100,
"source": "API",
"time": "23:59:59",
"weight": 200
}]
}
One really neat feature of this auto-generated conformance is that if you define an enum in your type called "CodingKeys" (or use a type alias with this name) that conforms to the CodingKey protocol – Swift will automatically use this as the key type. This therefore allows you to easily customise the keys that your properties are encoded/decoded with.
struct Base: Codable {
let weight : [Weight]?
enum CodingKeys: String, CodingKey {
case weight = "weight"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
weight = try values.decodeIfPresent([Weight].self, forKey: .weight)
}
}
struct Weight : Codable {
let bmi : Double?
let date : String?
let logId : Int?
let source : String?
let time : String?
let weight : Int?
enum CodingKeys: String, CodingKey {
case bmi = "bmi"
case date = "date"
case logId = "logId"
case source = "source"
case time = "time"
case weight = "weight"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
bmi = try values.decodeIfPresent(Double.self, forKey: .bmi)
date = try values.decodeIfPresent(String.self, forKey: .date)
logId = try values.decodeIfPresent(Int.self, forKey: .logId)
source = try values.decodeIfPresent(String.self, forKey: .source)
time = try values.decodeIfPresent(String.self, forKey: .time)
weight = try values.decodeIfPresent(Int.self, forKey: .weight)
}
}
Hope that will help!
or you can use SwiftyJSON lib: https://github.com/SwiftyJSON/SwiftyJSON

Updating a saved Codable struct to add a new property

My app uses a codable struct like so:
public struct UserProfile: Codable {
var name: String = ""
var completedGame: Bool = false
var levelScores: [LevelScore] = []
}
A JSONEncoder() has been used to save an encoded array of UserProfiles to user defaults.
In an upcoming update, I'd like to add a new property to this UserProfile struct. Is this possible in some way?
or would I need to create a new Codable struct that has the same properties plus one new property, and then copy all the values over to the new struct and then start using that new struct in place of anywhere that the UserProfile struct was previously used?
If I simply add a new property to the struct, then I won't be able to load the previous encoded array of UserProfiles as the struct will no longer have matching properties. When I get to this code for loading the saved users:
if let savedUsers = UserDefaults.standard.object(forKey: "SavedUsers") as? Data {
let decoder = JSONDecoder()
if let loadedUsers = try? decoder.decode([UserProfile].self, from: savedUsers) {
loadedUsers doesn't decode if the properties that UserProfile had when they were encoded and saved do not contain all the current properties of the UserProfile struct.
Any suggestions for updating the saved struct properties? Or must I go the long way around and re-create since I didn't already plan ahead for this property to be included previously?
Thanks for any help!
As mentioned in the comments you can make the new property optional and then decoding will work for old data.
var newProperty: Int?
Another option is to apply a default value during the decoding if the property is missing.
This can be done by implementing init(from:) in your struct
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
completedGame = try container.decode(Bool.self, forKey: .completedGame)
do {
newProperty = try container.decode(Int.self, forKey: .newProperty)
} catch {
newProperty = -1
}
levelScores = try container.decode([LevelScore].self, forKey: .levelScores)
}
This requires that you define a CodingKey enum
enum CodingKeys: String, CodingKey {
case name, completedGame, newProperty, levelScores
}
A third option if you don't want it to be optional when used in the code is to wrap it in a computed non-optional property, again a default value is used. Here _newProperty will be used in the stored json but newProperty is used in the code
private var _newProperty: Int?
var newProperty: Int {
get {
_newProperty ?? -1
}
set {
_newProperty = newValue
}
}

Resources