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?

3
  • 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. Commented Jun 24, 2020 at 21:47
  • If I don't force unwrap it, I get an empty array. Commented Jun 24, 2020 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 :) Commented Jun 25, 2020 at 2:22

1 Answer 1

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())
    } ?? [])
}
Sign up to request clarification or add additional context in comments.

1 Comment

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)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.