Getting document ID when decoding firestore document using Swift Codable - ios

I'm using a class to decode retrieved firestore documents, and it works as expected if I don't want to manipulate the data:
class Room: Identifiable, Codable {
#DocumentID public var id:String?
var name:String
}
However if I try to use my own init to set values, I can't get the firestore document ID?
class Room: Identifiable, Codable {
#DocumentID public var id:String?
var name:String
enum Keys:String, CodingKey {
case name
case capacity
case photo = "url"
}
required init(from decoder: Decoder) throws {
// How do I get the document ID to set the id value?
let container = try decoder.container(keyedBy: Keys.self)
name = try container.decode(String.self, forKey: .name)
capacity = try container.decode(Int.self, forKey: .capacity)
photo = try container.decode(String.self, forKey: .photo)
// Do some more stuff here...
}
}

Found the answer elsewhere, and this works perfectly. Posting for anyone else who arrives here with the same query.
TL;DR -
Use the following to decode the DocumentReference in your init function:
ref = try container.decode(DocumentID<DocumentReference>.self, forKey: .ref)
.wrappedValue
Longer explanation:
I won't pretend to understand this 100%, but there's a good explanation here https://github.com/firebase/firebase-ios-sdk/issues/7242

Related

The data couldn’t be read because it isn’t in the correct format with openweathermap api

I am developing weather app in swift with openweathermap api and parse json data with mvvm pattern.I have used this code it is displaying data could not be read because it's not in the correct format.
import UIKit
struct WeatherModel: Codable {
var main:String!
var description:String!
var icon:String!
var temp:Int?
enum CodingKeys: String, CodingKey {
case main = "main"
case description = "description"
case icon = "icon"
case temp = "temp"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
main = try values.decodeIfPresent(String.self, forKey: .main)
description = try values.decodeIfPresent(String.self, forKey: .description)
icon = try values.decodeIfPresent(String.self, forKey: .icon)
temp = try values.decodeIfPresent(Int.self, forKey: .temp)
}
}
This is model
import UIKit
import Alamofire
class WeatherViewModel{
var modelWeather = [WeatherModel]()
weak var vc: ViewController?
func getWeather() {
AF.request("http://api.openweathermap.org/data/2.5/weather?q="+(vc?.cityTextField.text)!+"&units=metric&appid=88236c7916643a06e2cd64d56f4e5077", method: .get).response{
response in
debugPrint(response)
if let data = response.data
{
do{
let apiResponse = try JSONDecoder().decode(WeatherModel.self, from: data)
self.modelWeather.append(apiResponse)
debugPrint(apiResponse)
DispatchQueue.main.async {
self.vc?.reloadInputViews()
}
}catch let error{
print(error.localizedDescription)
}
}
}
}
}
{"coord":{"lon":67.0822,"lat":24.9056},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"base":"stations","main":{"temp":28,"feels_like":27.67,"temp_min":28,"temp_max":28,"pressure":1014,"humidity":44},"visibility":6000,"wind":{"speed":2.57,"deg":50},"clouds":{"all":0},"dt":1616474499,"sys":{"type":1,"id":7576,"country":"PK","sunrise":1616463159,"sunset":1616507028},"timezone":18000,"id":1174872,"name":"Karachi","cod":200}
This is response but error in apiresponse veriable.
A good first step is to properly model Main. I checked out the dummy data that was used in the comments and your main property is missing the nested dictionaries inside the main dictionary. You can map this by simply creating a struct that maps each dictionary key/value pair.
struct Main: Codable {
let temp: Double
let feels_like: Double
let temp_min: Double
// Continue to add the rest of the dictionaries here.
}
Once this is done, add this struct to your property and make it of type Main, like so:
var main: Main
Also you don't need to add CodingKeys unless your properties are going to be named differently than the property names that are coming back from the sever.
EDIT
If you wanna retrieve
var description:String!
var icon:String!
You'll need to make a Weather struct as well.
Hopefully this helps and you can figure out the rest of the mapping for your WeatherModel object.

Convert quoted number from JSON (swift)

I have a struct like bellow:
struct MyStruct: Codable {
var id: Int?
}
and the JSON that i was receiving from server was like this:
{
"id": 12345
}
But now server-side decided to send all numbers as quoted numbers like this:
{
"id": "12345"
}
When decoding this json using JSONDecoder().decode i got an error,
The data couldn’t be read because it isn’t in the correct format
Is there any way, (except writing custom Encodable and Decodable implementation for every struct that i created up to now) to solve this problem? For example doing something on JSONDecoder()
You can do it by implementing Decodable protocol's required initializer, init(from:):
extension MyStruct {
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let idString = try values.decode(String.self, forKey: .id)
id = Int(idString)
}
}
And don't forget to decode values of other properties.

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
}
}

Best approach to create Non-optional Codable with `default values` in swift

