Error when I geocode an address (rare) in Swift - swift

I have a SearchBar that updates a certain Binding string that is the geocoded into a list of potential location matches. Sometimes as I type in a location, I get the following:
libswiftCore.dylib`_swift_runtime_on_report:
-> 0x1054f7180 <+0>: pushq %rbp //Error on this line says "= Thread 1: Fatal Error: Duplicate keys
of type 'GeocodedPlacemark' were found in a Dictionary. This usually means either that the type
violates Hashable's requirements, or that members of the dictionary were mutated after
insertion."
0x1054f7181 <+1>: movq %rsp, %rbp
0x1054f7184 <+4>: popq %rbp
0x1054f7185 <+5>: retq
0x1054f7186 <+6>: nopw %cs:(%rax,%rax)
Only problem is, it gives me no clue as to where the source of the error is...is there some clue in all those numbers or the 'pushq' keyword found on that line that could direct me to the dictionary it is referring to?
Side Note: This error happens maybe once in every 15 or so searches, so it's quite rare.
Search bar code is as follows:
import SwiftUI
import Mapbox
import MapboxGeocoder
struct SearchBar: View {
var annotation: AnnotationsVM
#State var searchText: String = ""
//#State var typing: Bool = false
#State private var showCancelButton: Bool = false
#ObservedObject var locationManager = LocationManager()
#ObservedObject var VModel : ViewModel
#Binding var searchedText: String
#Binding var showResults: Bool
#Binding var showMoreDetails: Bool
var mapStyle: URL
var body: some View {
let binding = Binding<String>(get: {
self.searchText
}, set: {
self.searchText = $0
self.searchedText = self.searchText
self.VModel.findResults(address: self.searchedText)
if self.VModel.searchResults.count >= 0 {
self.showResults = true
self.showMoreDetails = false
} else {
self.showResults = false
}
}
)
return VStack {
// Search view
HStack {
Image(systemName: "magnifyingglass")
TextField("search", text: binding, onEditingChanged: { isEditing in
self.showCancelButton = true
self.showMoreDetails = false
}, onCommit: {
if self.VModel.searchResults.first != nil {
self.annotation.addNextAnnotation(address: self.rowText(result: self.VModel.searchResults.first!).label)
self.searchedText = "\(self.rowText(result: self.VModel.searchResults.first!).label)"
}
self.showMoreDetails = false
self.showResults = false
})
Button(action: {
self.searchText = ""
self.showResults = false
}) {
Image(systemName: "xmark.circle.fill").opacity(searchText == "" ? 0.0 : 1.0)
}
}
.padding(EdgeInsets(top: 8, leading: 6, bottom: 8, trailing: 6))
}
if showCancelButton {
Button("Cancel") {
UIApplication.shared.endEditing(true) // this must be placed before the other commands here
self.searchText = ""
self.showResults = false
self.showCancelButton = false
}
}
.padding(.horizontal)
}
}
private func rowText(result: GeocodedPlacemark) -> (view: Text, label: String) {
// city is not nil
// state is not nil
// country is not nil
if result.postalAddress != nil && result.postalAddress?.city != "" && result.postalAddress?.state != "" && result.postalAddress?.country != "" {
return (Text("\(result.formattedName), \(result.postalAddress!.city), \(result.postalAddress!.state), \(result.postalAddress!.country)"), "\(result.formattedName), \(result.postalAddress!.city), \(result.postalAddress!.state), \(result.postalAddress!.country)")
}
// city is not nil
// state is not nil
// country is nil
else if result.postalAddress != nil && result.postalAddress?.city != "" && result.postalAddress?.state != "" && result.postalAddress?.country == "" {
return (Text("\(result.formattedName), \(result.postalAddress!.city), \(result.postalAddress!.state)"), "\(result.formattedName), \(result.postalAddress!.city), \(result.postalAddress!.state)")
}
// city is not nil
// state is nil
// country is nil
else if result.postalAddress != nil && result.postalAddress?.city != "" && result.postalAddress?.state == "" && result.postalAddress?.country == "" {
return (Text("\(result.formattedName), \(result.postalAddress!.city)"), "\(result.formattedName), \(result.postalAddress!.city)")
}
//More if statements to cover all the different states, this section essentially just returns the way to format the different search results in the search results view (that results view works fine btw)
}
extension UIApplication {
func endEditing(_ force: Bool) {
self.windows
.filter{$0.isKeyWindow}
.first?
.endEditing(force)
}
}
struct ResignKeyboardOnDragGesture: ViewModifier {
var gesture = DragGesture().onChanged{_ in
UIApplication.shared.endEditing(true)
}
func body(content: Content) -> some View {
content.gesture(gesture)
}
}
extension View {
func resignKeyboardOnDragGesture() -> some View {
return modifier(ResignKeyboardOnDragGesture())
}
}
The VModel class is as follows:
class ViewModel: ObservableObject {
#ObservedObject var locationManager = LocationManager()
#Published var lat: Double?
#Published var lon: Double?
#Published var location: CLLocationCoordinate2D?
#Published var name: CLPlacemark?
#Published var searchResults: [GeocodedPlacemark] = []
var userLatitude: CLLocationDegrees {
return (locationManager.lastLocation?.latitude ?? 0)
}
var userLongitude: CLLocationDegrees {
return (locationManager.lastLocation?.longitude ?? 0)
}
func getLocation(from address: String, completion: #escaping (_ location: CLLocationCoordinate2D?)-> Void) {
//let geocoder = CLGeocoder()
let geocoder = Geocoder(accessToken: "pk.eyJ1Ijoibmlja2JyaW5zbWFkZSIsImEiOiJjazh4Y2dzcW4wbnJyM2ZtY2V1d20yOW4wIn0.LY1H3cf7Uz4BhAUz6JmMww")
let foptions = ForwardGeocodeOptions(query: address)
print("hit this point")
foptions.focalLocation = CLLocation(latitude: userLatitude, longitude: userLongitude)
geocoder.geocode(foptions) { (placemarks, attribution ,error) in
guard let placemarks = placemarks,
let location = placemarks.first?.location?.coordinate
else {
completion(nil)
return
}
completion(location)
}
}
func fetchCoords(address: String, completion: #escaping (Double, Double) -> Void){
self.getLocation(from: address) { coordinates in
print(coordinates ?? 0) // Print here
self.location = coordinates // Assign to a local variable for further processing
if let lat = coordinates?.latitude, let lon = coordinates?.longitude {
completion(lat, lon)
}
}
}
func findResults(address: String) {
let geocoder = Geocoder(accessToken: "pk.eyJ1Ijoibmlja2JyaW5zbWFkZSIsImEiOiJjazh4Y2dzcW4wbnJyM2ZtY2V1d20yOW4wIn0.LY1H3cf7Uz4BhAUz6JmMww")
let foptions = ForwardGeocodeOptions(query: address)
foptions.focalLocation = CLLocation(latitude: userLatitude, longitude: userLongitude)
foptions.maximumResultCount = 10
geocoder.geocode(foptions) { (placemarks, attribution ,error) in
guard let placemarks = placemarks
else {
return
}
self.searchResults = []
for placemark in placemarks {
self.searchResults.append(placemark)
}
}
}
}
After setting a Swift Error Breakpoint, it stopped the search in this function (I guess this is a backend MapBox function, because I certainly didn't write it; maybe it comes with the framework?):
fileprivate func dataTaskWithURL(_ url: URL, completionHandler: #escaping (_ data: Data?) -> Void, errorHandler: #escaping (_ error: NSError) -> Void) -> URLSessionDataTask {
var request = URLRequest(url: url)
request.setValue(userAgent, forHTTPHeaderField: "User-Agent")
return URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data else {
DispatchQueue.main.async {
if let e = error as NSError? {
errorHandler(e)
} else {
let unexpectedError = NSError(domain: MBGeocoderErrorDomain, code: -1024, userInfo: [NSLocalizedDescriptionKey : "unexpected error", NSDebugDescriptionErrorKey : "this error happens when data task return nil data and nil error, which typically is not possible"])
errorHandler(unexpectedError)
}
}
return
}
let decoder = JSONDecoder()
do {
// Handle multiple batch geocoding queries, THE ERROR IS ON THE LINE BELOW and says 'Thread 19: breakpoint 1.1'
let result = try decoder.decode([GeocodeAPIResult].self, from: data)
// Check if any of the batch geocoding queries failed
if let failedResult = result.first(where: { $0.message != nil }) {
let apiError = Geocoder.descriptiveError(["message": failedResult.message!], response: response, underlyingError: error as NSError?)
DispatchQueue.main.async {
errorHandler(apiError)
}
return
}
DispatchQueue.main.async {
completionHandler(data)
}
} catch {
// Handle single & single batch geocoding queries
do {
let result = try decoder.decode(GeocodeAPIResult.self, from: data)
// Check if geocoding query failed
if let message = result.message {
let apiError = Geocoder.descriptiveError(["message": message], response: response, underlyingError: error as NSError?)
DispatchQueue.main.async {
errorHandler(apiError)
}
return
}
DispatchQueue.main.async {
completionHandler(data)
}
} catch {
// Handle errors that don't return a message (such as a server/network error)
DispatchQueue.main.async {
errorHandler(error as NSError)
}
}
}
}
}

I'm going to recommend first to update the iOS Mapbox and MapBox Geocoder SDKs to the latest versions - sometimes these updates fix outstanding bugs in the frameworks.
Next, I'd recommend to wrap the error-causing geocode lines in synchronous DispatchQueue blocks, like this:
func getLocation(from address: String, completion: #escaping (_ location: CLLocationCoordinate2D?)-> Void) {
//let geocoder = CLGeocoder()
let geocoder = Geocoder(accessToken: "pk.eyJ1Ijoibmlja2JyaW5zbWFkZSIsImEiOiJjazh4Y2dzcW4wbnJyM2ZtY2V1d20yOW4wIn0.LY1H3cf7Uz4BhAUz6JmMww")
let foptions = ForwardGeocodeOptions(query: address)
print("hit this point")
foptions.focalLocation = CLLocation(latitude: userLatitude, longitude: userLongitude)
DispatchQueue.global().sync {
geocoder.geocode(foptions) { (placemarks, attribution ,error) in
guard let placemarks = placemarks,
let location = placemarks.first?.location?.coordinate
else {
completion(nil)
return
}
completion(location)
}
}
}
func findResults(address: String) {
let geocoder = Geocoder(accessToken: "pk.eyJ1Ijoibmlja2JyaW5zbWFkZSIsImEiOiJjazh4Y2dzcW4wbnJyM2ZtY2V1d20yOW4wIn0.LY1H3cf7Uz4BhAUz6JmMww")
let foptions = ForwardGeocodeOptions(query: address)
foptions.focalLocation = CLLocation(latitude: userLatitude, longitude: userLongitude)
foptions.maximumResultCount = 10
DispatchQueue.global().sync {
geocoder.geocode(foptions) { (placemarks, attribution ,error) in
guard let placemarks = placemarks else {
return
}
self.searchResults = []
for placemark in placemarks {
self.searchResults.append(placemark)
}
}
}
}
If this doesn't fix the issue, then I'd recommend viewing the various threads in the stack frame in Xcode when the Swift Error breakpoint is raised - you can do this on the left hand panel by tapping on the different thread names. See this:
How to select a thread:
Once you can see the individual lines of code for each thread (tapping each thread on the left, shows this for each of your files), you can add the same DispatchQueue.global().sync { } blocks around the conflicting access lines in your code for every single relevant thread. I outline how to select where to place these blocks now.
If you see in the image, for each thread, the call stack is listed from bottom to top. You only need to add the DispatchQueue.global().sync { } block around one line where the data variable is being accessed. But, if you are accessing the data variable inside a completion block (like your geocode functions) then the DispatchQueue.global().sync { } needs to go around the whole function.
How to select an error line in Thread 1:
Hope this helps! :)

Related

I'm getting a purple warning when trying to save via #AppStorage inside an asynchronous call

In the code below I ask the server for the popuplation rate for the city the user is current in via HTTP request.
Everything works as expected except that I'm getting a purple warning when I save lastSearchedCity and lastSearchedPopulationRate to UserDefaults inside the http synchronous function call via #AppStorage. Again, I get the right info from the server and everything seem to be saving to UserDefaults, the only issue is the purple warning.
Purple Warning:
Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
I tried wraping self.lastSearchedCity = city and self.lastSearchedPopulationRate = pRate inside DispatchQueue.main.async {} but I'm afraid this is more then that since the compiler suggest using the receive(on:) operator but I'm not sure how to implement it.
if let pRate = populationRate{
self.lastSearchedCity = city // purple warning points to this line
self.lastSearchedPopulationRate = pRate // purple warning points to this line
}
What would be the right way to solve this warning?
Core Location
class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
private let locationManager = CLLocationManager()
#AppStorage("kLastSearchedCity")private var lastSearchedCity = ""
#AppStorage("kLastSearchedPopulationRate")private var lastSearchedPopulationRate = ""
#Published var locationStatus: CLAuthorizationStatus?
var hasFoundOnePlacemark:Bool = false
let httpRequestor = HttpPopulationRateRequestor()
override init() {
super.init()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
}
var statusString: String {
guard let status = locationStatus else {
return "unknown"
}
switch status {
case .notDetermined: return "notDetermined"
case .authorizedWhenInUse: return "authorizedWhenInUse"
case .authorizedAlways: return "authorizedAlways"
case .restricted: return "restricted"
case .denied: return "denied"
default: return "unknown"
}
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
locationStatus = status
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
hasFoundOnePlacemark = false
CLGeocoder().reverseGeocodeLocation(manager.location!, completionHandler: {(placemarks, error)-> Void in
if error != nil {
self.locationManager.stopUpdatingLocation()
if placemarks!.count > 0 {
if !self.hasFoundOnePlacemark{
self.hasFoundOnePlacemark = true
let placemark = placemarks![0]
let city:String = placemark.locality ?? ""
let zipCode:String = placemark.postalCode ?? ""
// make request
if city != self.lastSearchedCity{
// asynchronous function call
self.httpRequestor.populationRateForCurrentLocation(zipCode: zipCode) { (populationRate) in
if let pRate = populationRate{
self.lastSearchedCity = city // purple warning points to this line
self.lastSearchedPopulationRate = pRate // purple warning points to this line
}
}
}
}
self.locationManager.stopUpdatingLocation()
}else{
print("No placemarks found.")
}
})
}
}
SwiftUI - For reference only
struct ContentView: View {
#StateObject var locationManager = LocationManager()
#AppStorage("kLastSearchedCity")private var lastSearchedCity = ""
#AppStorage("kLastSearchedPopulationRate")private var lastSearchedPopulationRate = ""
var body: some View {
VStack {
Text("Location Status:")
.font(.callout)
Text("Location Status: \(locationManager.statusString)")
.padding(.bottom)
Text("Population Rate:")
.font(.callout)
HStack {
Text("\(lastSearchedCity)")
.font(.title2)
Text(" \(lastSearchedPopulationRate)")
.font(.title2)
}
}
}
}
HTTP Request class
class HttpPopulationRateRequestor{
let customKeyValue = "ryHGehesdorut$=jfdfjd"
let customKeyName = "some-key"
func populationRateForCurrentLocation(zipCode: String, completion:#escaping(_ populationRate:String?) -> () ){
print("HTTP Request: Asking server for population rate for current location...")
let siteLink = "http://example.com/some-folder/" + zipCode
let url = URL(string: siteLink)
var request = URLRequest(url: url!)
request.setValue(customKeyValue, forHTTPHeaderField: customKeyName)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard error == nil else {
print("ERROR: \(error!)")
completion(nil)
return
}
guard let data = data else {
print("Data is empty")
completion(nil)
return
}
let json = try! JSONSerialization.jsonObject(with: data, options: [])
guard let jsonArray = json as? [[String: String]] else {
return
}
if jsonArray.isEmpty{
print("Array is empty...")
return
}else{
let rate = jsonArray[0]["EstimatedRate"]!
let rateAsDouble = Double(rate)! * 100
completion(String(rateAsDouble))
}
}
task.resume()
}
}
CLGeocoder().reverseGeocodeLocation is an async method and so is self.httpRequestor.populationRateForCurrentLocation. Neither of those 2 are guaranteed to execute their completion closures on the main thread.
You are updating properties from your closure which are triggering UI updates, so these must be happening from the main thread.
You can either manually dispatch the completion closure to the main thread or simply call DispatchQueue.main.async inside the completion handler when you are accessing #MainActor types/properties.
receive(on:) is a Combine method defined on Publisher, but you aren't using Combine, so you can't use that.
Wrapping the property updates in DispatchQueue.main.async is the correct way to solve this issue.
self.httpRequestor.populationRateForCurrentLocation(zipCode: zipCode) { (populationRate) in
if let pRate = populationRate {
DispatchQueue.main.async {
self.lastSearchedCity = city
self.lastSearchedPopulationRate = pRate
}
}
}

Firestore pagination using MVVM architecture swift

I don't quite understand what I am doing wrong since I am very new to MVVM. It worked in MVC architecture. I've setup my VM and am able to get the first set of results and even then that's not working properly. I get 4 results instead of 10 which is what LOADLIMIT is set as. I was able to get it to work in an MVC architecture without any issues. The VM function which triggers the query is called multiple (3) times instead of just once i.e. even prior to scrolling.
Here is my VM:
enum FetchRestaurant {
case success
case error
case location
case end
}
class ListViewModel {
let restaurant: [Restaurant]?
let db = Firestore.firestore()
var restaurantArray = [Restaurant]()
var lastDocument: DocumentSnapshot?
var currentLocation: CLLocation?
typealias fetchRestaurantCallback = (_ restaurants: [Restaurant]?, _ message: String?, _ status: FetchRestaurant) -> Void
var restaurantFetched: fetchRestaurantCallback?
var fetchRestaurant: FetchRestaurant?
init(restaurant: [Restaurant]) {
self.restaurant = restaurant
}
func fetchRestaurantCallback (callback: #escaping fetchRestaurantCallback) {
self.restaurantFetched = callback
}
func fetchRestaurants(address: String) {
print("address received: \(address)")
getLocation(from: address) { location in
if let location = location {
self.currentLocation = location
self.queryGenerator(at: location)
} else {
self.restaurantFetched?(nil, nil, .location)
}
}
}
func queryGenerator(at location: CLLocation) {
var query: Query!
if restaurantArray.isEmpty {
query = db.collection("Restaurant_Data").whereField("distributionType", isLessThanOrEqualTo: 2).limit(to: Constants.Mealplan.LOADLIMIT)
} else {
print("last document:\(String(describing: lastDocument?.documentID))")
query = db.collection("Restaurant_Data").whereField("distributionType", isLessThanOrEqualTo: 2).start(afterDocument: lastDocument!).limit(to: Constants.Mealplan.LOADLIMIT)
}
batchFetch(query: query)
}
func batchFetch(query: Query) {
query.getDocuments { (querySnapshot, error) in
if let error = error {
self.restaurantFetched?(nil, error.localizedDescription, .error)
} else if querySnapshot!.isEmpty {
self.restaurantFetched?(nil, nil, .end)
} else if !querySnapshot!.isEmpty {
let queriedRestaurants = querySnapshot?.documents.compactMap { querySnapshot -> Restaurant? in
return try? querySnapshot.data(as: Restaurant.self)
}
guard let restaurants = queriedRestaurants,
let currentLocation = self.currentLocation else {
self.restaurantFetched?(nil, nil, .end)
return }
self.restaurantArray.append(contentsOf: self.applicableRestaurants(allQueriedRestaurants: restaurants, location: currentLocation))
DispatchQueue.main.asyncAfter(deadline: .now(), execute: {
self.restaurantFetched?(self.restaurantArray, nil, .success)
})
self.lastDocument = querySnapshot!.documents.last
}
}
}
func getLocation(from address: String, completionHandler: #escaping (_ location: CLLocation?) -> Void) {
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(address) { (placemarks, error) in
guard let placemarks = placemarks,
let location = placemarks.first?.location else {
completionHandler(nil)
return
}
completionHandler(location)
}
}
}
And in the VC viewDidLoad:
var fetchMore = false
var reachedEnd = false
let leadingScreensForBatching: CGFloat = 5.0
var searchController = UISearchController(searchResultsController: nil)
var currentAddress : String?
var listViewModel = ListViewModel(restaurant: [Restaurant]())
override func viewDidLoad() {
super.viewDidLoad()
listViewModel.fetchRestaurantCallback { (restaurants, error, result) in
switch result {
case .success :
self.loadingShimmer.stopShimmering()
self.loadingShimmer.removeFromSuperview()
guard let fetchedRestaurants = restaurants else { return }
self.restaurantArray.append(contentsOf: fetchedRestaurants)
self.tableView.reloadData()
self.fetchMore = false
case .location :
self.showAlert(alertTitle: "No businesses nearby", message: "Try going back and changing the address")
case .error :
guard let error = error else { return }
self.showAlert(alertTitle: "Error", message: error)
case .end :
self.fetchMore = false
self.reachedEnd = true
}
}
if let currentAddress = currentAddress {
listViewModel.fetchRestaurants(address: currentAddress)
}
}
I would really appreciate links or resources for implementing MVVM in Swift for a Firestore back-end. I'm coming up short on searches here and on Google. Even tried medium.
EDIT
class ListViewController: UITableViewController {
lazy var loadingShimmer: UIImageView = {
let image = UIImage(named: "shimmer_background")
let imageview = UIImageView(image: image)
imageview.contentMode = .top
imageview.translatesAutoresizingMaskIntoConstraints = false
return imageview
}()
var restaurantArray = [Restaurant]()
var planDictionary = [String: Any]()
var fetchMore = false
var reachedEnd = false
let leadingScreensForBatching: CGFloat = 5.0
var searchController = UISearchController(searchResultsController: nil)
var currentAddress : String?
var listViewModel = ListViewModel(restaurant: [Restaurant]())
override func viewDidLoad() {
super.viewDidLoad()
setupTable()
}
override func viewWillAppear(_ animated: Bool) {
clearsSelectionOnViewWillAppear = false
}
func setupTable() {
navigationItem.backBarButtonItem = UIBarButtonItem(title: "Restaurant", style: .plain, target: nil, action: nil)
tableView.register(RestaurantCell.self, forCellReuseIdentifier: "Cell")
tableView.delegate = self
tableView.dataSource = self
let navigationBarHeight: CGFloat = self.navigationController!.navigationBar.frame.height
tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: -navigationBarHeight, right: 0)
tableView.separatorStyle = .none
tableView.showsVerticalScrollIndicator = false
tableView.addSubview(loadingShimmer)
loadingShimmer.topAnchor.constraint(equalTo: tableView.safeAreaLayoutGuide.topAnchor).isActive = true
loadingShimmer.leadingAnchor.constraint(equalTo: tableView.leadingAnchor).isActive = true
loadingShimmer.trailingAnchor.constraint(equalTo: tableView.trailingAnchor).isActive = true
loadingShimmer.startShimmering()
initialSetup()
}
func initialSetup() {
let addressOne = planDictionary["addressOne"] as! String + ", "
let city = planDictionary["city"] as! String + ", "
let postalCode = planDictionary["postalCode"] as! String
currentAddress = addressOne + city + postalCode
setupSearch()
listViewModel.fetchRestaurantCallback { (restaurants, error, result) in
switch result {
case .success :
self.loadingShimmer.stopShimmering()
self.loadingShimmer.removeFromSuperview()
guard let fetchedRestaurants = restaurants else { return }
self.restaurantArray.append(contentsOf: fetchedRestaurants)
self.tableView.reloadData()
self.fetchMore = false
case .location :
self.showAlert(alertTitle: "No businesses nearby", message: "Try going back and changing the address")
case .error :
guard let error = error else { return }
self.showAlert(alertTitle: "Error", message: error)
case .end :
self.fetchMore = false
self.reachedEnd = true
}
}
if let currentAddress = currentAddress {
listViewModel.fetchRestaurants(address: currentAddress)
}
}
override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let off = scrollView.contentOffset.y
let off1 = scrollView.contentSize.height
if off > off1 - scrollView.frame.height * leadingScreensForBatching {
print("\(fetchMore), \(reachedEnd)")
if !fetchMore && !reachedEnd {
if let address = self.currentAddress {
print("address sent: \(address)")
listViewModel.fetchRestaurants(address: address)
}
}
}
}
}
That you're only getting back 4 results instead of 10 is not due to a faulty query or get-document request—those are coded properly. You're either losing documents when you parse them (some are failing Restaurant initialization), Constants.Mealplan.LOADLIMIT is wrong, or there aren't more than 4 documents in the collection itself that satisfy the query.
That the query is executed 3 times instead of once is also not due to anything in this code—viewDidLoad is only called once and geocodeAddressString only returns once. You're making a fetch request elsewhere that we can't see.
In the batchFetch method, you have a guard that returns out of the function without ever calling its completion handler. This will leave the UI in a state of limbo. I'd recommend always calling the completion handler no matter why the function returns.
You never manage the document cursor. If the get-document return has less documents than the load limit, then nil the last-document cursor. This way, when you attempt to get the next page of documents, guard against a nil cursor and see if there is even more to fetch.
There's no need to pass in an empty array and have your function fill it; simply construct and return an array of results within ListViewModel itself.
We can't see how you trigger pagination. Is it through a scroll delegate when the user reaches the bottom or through a button tap, for example? If it's through a scroll delegate, then I'd disable that for now and see how many returns you get—I suspect one, instead of 3.
What is the particular reason you've ditched MVC for MVVM here? With MVC, you can get pagination up with just a few lines of code. I think MVVM is overkill for iOS applications and would advise against using it unless you have a compelling reason.

SwiftUI - Can I use a completion handler in Button action?

I have a form that gets a new user's name and zip code. When the user presses Save, I use Core Location to take the zip code and find the associated city and state. At that point I want a completion handler to save the form data along with the city and state.
But for some reason, the completion part isn't kicking in. I'm still trying to figure out completion handlers but I thought I got pretty close... (obviously I need to deal with error handling and the code could be more concise.)
Button(action: {
self.getCityStateFromPostalCode(zip: self.zip, completion: {
//This isn't getting called
let newCustomer = Customer(context: self.moc)
newCustomer.custName = self.name
newCustomer.custZip = self.zip
newCustomer.custState = self.state
newCustomer.custCity = self.city
self.appDelegate.saveContext()
})
}) {
Text("Save")
}
func getCityStateFromPostalCode(zip: String, completion: #escaping () -> ()) {
let geocoder = CLGeocoder()
var city = ""
var state = ""
geocoder.geocodeAddressString(zip) { (placemarks, error) in
if let placemark = placemarks?[0] {
if placemark.postalCode == zip {
city = placemark.locality!
state = placemark.administrativeArea!
self.city = city
self.state = state
}
}
}
}
In your function you're not calling the completion parameter:
func getCityStateFromPostalCode(zip: String, completion: #escaping () -> ()) {
let geocoder = CLGeocoder()
var city = ""
var state = ""
geocoder.geocodeAddressString(zip) { (placemarks, error) in
if let placemark = placemarks?[0] {
if placemark.postalCode == zip {
city = placemark.locality!
state = placemark.administrativeArea!
self.city = city
self.state = state
}
}
completion() // <- add this (may be moved depending on the `error` parameter
}
}

swift 3 Calculate distance to current location and sort result from closet to furthest

I'm trying to to calculate the distance from an event to my current location, sort the results and populate that in a tableview. I keep getting error for optional unwrapped value distance is nil.
private func observeEvents() {
refHandle = ref.observe(.childAdded, with: { (snapshot) -> Void in
let eventDetails = snapshot.value as! Dictionary<String, AnyObject>
let eventID = snapshot.key
let location = eventDetails["location"] as! String!
//calculating distance
self.forwardGeocoding(address: location!)
let distance = self.eventLocation?.distance(from: self.currentLocation!) as Double!
//end calculating
let dateTime = eventDetails["dateTime"] as! String!
let addedByUser = eventDetails["addedByUser"] as! String!
let attending = eventDetails["attendance"] as! String!
if let name = eventDetails["eventName"] as! String! , name.characters.count > 0
{
self.events.append(Events(id:eventID, name: name, location: location!, dateTime: dateTime!, addedByUser: addedByUser!, attending: attending! , distance: distance!))
self.events.sort(by: { $0.distance < $1.distance})
self.tableView.reloadData()
} else {
print("Error ! Can't load events from database")
}
})
} //load events data to uitableview
I created a function to return a CLLocation from an address
func forwardGeocoding(address: String) {
CLGeocoder().geocodeAddressString(address, completionHandler: { (placemarks, error) in
if error != nil {
print(error!)
return
}
if (placemarks?.count)! > 0 {
let placemark = placemarks?[0]
self.eventLocation = placemark?.location
}
})
}
I finally figured out the answer. The issue was the function for distance is called asynchronously there for the result would always be nil. I created a completion handler for the forwardGeocoding function to return latitude and longitude from the address string and call the result inside the nested firebase listener. Here is the code, I hope if someone ran into something similar problem to me will find it helpful.
//Get lat and long
func getCoordinates(address: String, completionHandler: #escaping (_ lat: CLLocationDegrees?, _ long: CLLocationDegrees?, _ error: Error?) -> ()) -> Void {
var _:CLLocationDegrees
var _:CLLocationDegrees
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(address) { (placemarks: [CLPlacemark]!, error: Error!) in
if error != nil {
print("Geocode failed with error: \(error.localizedDescription)")
} else if placemarks.count > 0 {
let placemark = placemarks[0] as CLPlacemark
let location = placemark.location
let lat = location?.coordinate.latitude
let long = location?.coordinate.longitude
completionHandler(lat, long, nil)
}
}
}
Nested call in firebase listener
refHandle = ref.observe(.childAdded, with: { (snapshot) -> Void in
let location = event["address"] as! String
self.getCoordinates(address: location!) { lat, long, error in
if error != nil {
print("Error")
} else {
self.latitude = lat
self.longitude = long
let distance = CLLocation(latitude: self.latitude!,longitude: self.longitude!).distance(from: self.currentLocation!)
if let name = eventDetails["eventName"] as! String! , name.characters.count > 0
{
self.events.append(Events(id:eventID, name: name, location: location!, dateTime: dateTime!, addedByUser: addedByUser!, attending: attending!, distance: distance))
self.events.sort(by: { $0.distance < $1.distance})
self.tableView.reloadData()
} else {
print("Error ! Can't load events from database")
}
}
}
})

Alamofire memory leaks Instruments

I am trying to clean my app from memory leaks and I have a few problems understanding this
Why Alamofire function Request.serializeResponseJSON is called 30 seconds after I've launched app: I did not touch anything or navigate anywhere, the screen was static.
Why does it leaks?
Why does my code leaks?
I get same leaks when the screen have loaded.
What I've tried so far:
Autoreleasepool;
Appending to and initialising arrays in every way that possible;
Changing all variable (class, func) to be optional/not
optional/weak;
Initialising classes in UIViewController;
Initialising classes in main thread;
Searching these problems in the internet.
I've found out, using Xcode memory tool, that it is somehow connected with _ContiguousArrayStorage, but I do not understand how and what is it actually.
I am out of any ideas what is wrong here. Any tips would be much appreciated.
Here is all related code:
My general API request
public func requestWithLocation(_ httpmethod: Alamofire.HTTPMethod, URL: String, parameters: [String: AnyObject]?, completionHandler: #escaping CompletionHandler) -> (){
var header: HTTPHeaders = [:]
var location: [String: Double] = [:]
let locationManager = CLLocationManager()
if (CLLocationManager.authorizationStatus() == .authorizedWhenInUse
|| CLLocationManager.authorizationStatus() == .authorizedAlways) && locationManager.location != nil {
location = [
"lon" : locationManager.location!.coordinate.longitude,
"lat" : locationManager.location!.coordinate.latitude
]
}
if User.sharedInstance.token != "" {
header["Authorization"] = User.sharedInstance.token
}
var parametersWithLocation = parameters ?? [:]
parametersWithLocation["location"] = location as AnyObject
Alamofire.request("\(serverAddress)/\(URL)", method: httpmethod, parameters: parametersWithLocation, encoding: JSONEncoding.default, headers: header).validate().responseJSON { response in
var data: JSON?
if response.result.value != nil {
data = JSON(response.result.value!)
}
if User.sharedInstance.token == "" {
User.sharedInstance.token = response.response?.allHeaderFields["Authorization"] as! String
} else {
if let header = response.response?.allHeaderFields["Authorization"] as? String {
if User.sharedInstance.token != header {
User.sharedInstance.token = header
}
}
}
completionHandler(data, response.result.error as NSError?)
}
}
My screen request
class func requestMainScreen(handler: #escaping ([ShortRestaurant], [ShortRestaurant], [ShortRestaurant]) -> ()) {
var dataForBestChoise: [ShortRestaurant] = []
var dataForTop: [ShortRestaurant] = []
var dataForNearest: [ShortRestaurant] = []
let group = DispatchGroup()
group.enter()
APIModel.sharedInstance.requestWithLocation(.post, URL: "restaurants/near", parameters: nil, completionHandler: {(data, error) in
guard let `data` = data else {
group.leave()
return
}
for JSON in data["restaurants"].arrayValue {
dataForNearest.append(ShortRestaurant.initFromJSON(JSON)) //here is leak
}
group.leave()
})
group.enter()
APIModel.sharedInstance.requestWithLocation(.post, URL: "restaurants/top", parameters: nil, completionHandler: {(data, error) in
guard let `data` = data else {
group.leave()
return
}
for JSON in data["restaurants"].arrayValue {
dataForTop.append(ShortRestaurant.initFromJSON(JSON))//here is leak
}
group.leave()
})
group.enter()
APIModel.sharedInstance.requestWithLocation(.post, URL: "restaurants/personal", parameters: nil, completionHandler: {(data, error) in
guard let `data` = data else {
group.leave()
return
}
for JSON in data["restaurants"].arrayValue {
dataForBestChoise.append(ShortRestaurant.initFromJSON(JSON)) //here is leak
}
group.leave()
})
group.notify(queue: DispatchQueue.main) {
handler(dataForBestChoise, dataForTop, dataForNearest)
}
}
My classes (I know this kind of initialisation is kinda wrong, but I'd changed to init(data: JSON) - did not help:
class func initFromJSON(_ data: JSON) -> ShortRestaurant {
let restaurant = ShortRestaurant()
restaurant.id = data["id"].stringValue
restaurant.name = data["name"].stringValue
restaurant.image = data["img"].stringValue
restaurant.description = data["shortDesc"].stringValue
restaurant.nameOfMetrostatin = data["address"]["metro"]["name"].stringValue
restaurant.mapType = data["mapType"].stringValue
restaurant.address = data["address"]["street"].stringValue
restaurant.longitude = data["address"]["location"][0].doubleValue
restaurant.latitude = data["address"]["location"][1].doubleValue
restaurant.phone = data["phone"].stringValue
restaurant.workTime = data["currentWork"].stringValue
restaurant.avarageBill = data["price"].stringValue
restaurant.peopleInfo = data["croud"].stringValue
restaurant.rating = data["rating"].stringValue
restaurant.ratingTrend = data["trend"].stringValue
restaurant.distance = data["distance"].doubleValue
restaurant.isFavourited = data["isFavourited"].bool ?? false
restaurant.specialOfferDescription = data["discounts"]["name"].string
restaurant.specialOfferName = data["discounts"]["type"].string
restaurant.alertText = data["label"]["name"].string
restaurant.alertIcon = data["label"]["type"].string
restaurant.alertBackground = data["label"]["color"].string
restaurant.avaliableDates = ReservationSchedule.initArrayFrom(data: data["availableDates"])
restaurant.avaliableTimes = data["scheduleRes"].arrayObject as? [String] ?? []
restaurant.doesHaveDiscount = data["discounts"]["id"].string != nil
restaurant.doesHaveEvent = data["events"]["id"].string != nil
restaurant.weeklyTop = data["weeklyTop"].bool ?? false
restaurant.monthlyTop = data["monthlyTop"].bool ?? false
restaurant.yearTop = data["yearTop"].bool ?? false
restaurant.isActive = data["isActive"].bool ?? true
return restaurant
}
Array of these leaks:
class ReservationSchedule {
var description: String
var data: String
var dayTitle: String
var fullTitle: String
init(data: JSON) {
self.data = data["value"].stringValue
self.dayTitle = data["day"].stringValue
self.description = data["label"].stringValue
self.fullTitle = data["title"].stringValue
}
class func initArrayFrom(data: JSON) -> [ReservationSchedule] {
var schedule: [ReservationSchedule] = []
for day in data.arrayValue {
schedule.append(ReservationSchedule.init(data: day)) //here is leak
}
return schedule
}
}
Have you tried setting URLCache time to request, This will remove the the cache of request and free's the memory