Currently making an app in which a view controller takes in the addressString and then geocodes the address to retrieve the latitude and longitude of said address as seen below inside of a #objc method.
#objc func addLocationTouchUpInside() {
let addressString = "\(streetAddressField.text!), \(cityField.text!), \(stateField.text!)"
var location = Location(id: Location.setid(), locationName: nil, addressString: addressString, latitude: nil, longitude: nil, HoHid: nil)
// This geocoder instance is a let property initialized with the class
geocoder.geocodeAddressString(location.addressString) { placemark, error in // Completion handler starts here...
print("WHY")
if error != nil {
print("error: \(error!)")
}
if let placemark = placemark?[0] {
if let addresslocation = placemark.location {
location.latitude = addresslocation.coordinate.latitude
location.longitude = addresslocation.coordinate.longitude
}
}
print("3: \(location.latitude)")
}
do {
try AppDatabase.shared.dbwriter.write({ db in
try location.insert(db)
})
self.currentUser?.locationid = location.id
try AppDatabase.shared.dbwriter.write({ db in
if var user = try User.fetchOne(db, key: self.currentUser?.id) {
user = self.currentUser!
try user.update(db)
}
})
} catch {
fatalError("\(error)")
}
let locationInputViewController = LocationInputViewController()
locationInputViewController.setUser(user: self.currentUser!)
locationInputViewController.setLocation(location: location)
locationInputViewController.setHoH()
if let navigationController = self.navigationController {
navigationController.pushViewController(locationInputViewController, animated: true)
}
}
The issue I am having is that any code within the completion handler (of type CLGeocodeCompletionHandler) does not seem to execute. Originally I had assumed that location simply wasnt being mutated for some reason. I then learned that nothing within the completion handler executes.
This question may seem like a duplicate to this post however it seems a clear answer was not quite found. Via some comments on said post I had set both set a custom location for the simulator and used my personal iphone and saw no difference.
EDIT: I have updated the question with the entire function as requested in the comments. Second the completion handler I have been referring to would be that of CLGeocodeCompletionHandler. Defined as
typealias CLGeocodeCompletionHandler = ([CLPlacemark]?, Error?) -> Void
where
func geocodeAddressString(_ addressString: String, completionHandler: #escaping CLGeocodeCompletionHandler)
in the documentation. I have also commented the line where the completion handler starts.
Finally in only ONE of my runs was this strange error outputted:
#NullIsland Received a latitude or longitude from getLocationForBundleID that was exactly zero
Related
I am struggling to trigger the logic responsible for changing the view at the right time. Let me explain.
I have a view model that contains a function called createNewUserVM(). This function triggers another function named requestNewUser() which sits in a struct called Webservices.
func createNewUserVM() -> String {
Webservices().requestNewUser(with: User(firstName: firstName, lastName: lastName, email: email, password: password)) { serverResponse in
guard let serverResponse = serverResponse else {
return "failure"
}
return serverResponse.response
}
}
Now that's what's happening in the Webservices' struct:
struct Webservices {
func requestNewUser(with user: User, completion: #escaping (Response?) -> String) -> String {
//code that creates the desired request based on the server's URL
//...
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else {
DispatchQueue.main.async {
serverResponse = completion(nil)
}
return
}
let decodedResponse = try? JSONDecoder().decode(Response.self, from: data)
DispatchQueue.main.async {
serverResponse = completion(decodedResponse)
}
}.resume()
return serverResponse //last line that gets executed before the if statement
}
}
So as you can see, the escaping closure (whose code is in the view model) returns serverResponse.response (which can be either "success" or "failure"), which is then stored in the variable named serverResponse. Then, requestNewUser() returns that value. Finally, the createNewUserVM() function returns the returned String, at which point this whole logic ends.
In order to move to the next view, the idea was to simply check the returned value like so:
serverResponse = self.signupViewModel.createNewUserVM()
if serverResponse == "success" {
//move to the next view
}
However, after having written a few print statements, I found out that the if statement gets triggered way too early, around the time the escaping closure returns the value, which happens before the view model returns it. I attempted to fix the problem by using some DispatchQueue logic but nothing worked. I also tried to implement a while loop like so:
while serverResponse.isEmpty {
//fetch the data
}
//at this point, serverResponse is not empty
//move to the next view
It was to account for the async nature of the code.
I also tried was to pass the EnvironmentObject that handles the logic behind what view's displayed directly to the view model, but still without success.
As matt has pointed out, you seem to have mixed up synchronous and asynchronous flows in your code. But I believe the main issue stems from the fact that you believe URLSession.shared.dataTask executes synchronously. It actually executes asynchronously. Because of this, iOS won't wait until your server response is received to execute the rest of your code.
To resolve this, you need to carefully read and convert the problematic sections into asynchronous code. Since the answer is not trivial in your case, I will try my best to help you convert your code to be properly asynchronous.
1. Lets start with the Webservices struct
When you call the dataTask method, what happens is iOS creates a URLSessionDataTask and returns it to you. You call resume() on it, and it starts executing on a different thread asynchronously.
Because it executes asynchronously, iOS doesn't wait for it to return to continue executing the rest of your code. As soon as the resume() method returns, the requestNewUser method also returns. By the time your App receives the JSON response the requestNewUser has returned long ago.
So what you need to do to pass your response back correctly, is to pass it through the "completion" function type in an asynchronous manner. We also don't need that function to return anything - it can process the response and carry on the rest of the work.
So this method signature:
func requestNewUser(with user: User, completion: #escaping (Response?) -> String) -> String {
becomes this:
func requestNewUser(with user: User, completion: #escaping (Response?) -> Void) {
And the changes to the requestNewUser looks like this:
func requestNewUser(with user: User, completion: #escaping (Response?) -> Void) {
//code that creates the desired request based on the server's URL
//...
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else {
DispatchQueue.main.async {
completion(nil)
}
return
}
let decodedResponse = try? JSONDecoder().decode(Response.self, from: data)
DispatchQueue.main.async {
completion(decodedResponse)
}
}.resume()
}
2. View Model Changes
The requestNewUser method now doesn't return anything. So we need to accommodate that change in our the rest of the code. Let's convert our createNewUserVM method from synchronous to asynchronous. We should also ask the calling code for a function that would receive the result from our Webservice class.
So your createNewUserVM changes from this:
func createNewUserVM() -> String {
Webservices().requestNewUser(with: User(firstName: firstName, lastName: lastName, email: email, password: password)) { serverResponse in
guard let serverResponse = serverResponse else {
return "failure"
}
return serverResponse.response
}
}
to this:
func createNewUserVM(_ callback: #escaping (_ response: String?) -> Void) {
Webservices().requestNewUser(with: User(firstName: firstName, lastName: lastName, email: email, password: password)) { serverResponse in
guard let serverResponse = serverResponse else {
callback("failure")
return
}
callback(serverResponse.response)
}
}
3. Moving to the next view
Now that createNewUserVM is also asynchronous, we also need to change how we call it from our controller.
So that code changes from this:
serverResponse = self.signupViewModel.createNewUserVM()
if serverResponse == "success" {
//move to the next view
}
To this:
self.signupViewModel.createNewUserVM{ [weak self] (serverResponse) in
guard let `self` = self else { return }
if serverResponse == "success" {
// move to the next view
// self.present something...
}
}
Conclusion
I hope the answer gives you an idea of why your code didn't work, and how you can convert any existing code of that sort to execute properly in an asynchronous fashion.
This can be achieve using DispatchGroup and BlockOperation together like below:
func functionWillEscapeAfter(time: DispatchTime, completion: #escaping (Bool) -> Void) {
DispatchQueue.main.asyncAfter(deadline: time) {
completion(false) // change the value to reflect changes.
}
}
func createNewUserAfterGettingResponse() {
let group = DispatchGroup()
let firstOperation = BlockOperation()
firstOperation.addExecutionBlock {
group.enter()
print("Wait until async block returns")
functionWillEscapeAfter(time: .now() + 5) { isSuccess in
print("Returned value after specified seconds...")
if isSuccess {
group.leave()
// and firstoperation will be complete
} else {
firstOperation.cancel() // means first operation is cancelled and we can check later if cancelled don't execute next operation
group.leave()
}
}
group.wait() //Waits until async closure returns something
} // first operation ends
let secondOperation = BlockOperation()
secondOperation.addExecutionBlock {
// Now before executing check if previous operation was cancelled we don't need to execute this operation.
if !firstOperation.isCancelled { // First operation was successful.
// move to next view
moveToNextView()
} else { // First operation was successful.
// do something else.
print("Don't move to next block")
}
}
// now second operation depends upon the first operation so add dependency
secondOperation.addDependency(firstOperation)
//run operation in queue
let operationQueue = OperationQueue()
operationQueue.addOperations([firstOperation, secondOperation], waitUntilFinished: false)
}
func moveToNextView() {
// move view
print("Move to next block")
}
createNewUserAfterGettingResponse() // Call this in playground to execute all above code.
Note: Read comments for understanding. I have run this in swift playground and working fine. copy past code in playground and have fun!!!
So I am trying to restructure my API calls to make use of dispatchGroups so I don't have to make collectionViews and other elements reload so quick. I know that with dispatch groups you typically have to create one, enter, and then leave a certain number of times. Upon completion of this, you typically notify the main queue to do some operation. However, in the code snippet below it notifies the main queue before I even enter the dispatch group once. Which is throwing things way off. If anyone could look at my code and tell me what's going wrong I would really appreciate that.
static func showFeaturedEvent(for currentLocation: CLLocation,completion: #escaping ([Event]) -> Void) {
//getting firebase root directory
let dispatchGroup1 = DispatchGroup()
var currentEvents:[Event]?
var geoFireRef: DatabaseReference?
var geoFire:GeoFire?
geoFireRef = Database.database().reference().child("featuredeventsbylocation")
geoFire = GeoFire(firebaseRef: geoFireRef!)
let circleQuery = geoFire?.query(at: currentLocation, withRadius: 17.0)
circleQuery?.observe(.keyEntered, with: { (key: String!, location: CLLocation!) in
print("Key '\(key)' entered the search area and is at location '\(location)'")
dispatchGroup1.enter()
print("entered dispatch group")
EventService.show(forEventKey: key, completion: { (event) in
if let newEvent = event {
currentEvents?.append(newEvent)
dispatchGroup1.leave()
print("left dispatch group")
}
})
})
dispatchGroup1.notify(queue: .main, execute: {
if let currentEventsFinal = currentEvents{
completion(currentEventsFinal)
}
})
}
I am running dispatch groups in other places. Im not sure if that would affect anything but I just felt like it was important to note in this question.
You need to enter the group before you start the asynchronous task and leave in the completion of the asynchronous task. You aren't executing the first enter until the first asynchronous task completes, so when execution hits your notify the dispatch group is empty and it fires straight away.
It is also important that you call leave the same number of times that you call enter or the group will never empty, so be wary of calling leave inside conditional statements.
static func showFeaturedEvent(for currentLocation: CLLocation,completion: #escaping ([Event]) -> Void) {
//getting firebase root directory
let dispatchGroup1 = DispatchGroup()
var currentEvents:[Event]?
var geoFireRef: DatabaseReference?
var geoFire:GeoFire?
geoFireRef = Database.database().reference().child("featuredeventsbylocation")
geoFire = GeoFire(firebaseRef: geoFireRef!)
let circleQuery = geoFire?.query(at: currentLocation, withRadius: 17.0)
disatchGroup1.enter()
circleQuery?.observe(.keyEntered, with: { (key: String!, location: CLLocation!) in
print("Key '\(key)' entered the search area and is at location '\(location)'")
dispatchGroup1.enter()
EventService.show(forEventKey: key, completion: { (event) in
if let newEvent = event {
currentEvents?.append(newEvent)
print("left dispatch group")
}
dispatchGroup1.leave()
})
dispatchGroup1.leave()
})
dispatchGroup1.notify(queue: .main, execute: {
if let currentEventsFinal = currentEvents{
completion(currentEventsFinal)
}
})
}
So I have a function called grabUserLoc which grabs the user location.
#objc func grabUserLoc(){
LocationService.getUserLocation { (location) in
guard let currentLocation = location else { return }
print("Latitude: \(currentLocation.coordinate.latitude)")
print("Longitude: \(currentLocation.coordinate.longitude)")
}
}
The function uses this service method, that contains this function.
struct LocationService {
static func getUserLocation(completion: #escaping (CLLocation?) -> Void){
print("Atttempting to get user location")
//May need location manager function
Location.getLocation(accuracy: .city, frequency:.continuous , success: { (_, location) -> (Void) in
//print("Latitide: \(location.coordinate.latitude)")
//print("Longitude: \(location.coordinate.longitude)")
return completion(location)
}) { (request, last, error) -> (Void) in
request.cancel()
print("Location monitoring failed due to an error \(error)")
return completion(nil)
}
}
}
Now I have print statements in the grabUserLoc function to make sure things are working properly which they are because the user location is being grabbed. However, the print statement as well as the completion block is being run twice so it is running the print statement two times. Did I do anything wrong here as far as my implementation?
Btw I am using thrid party location pod called swiftlocation
https://github.com/malcommac/SwiftLocation
Try to stop location update before completion as it may update location twice that usually happen when you implement didLocationUpdate delegate method of CLLocationManager
Location.getLocation(accuracy: .city, frequency:.continuous , success: { (_, location) -> (Void) in
//print("Latitide: \(location.coordinate.latitude)")
//print("Longitude: \(location.coordinate.longitude)")
//Stop location Update here after first call
return completion(location)
}) { (request, last, error) -> (Void) in
request.cancel()
print("Location monitoring failed due to an error \(error)")
return completion(nil)
}
OR surround function with once Bool
#objc func grabUserLoc(){
var once = true
LocationService.getUserLocation { (location) in
guard let currentLocation = location else {
return
}
if(once)
{
print("Latitude: \(currentLocation.coordinate.latitude)")
print("Longitude: \(currentLocation.coordinate.longitude)")
once = false;
}
}
}
You seem to be using some third-party framework to get location so I don't know exactly what your code is doing, but this line:
Location.getLocation(accuracy: .city, frequency:.continuous , success:
has a frequency parameter. Maybe that should be set to something other than .continuous to just get a single location?
If you want to use the iOS Core Location framework directly, getting a single location is just a call to requestLocation() in CLLocationManager.
I've been messing around with dispatch groups and am wondering how the placement of the notify callback of a dispatch group affects when the callback will be called. I'm reading data from my database and then fetching a picture for each item in the data that was read. When I have the notify outside of the initial database read block I notice it gets called immediately, however when the notify is inside the block it behaves the proper way. Here is my code:
override func viewDidAppear(_ animated: Bool) {
let ref = FIRDatabase.database().reference().child("users").child((FIRAuth.auth()?.currentUser?.uid)!).child("invites")
ref.observeSingleEvent(of: .value, with: { snapshot in
if let snapshots = snapshot.children.allObjects as? [FIRDataSnapshot] {
for child in snapshots {
self.dispatchGroup.enter()
let info = petInfo(petName: child.value! as! String, dbName: child.key)
print(info)
self.invitesData[info] = UIImage()
let picRef = FIRStorage.storage().reference().child("profile_images").child(info.dbName+".png")
picRef.data(withMaxSize: 1024 * 1024) { (data, error) -> Void in
if error != nil {
print(error?.localizedDescription ?? "Error getting picture")
}
// Create a UIImage, add it to the array
self.invitesData[info] = UIImage(data: data!)!
self.dispatchGroup.leave()
}
}
self.dispatchGroup.notify(queue: DispatchQueue.main, execute: {
print("YOOO")
self.invitesArray = Array(self.invitesData.keys)
print(self.invitesArray)
self.inviteTable.reloadData()
})
}
})
}
This code behaves properly when the notify is within the original database read block. However if I place this after the ref.observeSingleEvent block the notify gets called immediately.
Can somebody please explain this to me?
Yes. Asynchronous code :-)
Code execution runs all the way through to the end of the function, and then the completion handler will be called
I've been stuck with this and cannot find the culprit. I will try to include as much useful code that I can. See bottom for TL;DR version.
IDEA
User opens the app, you can find restaurants in a certain city OR set it to find the restaurants near your location (assuming you allowed the app to see your location). If you set the city, then the NSFetchedResultsController performs a fetch with a certain NSPredicate. When you tap on the button to find the restaurants near you, it changed the NSPredicate and executes the fetch again.
SET UP
1) When my UIViewController is loaded, it called a method on a subclass of NSUserDefaults to grab the user's current location.
2) This method tells a CLLocationManager object to start updating the user's location, once it has updated the location, the didUpdateLocations method gets called.
3) Since I have .desiredAccuracy set to kCLLocationAccuracyBest the didUpdateLocations method gets called often, so I set a timer to perform a method called updateRestaurantDistance which performs a fetch on the main NSManagedObjectContext and retrieves all the restaurants in the database.
4) A loop iterates through each restaurant and calls a method to set the restaurant's distance from the user.
5) Once the loop is finished, I save the context with the updated values.
ISSUE
Everything runs fine when I start the app, and once the fetch from 4) is finished iterating through each item, I can tap the button that fetches the restaurants that are near me.
However, if the iterations from 4) aren't finished, and I tap the button that finds the restaurants near me, the iterations finish, but the context is not saved (I have print statements set up) and the method that updates the restaurant distance is called infinitely blocking the main thread and making the UI unusable.
CODE
- UIViewController
override func viewDidLoad() {
super.viewDidLoad()
//configure properties
//core data
context = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
//fetch controller
fetchController.delegate = self
//default values
user = User()
self.user.getCurrentLocation()
}
//method that is called when the button to filter restaurants near user is tapped
func nearMe() {
restaurantPredicate = NSPredicate(format: "(distance < %f AND distance > %f) AND SUBQUERY(specials,$s, ($s.type LIKE %# OR $s.type LIKE %#) AND $s.day LIKE %#).#count > 0",
user.doubleForKey(UserData.MaxDistance.rawValue), 0.01, typeQuery!, "Combo", day)
fetchController.fetchRequest.sortDescriptors = [distSort,nameSort,citySort]
do {
try fetchController.performFetch()
} catch {
print("Error fetching when sorting by near me")
}
tableView.reloadData()
}
The User class that has the method to get location
func getCurrentLocation() {
//ask for access to location (if not given)
if CLLocationManager.locationServicesEnabled() {
locationManager.requestWhenInUseAuthorization()
}
//if allowed, start checking for location
if CLLocationManager.authorizationStatus() == .AuthorizedWhenInUse {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.startUpdatingLocation()
}
}
The CLLocationDelegate
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// get coordintes
if let locValue: CLLocationCoordinate2D = manager.location?.coordinate {
//check if timer has started
if timer == nil {
timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: "updateRestaurantDistance", userInfo: nil, repeats: false)
} else {
timer?.invalidate()
timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: "updateRestaurantDistance", userInfo: nil, repeats: false)
}
//stop updating since we're done
locationManager.stopUpdatingLocation()
}
}
Update location method
func updateRestaurantDistance() {
let fetch = NSFetchRequest(entityName: "Restaurant")
let context = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
do {
let results = try context.executeFetchRequest(fetch) as! [Restaurant]
for result in results {
print(results.indexOf(result))
result.getDistance()
}
} catch {
print("Error updating restaurant distances from UserClass")
}
do {
try context.save()
print("saved context")
} catch {
print("error saving context while updating restaurant distance: \(error)")
}
}
The Restaurant methods
//This method gets called infinitely and I have no idea why
func getDistance() {
print("getDistance")
guard CLLocationManager.authorizationStatus() == .AuthorizedWhenInUse else {
return
}
self.distance = self.getDistanceFromRestaurant()
}
func getDistanceFromRestaurant() -> CLLocationDistance {
let user = User()
guard CLLocationManager.authorizationStatus() == .AuthorizedWhenInUse else {
return Double(0)
}
let longitude = user.doubleForKey(UserData.Longitute.rawValue)
let latitude = user.doubleForKey(UserData.Latitude.rawValue)
guard abs(longitude) + abs(latitude) > 0 else {
return Double(0)
}
let loc = CLLocation(latitude: latitude, longitude: longitude)
let selfLoc = CLLocation(latitude: Double(self.latitude), longitude: Double(self.longitude))
return loc.distanceFromLocation(selfLoc) / 1000
}
Happy to add more if needed, thank you!
EDIT: Seems that it gets to try context.save() in updateRestaurantDistance() and starts the loop there (aka it doesn't get to the print statement
EDIT 2: TL;DR Code version:
This updates the restaurant's distance to the user, trys are in do-catch blocks but removed for simplicity
func updateRestaurantDistance() {
let fetch = NSFetchRequest(entityName: "Restaurant")
let context = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
let results = try context.executeFetchRequest(fetch) as! [Restaurant]
for result in results {
result.getDistance()
}
try context.save()
}
func getDistance() {
self.distance = 10 // method to calculate distance is called, but for simplicity set to 10
}
While updateResturantDistance is running, I cannot change the NSPredicate to anything with a distance attribute and perform a fetch for my NSFetchedResultsController i.e:
predicate = NSPredicate(format: "distance < %f", 10)
fetchController.fetchRequest.predicate = predicate
try fetchController.performFetch
doesn't work, but
predicate = NSPredicate(format: "city LIKE %#", "New York")
fetchController.fetchRequest.predicate = predicate
try fetchController.performFetch
works
Either
disable the UI control that causes the reload until everything is finished
or
keep a Bool variable around indicating if you are looping through the locations. When the control is hit, change this variable. In the loop check the variable and abort if it has changed and start from scratch without saving.
OH. MY. GOODNESS.
WOW
So let's recall getters and setters. In most programming languages that aren't like swift i.e Java, you must create them with methods i.e
private var distance = 1
getDistance() -> Int {
return self.distance
}
But in swift, this isn't necessary. Anyways, turns out my getDistance method was being called for my NSPredicate. I changed the name of the method to getRestaurantDistance and everything runs as expected. Do not be like my and name methods that would also be named the getter and setter.