oeste
oeste

Reputation: 1539

Updating Firestore Database causes iOS crash

When I update the firebase firestore database with any new field, it instantly kills any app running that uses the data with the fatal error in the code below.

The error I get says "fatalError: "Unable to initialize type Restaurant with dictionary [(name: "test", availability: "test", category: "test")]

I'd like to be able to update it without having the apps crash. If that happens, they have to delete and reinstall the app to get it to work again, so I think it's storing the data locally somehow, but I can't find where.

What can I do to make this reset the data or reload without crashing?

The file where the error is thrown (when loading the table data):

fileprivate func observeQuery() {
    stopObserving()
    guard let query = query else { return }
    stopObserving()
    listener = query.addSnapshotListener { [unowned self] (snapshot, error) in
        guard let snapshot = snapshot else {
            print("Error fetching snapshot results: \(error!)")
            return
        }
        let models = snapshot.documents.map { (document) -> Restaurant in
            if let model = Restaurant(dictionary: document.data()) {
                return model
            } else {
                // Don't use fatalError here in a real app.
                fatalError("Unable to initialize type \(Restaurant.self) with dictionary \(document.data())")
            }
        }
        self.restaurants = models
        self.documents = snapshot.documents

        if self.documents.count > 0 {
            self.tableView.backgroundView = nil
        } else {
            self.tableView.backgroundView = self.backgroundView
        }

        self.tableView.reloadData()
    }


  }

And the Restaurant.swift file:

import Foundation 

struct Restaurant {

  var name: String
  var category: String // Could become an enum
  var availability: String // from 1-3; could also be an enum
    var description: String

  var dictionary: [String: Any] {
    return [
      "name": name,
      "category": category,
      "availability": availability,
      "description": description
    ]
  }

}

extension Restaurant: DocumentSerializable {

    //Cities is now availability
  static let cities = [
    "In Stock",
    "Back Order",
    "Out of Stock"
  ]

  static let categories = [
    "Rock", "Boulder", "Grass", "Trees", "Shrub", "Barrier"
  ]

  init?(dictionary: [String : Any]) {
    guard let name = dictionary["name"] as? String,
        let category = dictionary["category"] as? String,
        let availability = dictionary["availability"] as? String,
        let description = dictionary["description"] as? String
    else { return nil }

    self.init(name: name,
              category: category,
              availability: availability,
              description: description
    )
  }

}

The Local Collection File with the Document.Serializable code:

import FirebaseFirestore

// A type that can be initialized from a Firestore document.
protocol DocumentSerializable {
  init?(dictionary: [String: Any])
}

final class LocalCollection<T: DocumentSerializable> {

  private(set) var items: [T]
  private(set) var documents: [DocumentSnapshot] = []
  let query: Query

  private let updateHandler: ([DocumentChange]) -> ()

  private var listener: ListenerRegistration? {
    didSet {
      oldValue?.remove()
    }
  }

  var count: Int {
    return self.items.count
  }

  subscript(index: Int) -> T {
    return self.items[index]
  }

  init(query: Query, updateHandler: @escaping ([DocumentChange]) -> ()) {
    self.items = []
    self.query = query
    self.updateHandler = updateHandler
  }

  func index(of document: DocumentSnapshot) -> Int? {
    for i in 0 ..< documents.count {
      if documents[i].documentID == document.documentID {
        return i
      }
    }

    return nil
  }

  func listen() {
    guard listener == nil else { return }
    listener = query.addSnapshotListener { [unowned self] querySnapshot, error in
      guard let snapshot = querySnapshot else {
        print("Error fetching snapshot results: \(error!)")
        return
      }
      let models = snapshot.documents.map { (document) -> T in
        if let model = T(dictionary: document.data()) {
          return model
        } else {
          // handle error
          fatalError("Unable to initialize type \(T.self) with local dictionary \(document.data())")
        }
      }
      self.items = models
      self.documents = snapshot.documents
      self.updateHandler(snapshot.documentChanges)
    }
  }

  func stopListening() {
    listener = nil
  }

  deinit {
    stopListening()
  }
}

Upvotes: 0

Views: 1015

Answers (1)

AgRizzo
AgRizzo

Reputation: 5271

fatalError: "Unable to initialize type Restaurant with dictionary [(name: "test", availability: "test", category: "test")]

Seems pretty straightforward - that dictionary does not contain enough information to create a Restaurant object.

The error is from

if let model = Restaurant(dictionary: document.data()) {
    return model
} else {
    // Don't use fatalError here in a real app.
    fatalError("Unable to initialize type \(Restaurant.self) with dictionary \(document.data())")
}

because your initializer returns a nil value, from:

init?(dictionary: [String : Any]) {
 guard let name = dictionary["name"] as? String,
    let category = dictionary["category"] as? String,
    let availability = dictionary["availability"] as? String,
    let description = dictionary["description"] as? String
 else { return nil }

 self.init(name: name,
          category: category,
          availability: availability,
          description: description
 )
}

because your guard is returning nil because you do not have a description key in the dictionary.

To fix, either put a description key in the dictionary OR change your initializer to use a default description when the key is missing.


For example, here is your initializer, rewritten to use a default description, for when the description entry is missing

init?(dictionary: [String : Any]) {
 guard let name = dictionary["name"] as? String,
    let category = dictionary["category"] as? String,
    let availability = dictionary["availability"] as? String
 else { return nil }

 let description = dictionary["description"] as? String
 let defaultDescription: String = description ?? "No Description"

 self.init(name: name,
          category: category,
          availability: availability,
          description: defaultDescription
 )
}

Upvotes: 1

Related Questions