0

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?

BVB09
  • 805
  • 9
  • 19
  • When you create `Restaurant` instance, what does `restaurantDocument.data()` contain? Does it contain `businessHours` field, and in which form? You force unwrap the `Restaurant` object which constructor can return `nil` if the `BusinessHours` is not a dictionary. I would recommend to avoid forced unwrapping wherever you can – this way your app won't crash. – zysoft Jun 24 '20 at 21:47
  • If I don't force unwrap it, I get an empty array. – BVB09 Jun 24 '20 at 21:51
  • The reason for that is only that your optional is `nil` and thus the provided default is used. Empty array is still much better than a fatal error from the end-user perspective :) – zysoft Jun 25 '20 at 02:22

1 Answers1

2

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())
    } ?? [])
}
zysoft
  • 2,268
  • 1
  • 16
  • 21
  • Query for one document has documentSnapshot where query for collection has querySnapshot. (It's recommended to follow default value names, because "real" documentSnapshot has no document property) – ShadeToD Feb 02 '21 at 10:49