Get geo location from city name - ios

I wrote code to display City Names when typing in a Text field. I can display the search results with a For Each loop. I need to click on one of the results and get the geo location of the cities name. Now I am struggling with getting geo location (latitude, longitude) from that city names. Is there a solution for that to implement it into my code?
class LocationService: NSObject, ObservableObject {
enum LocationStatus: Equatable {
case idle
case noResults
case isSearching
case error(String)
case result
}
#Published var queryFragment: String = ""
#Published private(set) var status: LocationStatus = .idle
#Published private(set) var searchResults: [MKLocalSearchCompletion] = []
private var queryCancellable: AnyCancellable?
private let searchCompleter: MKLocalSearchCompleter!
init(searchCompleter: MKLocalSearchCompleter = MKLocalSearchCompleter()) {
self.searchCompleter = searchCompleter
super.init()
self.searchCompleter.delegate = self
queryCancellable = $queryFragment
.receive(on: DispatchQueue.main)
// we're debouncing the search, because the search completer is rate limited.
// feel free to play with the proper value here
.debounce(for: .milliseconds(250), scheduler: RunLoop.main, options: nil)
.sink(receiveValue: { fragment in
self.status = .isSearching
if !fragment.isEmpty {
self.searchCompleter.queryFragment = fragment
} else {
self.status = .idle
self.searchResults = []
}
})
}
extension LocationService: MKLocalSearchCompleterDelegate {
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
// out a lot of places and only shows cities and countries.
self.searchResults = completer.results.filter({ $0.subtitle == "" })
self.status = completer.results.isEmpty ? .noResults : .result
}
func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
self.status = .error(error.localizedDescription)
}
}

The CoreLocation framework gives you CLGeocoder which can try to translate between coordinates and place names in both directions.
You can try this in a Playground to see how it works:
import Foundation
import CoreLocation
func getCoordinate( addressString : String,
completionHandler: #escaping(CLLocationCoordinate2D, NSError?) -> Void ) {
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(addressString) { (placemarks, error) in
if error == nil {
if let placemark = placemarks?[0] {
let location = placemark.location!
completionHandler(location.coordinate, nil)
return
}
}
completionHandler(kCLLocationCoordinate2DInvalid, error as NSError?)
}
}
let valid = "New Orleans, Louisiana, USA"
let invalid = "The Moon"
getCoordinate(addressString: valid) { location, error in
guard error == nil else {print("Error:"); return}
print(location)
}
getCoordinate(addressString: invalid) { location, error in
guard error == nil else {print("Error:"); return}
print(location)
}
The results are returned asynchronously, so you'll need to provide a completion handler.

Related

How would I convert a city name to CLLocation