I know the basic concept of class and struct but which is more effective to create models for API to fetch data and tell me with pros and cons.
Previously i don't use optional for models. Instead i give it some value. ie
class CompanyInfo : Codable {
var NameEn : String = ""
var CityEn : String = ""
var Website : String = ""
var Email : String = ""
var Phone : String = ""
var Fax : String = ""
}
but when it get some null value from API. ie "Fax": null then App get crashed because it can't parse data with following line
let data = try JSONDecoder().decode(dataModel.self, from: dataSet)
what is the best way to deffine a model so i don't need to unwrap optional or give it default value.
You can implement a custom decoder with default values:
class CompanyInfo : Codable {
var NameEn: String
var CityEn: String
var Website: String
var Email: String
var Phone: String
var Fax: String
required init(from decoder: Decoder) throws {
do {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.NameEn = try container.decodeIfPresent(String.self, forKey: .NameEn) ?? "Default"
self.CityEn = try container.decodeIfPresent(String.self, forKey: .CityEn) ?? "Default"
self.Website = try container.decodeIfPresent(String.self, forKey: .Website) ?? "Default"
self.Email = try container.decodeIfPresent(String.self, forKey: .Email) ?? "Default"
self.Phone = try container.decodeIfPresent(String.self, forKey: .Phone) ?? "Default"
self.Fax = try container.decodeIfPresent(String.self, forKey: .Fax) ?? "Default"
}
}
}
Unrelated to question, but important Note:
In Swift, only Types names should start with a capital letter. If you continue naming variables like this, you will have a serious refactoring issue one day if you decide to use CoreData or working with other Swift developers.
Any future colleague will thank you if the data model reflects the JSON response of the API ("Don't make me think"). Furthermore, for now you don't want to have optional values - in 3 weeks you perhaps need them - then you have some ugly checks:
if companyInfo.fax == "default" {
// Hey it's the default value but this indicates that the value is optional and nil
}
However, it's doable:
https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types
By the way - also have reading on Swift naming conventions regarding property names.
https://swift.org/documentation/api-design-guidelines/
There is no such answer for more efficient use of class or struct. that's depends on your need, app's demand and it's coding structure.
If you have to deal with the optionals at runtime this can be the best approach according to me.
I would prefer to use struct on this
struct YOUR_MODEL_NAME : Codable {
var NameEn : String?
var CityEn : String?
var Website : String?
var Email : String?
var Phone : String?
var Fax : String?
enum CodingKeys: String, CodingKey {
case NameEn = "YOUR_KEY_FOR_NameEn"
case CityEn = "YOUR_KEY_FOR_CityEn"
case Website = "YOUR_KEY_FOR_Website"
case Email = "YOUR_KEY_FOR_Email"
case Phone = "YOUR_KEY_FOR_Phone"
case Fax = "YOUR_KEY_FOR_Fax"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
NameEn = try values.decodeIfPresent(String.self, forKey: .NameEn)
CityEn = try values.decodeIfPresent(String.self, forKey: .CityEn)
Website = try values.decodeIfPresent(String.self, forKey: .Website)
Email = try values.decodeIfPresent(String.self, forKey: .Email)
Phone = try values.decodeIfPresent(String.self, forKey: .Phone)
Fax = try values.decodeIfPresent(String.self, forKey: .Fax)
}
}

How to store Big Integer in Swift 4 Codable?

I am trying auto parsing an API with swift 4 codable. Some of the fields in the API are big integer which Codable does not support nor Swift 4.NSNumber is not supported by Codable and UInt64 is small for it to fit. I tried with a thrid party library and made my variable within the codable to that type,but that also did not work. I tried to to make a custom class or struct which will do the conversion with only one value in container but don't know how to make the container accept Big Int type or convert it to string.
My code is below like this. Is there any solution to it?
import Foundation
import BigNumber
class PersonalizationLean:Codable {
var hubId:String?
var appId:UInt8?
var nodeId:Int?
var name:String?
var ico:String?
var icoBase64:String?
var isBin:Bool?
var lastModifiedAt:Int?
var shouldShowInUi:Bool?
var applianceType:String?
var tags:[String]?
var placeId:PlaceIdCodable?
var roomId:String?
var id:String?
var key:String?
enum CodingKeys:String,CodingKey {
case hubId
case appId
case nodeId
case name
case ico
case icoBase64
case isBin
case lastModifiedAt
case shouldShowInUi
case applianceType
case tags
case placeId
case roomId
case id
case key
}
// required init(from decoder: Decoder) throws {
// do {
// let container = try decoder.container(keyedBy: CodingKeys.self)
// self.hubId = try container.decode(String.self, forKey: .hubId)
// self.appId = try container.decode(UInt8.self, forKey: .appId)
// self.nodeId = try container.decode(Int.self, forKey: .nodeId)
// self.name = try container.decode(String.self, forKey: .name)
// self.ico = try container.decode(String.self, forKey: .ico)
// self.icoBase64 = try container.decode(String.self, forKey: .icoBase64)
// self.isBin = try container.decode(Bool.self, forKey: .isBin)
// self.lastModifiedAt = try container.decode(Int.self, forKey: .lastModifiedAt)
// self.shouldShowInUi = try container.decode(Bool.self, forKey: .shouldShowInUi)
// self.applianceType = try container.decode(String.self,forKey: .applianceType)
// self.tags = try container.decode([String].self,forKey: .tags)
//
//
// }catch {
// print(error)
// }
// }
}
class PlaceIdCodable:Codable {
var placeId:String?
required init(from decoder:Decoder) throws {
do {
let container = try decoder.singleValueContainer()
let placeIdBig = try container.decode(BInt.self) //this gives error
}catch {
print(error)
}
}
}
The library I am using is BigNumber
Use built-in Decimal which derives from NSDecimalNumber. It adopts Codable
BInt do not conform to Codable OR Decodable
to use it here it should confirm to mentioned protocol
extension BInt: Decodable {
public init(from decoder: Decoder) throws {
// Initialization goes here
}
}

Resources