BVB09
BVB09

Reputation: 875

Map an array of objects in Firestore to an a dictionary array in swift

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

Answers (1)

zysoft
zysoft

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

Related Questions