I am using WeatherKit and am trying to convert a city name to a CLLocation so I can pass it to the weatherService but I am not sure how to do this. I know there is a geocodeAddressString but I wasn't able to get it to work and I don't think it would return a CLLocation. Along with that I am also having trouble with getting location data from multiple locations. I am confused on what type of array I would make meaning would I make an array of Weather objects of an array of just city names and then convert them to coordinates when I need to get the weather data. My code that I have so far is below. Any help with these would be great.
WeatherKitTestApp
'''
import SwiftUI
import WeatherKit
#main
struct WeatherKitTestApp: App {
let weatherService = WeatherService.shared
#StateObject private var locationManager = LocationManager()
var body: some Scene {
WindowGroup {
ContentView(weatherService: weatherService, locationManager: locationManager)
}
}
}
'''
ContentView
'''
import SwiftUI
import WeatherKit
struct ContentView: View {
let weatherService: WeatherService
#ObservedObject var locationManager: LocationManager
#State private var weather: Weather?
#State var searchLocation: String = ""
var body: some View {
NavigationView {
if locationManager.weatherLocations.isEmpty {
Text("Add a location")
} else {
TabView {
ForEach(locationManager.weatherLocations, id: \.self) { location in
VStack {
HStack {
TextField("Weather Location", text: $searchLocation)
.padding(.horizontal)
.frame(width: 200)
.background {
RoundedRectangle(cornerRadius: 5)
.foregroundColor(Color(UIColor.lightGray))
}
Button(action: {
locationManager.weatherLocations.append(searchLocation)
searchLocation = ""
print(locationManager.weatherLocations)
}) {
Image(systemName: "plus")
}
}
.padding(.horizontal)
if let weather {
VStack {
Text("\(location)")
Text("\(weather.currentWeather.date.formatted())")
Text(weather.currentWeather.temperature.formatted())
}
}
}
.task(id: locationManager.currentLocation) {
do {
if let location = locationManager.currentLocation {
self.weather = try await weatherService.weather(for: location)
}
} catch {
print(error)
}
}
}
}
.tabViewStyle(.page)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(weatherService: WeatherService(), locationManager: LocationManager())
}
}
'''
LocationManager
'''
import Foundation
import CoreLocation
class LocationManager: NSObject, ObservableObject {
#Published var currentLocation: CLLocation? {
didSet {
if let currentLocation {
resolveLocationName(with: currentLocation) { locationName in
self.weatherLocations.append(locationName!)
}
}
}
}
#Published var city: String = ""
private let locationManager = CLLocationManager()
#Published var weatherLocations: [String] = []
override init() {
super.init()
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.distanceFilter = kCLDistanceFilterNone
locationManager.requestAlwaysAuthorization()
locationManager.startUpdatingLocation()
locationManager.delegate = self
}
public func resolveLocationName(with location: CLLocation, completion: #escaping ((String?) -> Void)) {
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location) { (placemarks, error) in
guard let place = placemarks?.first, error == nil else {
completion(nil)
return
}
var name = ""
if let locality = place.locality {
name += locality
}
if let adminRegion = place.administrativeArea {
name += ", \(adminRegion)"
}
completion(name)
}
}
func getCoordinate(addressString: String, completionHandler: #escaping(CLLocationCoordinate2D, NSError?) -> Void) {
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(addressString) { (placemarks, error) in
if error == nil {
if let placemark = placemarks?[0] {
let location = placemark.location!
completionHandler(location.coordinate, nil)
return
}
}
completionHandler(kCLLocationCoordinate2DInvalid, error as NSError?)
}
}
// Another attempt at trying to convert city name to CLLocation
/*
private func getLocation() {
CLGeocoder().geocodeAddressString(city, completionHandler: {(placemarks, error) in
if error != nil {
print("Error: ", error!)
}
if let placemark = placemarks?.first {
let location = placemark.location!.coordinate
return
}
})
}
*/
func appendLocation(location: String) {
weatherLocations.append(location)
}
}
extension LocationManager: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last, currentLocation == nil else { return }
DispatchQueue.main.async {
self.currentLocation = location
}
manager.stopUpdatingLocation()
}
}
'''
to convert an address, eg a city name (Tokyo) to a CLLocation try this example code, works for me:
import SwiftUI
import CoreLocation
struct ContentView: View {
#State var location: CLLocation?
var body: some View {
Text("Tokyo \(location?.coordinate.latitude ?? 0) , \(location?.coordinate.longitude ?? 0)")
.onAppear {
getLocation("Tokyo") { loc in
location = loc
}
}
}
func getLocation(_ adres: String, completion: #escaping (CLLocation?) -> Void) {
CLGeocoder().geocodeAddressString(adres) { placemarks, error in
if error != nil {
print("error: \(error as Optional)")
} else {
if let placemark = placemarks?.first,
let coord = placemark.location?.coordinate {
return completion(CLLocation(latitude: coord.latitude, longitude: coord.longitude))
}
}
return completion(nil)
}
}

How to wait for another value to be assigned to run .onAppear?

I am learning to develop apps for iOS from scratch. And I chose SwiftUI to make an app that gets the location of the user, get with geocode the city where he is and with that information obtained, show a list of items that belong to that city from a API.
So, I learned on one hand how to get the location and on the other hand how to display a list. My problem now is that when you run .onAppear(perform: loadData) to display my list, the "city" result is still empty. Evidently the value of city is obtained after I try to display the list of the city.
Both the algorithm I have to get the location and the one I have to show the list work separately.
So my code is:
import SwiftUI
struct Response: Codable {
var cinemas: [Cinema]
}
struct Cinema: Codable {
var _id: String
var cinemaName: String
var cinemaCategory: String
}
struct HomeScreenView: View {
#State var results = [Cinema]()
#ObservedObject var lm = LocationManager()
var latitude: String {
return("\(lm.location?.latitude ?? 0)") }
var longitude: String { return("\(lm.location?.longitude ?? 0)") }
var placemark: String { return("\(lm.placemark?.description ?? "XXX")") }
var status: String { return("\(lm.status)") }
var city: String {
return("\(lm.placemark?.locality ?? "empty")")
}
var body: some View {
VStack {
List(results, id: \._id) { item in
VStack(alignment: .leading) {
Text(item.cinemaName)
.font(.headline)
Text(item.cinemaCategory)
}
}.onAppear(perform: loadData)
}
}
func loadData() {
guard let url = URL(string: "https://mycinemasapi.com/cinemasbycity/\(self.city)") else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let decodedResponse = try? JSONDecoder().decode(Response.self, from: data) {
// we have good data – go back to the main thread
DispatchQueue.main.async {
// update our UI
self.results = decodedResponse.cinemas
}
// everything is good, so we can exit
return
}
}
// if we're still here it means there was a problem
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
}.resume()
}
}
UPDATE:
LocationManager class
import Foundation
import CoreLocation
import Combine
class LocationManager: NSObject, ObservableObject {
private let locationManager = CLLocationManager()
private let geocoder = CLGeocoder()
let objectWillChange = PassthroughSubject<Void, Never>()
#Published var status: CLAuthorizationStatus? {
willSet { objectWillChange.send() }
}
#Published var location: CLLocation? {
willSet { objectWillChange.send() }
}
override init() {
super.init()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
}
#Published var placemark: CLPlacemark? {
willSet { objectWillChange.send() }
}
private func geocode() {
guard let location = self.location else { return }
geocoder.reverseGeocodeLocation(location, completionHandler: { (places, error) in
if error == nil {
self.placemark = places?[0]
} else {
self.placemark = nil
}
})
}
}
extension LocationManager: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
self.status = status
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
self.location = location
self.geocode()
}
}
As by your code just do load data on placemark received, like
List(results, id: \._id) { item in
VStack(alignment: .leading) {
Text(item.cinemaName)
.font(.headline)
Text(item.cinemaCategory)
}
}.onReceive(lm.$placemark) {
if $0 != nil {
self.loadData()
}
}

