Add value to variable inside closure in swift - swift

I am new to swift language and I have problem that I can't solve.
After running my app i get output of empty String: nil
So, my question is how to add value to variable inside closure?
Because when I add line inside closure print(self.latLong) I get output of coordinates, but I need that value inside of variable because I need to manipulate with that variable later in my code and I don't want to write all functionality inside that closure
This is my code:
import UIKit
import CoreLocation
import Firebase
var latLong: String!
override func viewDidLoad() {
super.viewDidLoad()
findCordiante(adress: "Cupertino, California, U.S.")
print(latLong)
}
func findCordiante(adress:String){
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(adress) {
placemarks, error in
if (placemarks != nil){
let placemark = placemarks?.first
let lat = placemark?.location?.coordinate.latitude
let lon = placemark?.location?.coordinate.longitude
self.latLong = String(describing: lat!) + "," + String(describing: lon!)
}else{
//handle no adress
self.latLong = ""
}
}
}

The problem is that geocodeAddressString runs asynchronously (i.e. it takes so long they have written it to return immediately and call its closure later, when the request is done), so viewDidLoad is printing latLong well before this property was eventually set in the geocodeAddressString completion handler closure.
The solution is to adopt asynchronous programming pattern in your own code. For example, employ your own completion handler pattern:
override func viewDidLoad() {
super.viewDidLoad()
findCordiante(adress: "Cupertino, California, U.S.") { string in
// use `string` here ...
if let string = string {
self.latLong = string
print(string)
} else {
print("not found")
}
}
// ... but not here, because the above runs asynchronously and it has not yet been set
}
func findCordiante(adress:String, completionHandler: #escaping (String?) -> Void) {
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(adress) { placemarks, error in
if let location = placemarks?.first?.location, location.horizontalAccuracy >= 0 {
completionHandler("\(location.coordinate.latitude), \(location.coordinate.longitude)")
} else {
completionHandler(nil)
}
}
}

The latLong is nil because print(latLong) is called immediately after the view is loaded, before the value of latLong is set. You can define a function to handle the latLong and call it in the closure.
import UIKit
import CoreLocation
import Firebase
var latLong: String!
override func viewDidLoad() {
super.viewDidLoad()
findCordiante(adress: "Cupertino, California, U.S.")
print(latLong)
}
func findCordiante(adress:String){
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(adress) {
placemarks, error in
if (placemarks != nil){
let placemark = placemarks?.first
let lat = placemark?.location?.coordinate.latitude
let lon = placemark?.location?.coordinate.longitude
self.latLong = String(describing: lat!) + "," + String(describing: lon!)
//Do something with latLong now
self.doSomething(withlatLong: self.latLong)
}else{
//handle no adress
self.latLong = ""
}
}
}
private func doSomething(withlatLong latLong:String) {
//Do whatever you want with latLong
print(latLong)
}

Related

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

Trying to assign a value to a global variable inside a method with Guards & IFs

I'm still learning to code so please don't shoot me for asking a question. I have tried to find an answer however I haven't found anything on stack overflow that can help, nor within a couple of books I have...
I am using Xcode 11.3. I am trying to copy an address variable from inside a reverse geolocation function to a global variable. However, because the function uses guard statements and nested if's, Xcode wants me to place 'self' infront of my syntax when I try and assign the address. This is fine in that it works, however when the method has finished the global variable is empty and it seems that the address value is only held whilst that method/procedure is executing.
Is there anyway to get the data into a 'global variable' and not to some instance running inside the method? I have added comments at the base of the code where I am trying to give my global variable the contents of the address location (street number, street name and suburb).
My function (method):
// Uses MapKit Delegate
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
//let center = getCenterLocation(for: theMapView)
let center = getCenterLocation(for: theMapView)
let geoCoder = CLGeocoder()
guard let previousLocation = self.previousLocation else { return }
guard center.distance(from: previousLocation) > 100 else { return }
self.previousLocation = center
// The next little bit is mainly error checking stuff
geoCoder.reverseGeocodeLocation(center) { [weak self] (placemarks, error) in
guard let self = self else { return }
if let _ = error {
//TODO: Show alert informing the user
self.myAlertToolbar()
return
}
guard let placemark = placemarks?.first else {
//TODO: Show an alert to the user
self.myAlertToolbar()
return
}
let streetNumber = placemark.subThoroughfare ?? ""
let streetName = placemark.thoroughfare ?? ""
let suburb = placemark.locality ?? ""
if streetName == "" {
DispatchQueue.main.async {
self.addressLabelOutlet.text = "\(suburb)"
}
} else {
DispatchQueue.main.async {
// I want to be able to assign the streetNumber streetName and suburb over to a
// global variable without using self.blahblahblah
self.addressLabelOutlet.text = "\(streetNumber) \(streetName), \(suburb)"
self.myGlobalAddressVariable = String(streetNumber) + streetName + ", " + suburb
print("Address is \(self.myGlobalAddressVariable)")
}
}
}
}

