Reputation: 875
My data structure looks like this below with a document containing some fields and an array of "business hours":
The parent struct looks like this:
protocol RestaurantSerializable {
init?(dictionary:[String:Any], restaurantId : String)
}
struct Restaurant {
var distance: Double
var distributionType : Int
var businessHours : Array<BusinessHours>
var dictionary: [String: Any] {
return [
"distance": distance,
"distributionType": distributionType,
"businessHours": businessHours.map({$0.dictionary})
]
}
}
extension Restaurant : RestaurantSerializable {
init?(dictionary: [String : Any], restaurantId: String) {
guard let distance = dictionary["distance"] as? Double,
let distributionType = dictionary["distributionType"] as? Int,
let businessHours = dictionary["businessHours"] as? Array<BusinessHours>
else { return nil }
self.init(distance: distance, geoPoint: geoPoint, distributionType: distributionType, businessHours, restaurantId : restaurantId)
}
}
And here is the business hours struct:
protocol BusinessHoursSerializable {
init?(dictionary:[String:Any])
}
struct BusinessHours : Codable {
var selected : Bool
var thisDay : String
var startHour : Int
var closeHour : Int
var dictionary : [String : Any] {
return [
"selected" : selected,
"thisDay" : thisDay,
"startHour" : startHour,
"closeHour" : closeHour
]
}
}
extension BusinessHours : BusinessHoursSerializable {
init?(dictionary : [String : Any]) {
guard let selected = dictionary["selected"] as? Bool,
let thisDay = dictionary["thisDay"] as? String,
let startHour = dictionary["startHour"] as? Int,
let closeHour = dictionary["closeHour"] as? Int
else { return nil }
self.init(selected: selected, thisDay: thisDay, startHour: startHour, closeHour: closeHour)
}
}
I am trying to query the DB as such:
db.whereField("users", arrayContains: userId).getDocuments() { documentSnapshot, error in
if let error = error {
completion([], error.localizedDescription)
} else {
restaurantArray.append(contentsOf: (documentSnapshot?.documents.compactMap({ (restaurantDocument) -> Restaurant in
Restaurant(dictionary: restaurantDocument.data(), restaurantId: restaurantDocument.documentID)!
}))!)
}
And even though I have data, I keep getting this error on the last line above:
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value
If I put a default value then all I get is the default value. How do I get the array of objects from the flat JSON?
I tried to obtain each individual field. And then parse through the business hours field but that seems inefficient. Any idea what I am doing wrong here?
Upvotes: 0
Views: 1006
Reputation: 2358
I think the issue is at the place where you cast the businessHours
:
let businessHours = dictionary["businessHours"] as? Array<BusinessHours>
The actual data seems to be an array, but of the dictionaries, containing the hours, not the BusinessHours
obejcts. That's why guard fails, Restaurant
init returns nil
and the code fails on unwrapping.
I've found a good implementation of more general dictionary serialization in this answer, and based on that created the code example that should work for you:
/// A protocol to signify the types you need to be dictionaty codable
protocol DictionaryCodable: Codable {
}
/// The extension that actually implements the bi-directional dictionary encoding
/// via JSON serialization
extension DictionaryCodable {
/// Returns optional dictionary if the encoding succeeds
var dictionary: [String: Any]? {
guard let data = try? JSONEncoder().encode(self) else {
return nil
}
return try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any]
}
/// Creates the instance of self decoded from the given dictionary, or nil on failure
static func decode(from dictionary:[String:Any]) -> Self? {
guard let data = try? JSONSerialization.data(withJSONObject: dictionary, options: .fragmentsAllowed) else {
return nil
}
return try? JSONDecoder().decode(Self.self, from: data)
}
}
// Your structs now have no special code to serialze or deserialize,
// but only need to conform to DictionaryCodable protocol
struct BusinessHours : DictionaryCodable {
var selected : Bool
var thisDay : String
var startHour : Int
var closeHour : Int
}
struct Restaurant: DictionaryCodable {
var distance: Double
var distributionType : Int
var businessHours : [BusinessHours]
}
// This is the example of a Restaurant
let r1 = Restaurant(distance: 0.1, distributionType: 1, businessHours: [
BusinessHours(selected: false, thisDay: "Sun", startHour: 10, closeHour: 23),
BusinessHours(selected: true, thisDay: "Mon", startHour: 11, closeHour: 18),
BusinessHours(selected: true, thisDay: "Tue", startHour: 11, closeHour: 18),
])
// This is how it can be serialized
guard let dictionary = r1.dictionary else {
print("Error encoding object")
return
}
// Check the result
print(dictionary)
// This is how it can be deserialized directly to the object
guard let r2 = Restaurant.decode(from: dictionary) else {
print("Error decoding the object")
return
}
// Check the result
print(r2)
To avoid app crash on force unwraps (it's still better to show no results, than crash), I would recommend slightly changing the sequence of calls you use for data retrieval from the database:
db.whereField("users", arrayContains: userId).getDocuments() { documentSnapshot, error in
guard nil == error else {
// We can force unwrap the error here because it definitely exists
completion([], error!.localizedDescription)
return
}
// compactMap will get rid of improperly constructed Restaurant instances,
// it will not get executed if documentSnapshot is nil
// you only append non-nil Restaurant instances to the restaurantArray.
// Worst case scenario you will end up with an unchanged restaurantArray.
restaurantArray.append(contentsOf: documentSnapshot?.documents.compactMap { restaurantDocument in
Restaurant.decode(from: restaurantDocument.data())
} ?? [])
}
Upvotes: 2