Problem getting lat and lon from LocationManager SwiftUI

I'm a beginner, have been working on a weather project, trying to get the lat and long from LocationManager in WeatherViewModel class and pass it the API so i can get the user location based data. but can't get the lattitude and longitude in the WeatherViewModel. the simulator shows a random temperature and city as globe.(SwiftUI and Xcode 11.6)
here's the LocationManager class
import Foundation
import CoreLocation
import Combine
class LocationManager: NSObject, CLLocationManagerDelegate, ObservableObject {
private let manager: CLLocationManager
#Published var lastKnownLocation: CLLocation?
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if status == .denied{
print("denied")
}
else{
print("athorized")
manager.requestLocation()
}
}
func start() {
manager.requestWhenInUseAuthorization()
manager.startUpdatingLocation()
}
init(manager: CLLocationManager = CLLocationManager()) {
self.manager = manager
super.init()
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error.localizedDescription)
}
func startUpdating() {
self.manager.delegate = self
self.manager.requestWhenInUseAuthorization()
self.manager.startUpdatingLocation()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
lastKnownLocation = locations.last
print("\((lastKnownLocation?.coordinate.latitude)!)")
print("\((lastKnownLocation?.coordinate.longitude)!)")
}
}
Here's the WeatherViewModel class
import SwiftUI
import Combine
import CoreLocation
class WeatherViewModel: ObservableObject {
#ObservedObject var location = LocationManager()
let client = OpenWeatherAPI()
var stateView: StateView = StateView.loading{
willSet{
objectWillChange.send() }
}
var currentWeather = CurrentWeather.emptyInit(){
willSet{
objectWillChange.send()
}
}
var todayWeather = ForecastWeather.emptyInit(){
willSet{
objectWillChange.send()
}
}
var hourlyWeathers: [ForecastWeather] = [] {
willSet{
objectWillChange.send()
}
}
var dailyWeathers: [ForecastWeather] = [] {
willSet{
objectWillChange.send()
}
}
var currentDescription = ""{
willSet{
objectWillChange.send()
}
}
private var stateCurrentWeather = StateView.loading
private var stateForecastWeather = StateView.loading
var latitude: String{
return "\(location.lastKnownLocation?.coordinate.latitude ?? 0.0)"
}
var longitude: String{
return "\(location.lastKnownLocation?.coordinate.longitude ?? 0.0)"
}
func printLatlon(){
print("WeatherViewModel")
print(latitude )
print(longitude )
}
init (){
getData()
printLatlon()
}
func retry(){
stateView = .loading
stateCurrentWeather = .loading
stateForecastWeather = .loading
}
private func getData() {
client.getCurrentWeather(at: latitude, at: longitude) { [weak self] currentWeather, error in
guard let ws = self else { return }
if let currentWeather = currentWeather {
ws.currentWeather = currentWeather
ws.todayWeather = currentWeather.getForecastWeather()
ws.currentDescription = currentWeather.description()
ws.stateCurrentWeather = .success
} else {
ws.stateCurrentWeather = .failed
}
ws.updateStateView()
}
client.getForecastWeather(at: latitude, at: longitude) { [weak self] forecastWeatherResponse, error in
guard let ws = self else { return }
if let forecastWeatherResponse = forecastWeatherResponse {
ws.hourlyWeathers = forecastWeatherResponse.list
ws.dailyWeathers = forecastWeatherResponse.dailyList
ws.stateForecastWeather = .success
} else {
ws.stateForecastWeather = .failed
}
ws.updateStateView()
}
}
private func updateStateView() {
if stateCurrentWeather == .success, stateForecastWeather == .success {
stateView = .success
}
if stateCurrentWeather == .failed, stateForecastWeather == .failed {
stateView = .failed
}
}
}
I'm passing the data in this
private func getData() {
client.getCurrentWeather(at: latitude, at: longitude) { }
client.getForecastWeather(at: latitude, at: longitude) { }
}
if i use this
var latitude: String{
return "\(location.lastKnownLocation?.coordinate.latitude ?? 0.0)"
}
var longitude: String{
return "\(location.lastKnownLocation?.coordinate.longitude ?? 0.0)"
}
Then i get this output in the simulator
the console shows this
the console shows that it didnt get the lat and lon
but if i give a string of lat and lon like this
var latitude = "36.778259"
var longitude = "-119.417931"
Then i get expected output in the simulator
console output
because I pass the string location and thats why showing my string values