How to call CLGeocoder method synchronously

I have in ViewController code
var location = CLLocation()
DispatchQueue.global().sync {
let objLocationManager = clsLocationManager()
location = objLocationManager.findLocationByAddress(address: self.txtTo.stringValue)
}
lblToLatitude.stringValue = String(location.coordinate.latitude)
lblToLongitude.stringValue = String(location.coordinate.longitude)
calling findLocationByAddress method which is implemented in separate class clsLocationManager like this
func findLocationByAddress(address: String) -> CLLocation {
let geoCoder = CLGeocoder()
var location = CLLocation()
geoCoder.geocodeAddressString(address, completionHandler: {(places, error) in
guard error == nil else { return }
location = places![0].location!
})
return location
}
I try to ensure via DispatchQueue.global().sync that geo-coding is executed before passing coordinates to lblToLatitude and lblToLongitude labels but it doesn't work. Of course I could do geo-coding in ViewController code but I'm wondering how to keep it in separate class.
You need a completion
func findLocationByAddress(address: String,completion:#escaping((CLLocation?) -> ())) {
let geoCoder = CLGeocoder()
geoCoder.geocodeAddressString(address, completionHandler: {(places, error) in
guard error == nil else { completion(nil) ; return }
completion(places![0].location!)
})
}
to call
findLocationByAddress { (location) in
if let location = location {
lblToLatitude.stringValue = String(location.coordinate.latitude)
lblToLongitude.stringValue = String(location.coordinate.longitude)
}
}
Also no need for DispatchQueue.global().sync { as the geocoder runs asynchonously in a background thread

Get Data From Async Completion Handler

Trying to get name of a city, while having latitude and longitude.
Inside a model class Location, I'm using reverseGeocodeLocation(location: , completionHandler: ) func that comes with CLGeocoder (part of CoreLocation).
func getLocationName() {
let geoCoder = CLGeocoder()
let location = CLLocation(latitude: currentLatitude, longitude: currentLongitude)
geoCoder.reverseGeocodeLocation(location, completionHandler: { placemarks, error in
guard let addressDict = placemarks?[0].addressDictionary else {
return
}
if let city = addressDict["City"] as? String {
self.currentCity = city
print(city)
}
if let zip = addressDict["ZIP"] as? String {
print(zip)
}
if let country = addressDict["Country"] as? String {
print(country)
}
})
}
However, in ViewController, after running the getLocationName(), the location.currentCity is nil, since the completion handler is async, and wasn't finished yet.
How can I make sure that the completion handler is finished running so I can access location.currentCity ?
Pass a closure as a function parameter in your getLocationName which
you can call inside the reverseGeocodeLocation closure.
func updateLocation(currentCity : String) -> Void
{
print(currentCity)
}
func getLocationName(callback : #escaping (String) -> Void)
{
let geoCoder = CLGeocoder()
let location = CLLocation(latitude: currentLatitude, longitude: currentLongitude)
geoCoder.reverseGeocodeLocation(location, completionHandler: { placemarks, error in
guard let addressDict = placemarks?[0].addressDictionary else {
return
}
if let city = addressDict["City"] as? String
{
self.currentCity = city
callback(city)
print(city)
}
if let zip = addressDict["ZIP"] as? String {
print(zip)
}
if let country = addressDict["Country"] as? String {
print(country)
}
})
}
In your ViewController...
getLocationName(callback: updateLocation)
I would create a function where location.currentCity is used, and call this function from the completion handler
So if your code looks like:
func foo() {
var location
getLocationName()
print(location.currentcity) // nil
}
change it to:
func foo() {
var location
getLocationName()
}
func bar() {
print(location.currentcity) // someplace
}
and call bar() from your completion handler

How To Call a func within a Closure

In a model's class Location, I get the name of the current city:
var currentLatitude: Double!
var currentLongitude: Double!
var currentLocation: String!
var currentCity: String!
func getLocationName() {
let geoCoder = CLGeocoder()
let location = CLLocation(latitude: currentLatitude, longitude: currentLongitude)
geoCoder.reverseGeocodeLocation(location, completionHandler: { placemarks, error in
guard let addressDict = placemarks?[0].addressDictionary else {
return
}
if let city = addressDict["City"] as? String {
self.currentCity = city
print(city)
}
if let zip = addressDict["ZIP"] as? String {
print(zip)
}
if let country = addressDict["Country"] as? String {
print(country)
}
self.nowUpdateUI()
})
}
In view controller I want to update the UI and update my label to show the current city.
However, self.currentCity = city happens inside of a closure. So if I just run a func in view controller:
func updateUI() {
cityLbl.text = Location.sharedInstance.currentCity
}
I'm not getting anywhere because the closure haven't finished running.
I've been advised to add a completion handler to getLocationName() and inside of it, perform the call to a func that will update the UI.
However, from all the tutorials out there on closures, completion handlers, it is not clear to me how to achieve that.
How to construct a completion handler, pass it as an arg to getLocationName() and how to call getLocationName from view controller?
To handle this situation you have multiple option.
Create delegate/protocol with your Location class
Create one protocol and implement that protocol method with your ViewController and declare its instance in your Location class. After then in the completionHandler of reverseGeocodeLocation call this delegate method. Check Apple documentation on Protocol for more details.
You can create completionHandler with your getLocationName method of Location class.
Add completionHandler with getLocationName and called that completionHandler inside the completionHandler of reverseGeocodeLocation like this way.
func getLocationName(completionHandler: #escaping (_ success: Bool) -> Void) {
let geoCoder = CLGeocoder()
let location = CLLocation(latitude: currentLatitude, longitude: currentLongitude)
geoCoder.reverseGeocodeLocation(location, completionHandler: { placemarks, error in
guard let addressDict = placemarks?[0].addressDictionary else {
completionHandler(false)
return
}
if let city = addressDict["City"] as? String {
self.currentCity = city
print(city)
}
if let zip = addressDict["ZIP"] as? String {
print(zip)
}
if let country = addressDict["Country"] as? String {
print(country)
}
completionHandler(true)
//self.nowUpdateUI()
})
}
Now in ViewController where you are calling this function call your updateUI method inside the completion block.
Location.sharedInstance.getLocationName { (success) in
if success {//If successfully got response
self.updateUI()
}
}
You can add observer for (NS)NotificationCenter.
Register the observer with (NS)NotificationCenter and then post the notification inside the completionHandler of reverseGeocodeLocation. You can get more detail on this with this StackOverflow Post.
// I thing issue back ground thread you need to update your UI in main thread
var currentLatitude: Double!
var currentLongitude: Double!
var currentLocation: String!
var currentCity: String!
func getLocationName() {
let geoCoder = CLGeocoder()
let location = CLLocation(latitude: currentLatitude, longitude: currentLongitude)
geoCoder.reverseGeocodeLocation(location, completionHandler: { placemarks, error in
guard let addressDict = placemarks?[0].addressDictionary else {
return
}
if let city = addressDict["City"] as? String {
self.currentCity = city
print(city)
}
if let zip = addressDict["ZIP"] as? String {
print(zip)
}
if let country = addressDict["Country"] as? String {
print(country)
}
DispatchQueue.main.async {
self.nowUpdateUI()
// Update your UI in main thread
}
})
}
This entire piece of your code:
completionHandler: { placemarks, error in
guard let addressDict = placemarks?[0].addressDictionary else {
return
}
if let city = addressDict["City"] as? String {
self.currentCity = city
print(city)
}
if let zip = addressDict["ZIP"] as? String {
print(zip)
}
if let country = addressDict["Country"] as? String {
print(country)
}
self.nowUpdateUI()
}
)
is already happening in the completionHandler (which happens after everything is finished) Just also run your updateUI() inside the completionHandler. So your end code would be :
completionHandler: { placemarks, error in
guard let addressDict = placemarks?[0].addressDictionary else {
return
}
if let city = addressDict["City"] as? String {
self.currentCity = city
DispatchQueue.main.async {
updateUI()
}
}
if let zip = addressDict["ZIP"] as? String {
print(zip)
}
if let country = addressDict["Country"] as? String {
print(country)
}
self.nowUpdateUI()
}
)
The reason you have to use DispatchQueue.main is because your completionHandler is on a backgroundqueue but you MUST always do you UI related stuff from your mainQueue—so users get the fastest changing in their UI without any glitches. Imagine if you were doing on a background thread and it was happening slow