Can't access model object data outside the alamofire request scope while populating api data on badoo/Chatto chat text

Previously I successfully access model objects anywhere in the class but while populating data on badoo/chatto text view I am stuck.
I am integrating chat message api into badoo chat view
Basically, the issue is alamofire response is not getting outside of the scope.
Did I try with compilation handler but no luck? Is there any way to resolve this issue?
Thanks in advance .
Here is code snippet :
import Foundation
import Chatto
import ChattoAdditions
import SwiftyJSON
import Alamofire
class DemoChatMessageFactory {
public static var chats = [ChatModel]()
class func makeMessage(_ uid:String) -> DemoTextMessageModel{
print("uid makeMessage : \(uid)")
return self.makeMessageData(uid, isIncoming:false)
}
class func makeMessageData(_ uid: String,isIncoming:Bool) -> DemoTextMessageModel {
if isIncoming == true{
return self.makeTextFinalMessage(uid, isIncoming:isIncoming)
} else {
return self.makeTextFinalMessage(uid, isIncoming: isIncoming)
}
}
public class func makeTextMessage(_ uid: String, isIncoming: Bool,text:String) -> DemoTextMessageModel {
let messageModel = self.makeMessageModel(uid, isIncoming: isIncoming,
type: TextMessageModel<MessageModel>.chatItemType)
let textMessageModel = DemoTextMessageModel(messageModel:messageModel,
text: text)
return textMessageModel
}
public class func makeTextFinalMessage(_ uid: String, isIncoming: Bool) -> DemoTextMessageModel {
var text = String()
var uidInt = Int(uid)
print("string uid 121 \(uid)")
print("print is Incomming data or not 1: \(isIncoming)")
print("uid count :\(uid.count)")
let urlString = "[My message Api]"
Alamofire.request(urlString, method: .get).validate().responseJSON {
(response) -> Void in
if let value = response.data {
do {
let json = try JSON(data: value)
if let dictionnary = json.dictionaryObject {
if let messageArray = dictionnary["message"] as?[[String: Any]] {
self.chats.removeAll()
for arr in messageArray {
self.chats.append(ChatModel(ChatListJSON: arr))
}
}
}
} catch {
print("cannot convert to Json")
}
}
print("print int 122 : \(uidInt!)")
print("Chat List Id DemoChatMessageFactory \(self.chats[uidInt!].chatId)")
print("chat message: \(String(describing: uidInt!)) th \(self.chats[uidInt!].chatMessage)")
self.textData = "\(self.chats[uidInt!].chatMessage)"
self.makeTextMessage(uid, isIncoming: isIncoming, text:self.textData) //Here I am bale to pass textData but ouside the Alamofire block can't access
}
//Here getting empty values
print("uid makeTextFinalMessage \(uid)")
print("in coming makeTextFinalMessage \(isIncoming)")
print("text makeTextFinalMessage \(text)")
//chat count also getting zero count
print("chat count final text\(chats.count)")
print("print chat count : \(self.chats.count)")
return self.makeTextMessage(uid, isIncoming: isIncoming, text:self.textData)
}
}
Test for completion handler
public var res: Any = ""
func getAllChatData(completionhandler:#escaping ([String: Any]?) -> ()){
let URL = "my api"
Alamofire.request(URL).responseJSON {
response in
if let json = response.result.value as? [String: Any] {
completionhandler(json, nil)
}
else if let error = response.result.error as Error? {
completionhandler(nil, error)
}
}
}
and call using like below inside the function
DemoChatMessageFactory.getAllChatData {
(result) in
res = result
print("response (res)")
}
please suggest me the proper way to alamofire with compilation handler
This is an example of converting all methods using the result of asynchronous call. As I have never used Chatto and you are not showing all the types in your code, so you may need to modify many parts of my code, but I believe you can see what you need to do with this code.
import Foundation
import Chatto
import ChattoAdditions
import SwiftyJSON
import Alamofire
class DemoChatMessageFactory {
public static var chats = [ChatModel]()
class func requestMessage(_ uid:String,
completion: #escaping (DemoTextMessageModel?, Error?)->Void) {
print("uid makeMessage : \(uid)")
self.requestMessageData(uid, isIncoming: false) { (model, error) in
completion(model, error)
}
}
class func requestMessageData(_ uid: String, isIncoming: Bool,
completion: #escaping (DemoTextMessageModel?, Error?)->Void) {
if isIncoming {
//...put any code needed when isIncoming is true
self.requestTextFinalMessage(uid, isIncoming: isIncoming) { model in
completion(model, error)
}
} else {
//...put any code needed when isIncoming is false
self.requestTextFinalMessage(uid, isIncoming: isIncoming) { model in
completion(model, error)
}
}
}
public class func makeTextMessage(_ uid: String, isIncoming: Bool, text: String) -> DemoTextMessageModel {
let messageModel = self.makeMessageModel(uid, isIncoming: isIncoming,
type: TextMessageModel<MessageModel>.chatItemType)
let textMessageModel = DemoTextMessageModel(messageModel:messageModel,
text: text)
return textMessageModel
}
public class func requestTextFinalMessage(_ uid: String, isIncoming: Bool,
completion: #escaping (DemoTextMessageModel?, Error?)->Void) {
var text = String()
var uidInt = Int(uid)
print("string uid 121 \(uid)")
print("print is Incomming data or not 1: \(isIncoming)")
print("uid count :\(uid.count)")
let urlString = "[My message Api]"
Alamofire.request(urlString, method: .get).validate().responseJSON {
(response) -> Void in
if let value = response.data {
do {
let json = try JSON(data: value)
if let dictionnary = json.dictionaryObject {
if let messageArray = dictionnary["message"] as?[[String: Any]] {
self.chats.removeAll()
for arr in messageArray {
self.chats.append(ChatModel(ChatListJSON: arr))
}
}
}
print("print int 122 : \(uidInt!)")
print("Chat List Id DemoChatMessageFactory \(self.chats[uidInt!].chatId)")
print("chat message: \(String(describing: uidInt!)) th \(self.chats[uidInt!].chatMessage)")
self.textData = "\(self.chats[uidInt!].chatMessage)"
completion(self.makeTextMessage(uid, isIncoming: isIncoming, text: self.textData), nil)
} catch {
print("cannot convert to Json")
completion(nil, error)
}
} else {
//better generate an error case result, and call completion.
//...
}
}
}
}
I changed some method names from make... to request... to show clarify they are asynchronous methods.
And the usage, if you intend to use your original code as:
let model = DemoChatMessageFactory.makeMessage(uid)
//Do some UI updates using `model`...
You may need to use asynchronous methods like:
DemoChatMessageFactory.requestMessage(uid) { (model, error) in
if let model = model {
//Do some UI updates using `model`...
} else {
//Do something for the error...
}
}

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()
}
}
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
)
}

Resources