How do delegate methods work in Swift? [closed] - ios

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 8 years ago.
Improve this question
I'm trying to learn how to program location into apps, and this whole chunk of code just really confused me with the locationManager and delegates. I don't get at all what's going on when you declare a function called locationManager. You are defining this function, locationManager, right? With 2 parameters. So what exactly is calling this locationManager? When going through the Complete iOS Developer Course, he takes this locationManager snippet and copy pastes it without explaining the principles behind what you're doing when copying and pasting it. Is there some line of code that calls "locationManager(...)"? If so, where does this happen? My brain keeps thinking that if it's a function that's inherited from a superclass, CLLocationManagerDelegate, wouldn't you have to override it in order to get it to work? And could you give some intuition on how delegates work exactly?
class ViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {
#IBOutlet var myMap : MKMapView!
var manager:CLLocationManager!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
// Core Location
manager = CLLocationManager()
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.requestWhenInUseAuthorization()
manager.startUpdatingLocation()
}
func locationManager(manager:CLLocationManager, didUpdateLocations locations:[AnyObject]) {
var userLocation:CLLocation = locations[0] as CLLocation
var latitude:CLLocationDegrees = userLocation.coordinate.latitude
var longitude:CLLocationDegrees = userLocation.coordinate.longitude
var latDelta:CLLocationDegrees = 0.01
var lonDelta:CLLocationDegrees = 0.01
var span:MKCoordinateSpan = MKCoordinateSpanMake(latDelta, lonDelta)
var location:CLLocationCoordinate2D = CLLocationCoordinate2DMake(latitude, longitude)
var region:MKCoordinateRegion = MKCoordinateRegionMake(location, span)
myMap.setRegion(region, animated: true)
}
func locationManager(manager:CLLocationManager, didFailWithError error:NSError)
{
println(error)
}
Thank you very much for your help!

These delegate methods, defined in the CLLocationManagerDelegate protocol, are called by the CLLocationManager object that you instantiated and are referencing in the manager variable. So, you've instantiated the CLLocationManager object, you've asked it to inform you when there are location updates, and it does that by calling these delegate methods you've implemented.
You say:
My brain keeps thinking that if it's a function that's inherited from a superclass, CLLocationManagerDelegate, wouldn't you have to override it in order to get it to work?
The CLLocationManagerDelegate is not a class. It is a "protocol". It defines what functions the delegate object (in this case, your view controller) may/should implement. So, there's nothing to override.

Related

Plotting a Specific Location on map view with latitude and longitude

I want to plot a specific point on the map having the lat and lon from an api.
Program flow:
Get LAT & LON from api (done)
Ping api again via timer after every 5 seconds to get the latest location (done)
Plot location with retrieved LAT & LON on map
The issue is every code on the net has to do with 2 points, so user loc and destination loc. I cant seem to get it to work without user loc. I have however coded this to plot the location. However, with this when I touch the map, the map zooms out. Another issue is when I get another point the previous one also remains on the screen. (for testing purpose I hard coded the lat and lon but when I connect the api code that refreshes, prior points remain and the map code is the same as this. The lat and lon are passed via func parameters in createAnnotation().)
My code:
import UIKit
import MapKit
class ViewController: UIViewController, MKMapViewDelegate {
#IBOutlet weak var mapView: MKMapView!
override func viewDidLoad() {
super.viewDidLoad()
mapView.delegate = self // or connect in storyboard
createAnnotation()
}
func createAnnotation(){
let annotations = MKPointAnnotation()
annotations.coordinate = CLLocationCoordinate2D(latitude: 41.87369, longitude: -87.813293)
mapView.addAnnotation(annotations)
}}
How Do I plot the coordinates properly? and then delete the prior and show the new one?.
For the "previous one also remains on the screen" problem: don't keep making a new annotation and calling addAnnotation if you don't want to keep adding new annotations. Instead, keep hold of the annotation that you add, and move it later using its coordinate property. Something like this maybe:
class ViewController: UIViewController, MKMapViewDelegate {
#IBOutlet weak var mapView: MKMapView!
var annotationForThing: MKPointAnnotation?
var coordinateOfThing: CLLocationCoordinate2D? {
didSet {
guard let newCoord = coordinateOfThing else {
if let existing = annotationForThing {
mapView.removeAnnotation(existing)
}
return
}
if let existing = annotationForThing {
existing.coordinate = coordinateOfThing
}
else {
let newAnnotation = MKPointAnnotation()
newAnnotation = coordinateOfThing
mapView.addAnnotation(newAnnotation)
annotationForThing = newAnnotation
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
mapView.delegate = self // or connect in storyboard
}

How to catch GPS data of func LocationManager?

i got some problems with this whole UIViewController thing. My thoughts were, that the viewDidLoad() would be something like a main() in other languages, but especially in this case I don't see any functions called in the viewDidLoad() func.
First of all I am totally confused by the var locationManager which is actually a CLLocationManager and a func at the same time. How?
Where do I call the func locationManager? Can I return the locValue.latitude and the locValue.longitude? How do I catch them in the viewDidLoad()? Finally I want to send these two parameters to something, after I pressed a button (see: func SendButtonAction).
But my problem is, that I don't know how to bring these two guys from the body of func locationManager to an input in func SendButtonAction.
Appreciate any help :) I guess I need more basic knowledge.
import UIKit
import MapKit
import CoreLocation
class GPSNew: UIViewController, CLLocationManagerDelegate {
#IBOutlet weak var mapView: MKMapView!
#IBOutlet weak var zurueckButton: UIButton!
#IBOutlet weak var SendButton: UIButton!
var locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
// Ask for Authorisation from the User.
self.locationManager.requestAlwaysAuthorization()
// For use in foreground
self.locationManager.requestWhenInUseAuthorization()
if CLLocationManager.locationServicesEnabled() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
locationManager.startUpdatingLocation()
}
// Do any additional setup after loading the view.
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
//guard let locValue: CLLocationCoordinate2D = manager.location?.coordinate else { return }
let locValue: CLLocationCoordinate2D = manager.location!.coordinate
print("locations = \(locValue.latitude) \(locValue.longitude)")
let userLocation = locations.last
let viewRegion = MKCoordinateRegion(center: (userLocation?.coordinate)!, latitudinalMeters: 600, longitudinalMeters: 600)
self.mapView.setRegion(viewRegion, animated: true)
//return (locValue.latitude, locValue.longitude)
}
#IBAction func SendButtonAction(_ sender: Any) {
//send the user location to something
//end updating location
locationManager.stopUpdatingLocation()
}
}
Actually the method that gives the location is asynchronous so you can try
#IBAction func sendButtonAction(_ sender: Any) {
if let loc = locationManager.location?.coordinate {
print(loc.latitude, loc.longitude)
locationManager.stopUpdatingLocation()
}
}
From your question it sounds like you're used to procedural programming. In a C command-line utility, for example, you have a main() function that gets called at the launch-time. Main calls other functions to do setup, then might have a loop that steps through the work it has to do, and then either loops, waiting for input from the user, or returns if it's a "one-and-done" utility.
Apps for most (all?) GUI-based OS'es don't work that way. They are event-driven, and usually use an object-oriented design. You should read up on event-driven development. Until you study it, you're going to be very confused and won't be able to get off of square one.
Here is a short intro to the concepts, but this is a much deeper topic than we can cover in a simple post:
In an object-oriented,event-driven program, you create objects that have methods (functions) that respond to things that happen. Your program defines a set of starting objects, and then those objects wait for stuff to happen.
The method viewDidLoad() is an example of a method that gets called when something happens. It gets called when a view controller's (an object that manages a view) view gets created. It gives you a chance to do one-time setup to get ready for the user to "do stuff." Your viewDidLoad() function does that one-time setup, and then returns.
Control then returns to the system, and your app just waits to get called again.
You might also add methods that respond to the user tapping on buttons, sliding, notifications about updated GPS locations, etc.
The location manager (CLLocationManager) is an object that you create when you want to get information about the device's location. You create one, and ask it to notify you about various types of location events. You set up an object to be the location manager's "delegate". This is like giving the location manager a phone number and saying "call this number when the user's location changes."
The location manager calls its delegate when events occur that you told it you care about.

Google places API doesn't work as expected

I am following Google Places API for IOS tutorial to view the user current place.
I used the same code in the tutorial as follow:
var placesClient: GMSPlacesClient!
// Add a pair of UILabels in Interface Builder, and connect the outlets to these variables.
#IBOutlet weak var nameLabel: UILabel!
#IBOutlet weak var addressLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
placesClient = GMSPlacesClient.shared()
}
// Add a UIButton in Interface Builder, and connect the action to this function.
#IBAction func getCurrentPlace(_ sender: UIButton) {
placesClient.currentPlace(callback: { (placeLikelihoodList, error) -> Void in
if let error = error {
print("Pick Place error: \(error.localizedDescription)")
return
}
self.nameLabel.text = "No current place"
self.addressLabel.text = ""
if let placeLikelihoodList = placeLikelihoodList {
let place = placeLikelihoodList.likelihoods.first?.place
if let place = place {
self.nameLabel.text = place.name
self.addressLabel.text = place.formattedAddress?.components(separatedBy: ", ")
.joined(separator: "\n")
}
}
})
}
But I get the following error in the console:
Pick Place error: The operation couldn’t be completed. The Places API
could not find the user's location. This may be because the user has
not allowed the application to access location information.
NOTE: I have set the NSLocationWhenInUseUsageDescription key (Privacy - Location When In Use Usage Description) in info.plist file.
It's confusing because I followed the tutorial step by step. And am testing the application using physical device with "Locations Services" enabled .
Any idea what I might be doing wrong?
Or is it because the documentation is not up-to-date?
This may be because the user has not allowed the application to access location information.
This points you towards your answer. For Google Places to work you need to request to use location services by calling requestWhenInUseAuthorization(). This will prompts the user to grant permission to the app to use location services.
Please refer to the Apple Docs for more info.
EDIT
You should keep a strong reference to the CLLocationManager that you create so it does not get deallocated when your function exits.
"Create an instance of the CLLocationManager class and store a strong reference to it somewhere in your app.
Keeping a strong reference to the location manager object is required until all tasks involving that object are complete. Because most location manager tasks run asynchronously, storing your location manager in a local variable is insufficient."
Taken from the CLLocationManager Docs
EXAMPLE
class LocationViewController: UIViewController, CLLocationManagerDelegate {
let locationManager = CLLocationManager()
override func viewDidLoad()
{
super.viewDidLoad()
locationManager.delegate = self
if CLLocationManager.authorizationStatus() == .notDetermined
{
locationManager.requestWhenInUseAuthorization()
}
}
}
1. Request user for Location Usage Authorization
requestWhenInUseAuthorization() OR requestAlwaysAuthorization() according to your requirement.
2. In Info.plist, add the following keys:
a. NSLocationAlwaysUsageDescription OR NSLocationWhenInUseUsageDescription
b. NSLocationAlwaysAndWhenInUseUsageDescription
Example:
class ViewController: UIViewController, CLLocationManagerDelegate
{
let locationManager = CLLocationManager()
override func viewDidLoad()
{
super.viewDidLoad()
locationManager.requestWhenInUseAuthorization()
locationManager.delegate = self
locationManager.startUpdatingLocation()
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus)
{
}
}

How to I take my current starting location and measure the distance after/during a run, using Swift 3

Currently I have the following, its really basic but whenever I run the application it has this random initial jump, so it will start at 0 then jump to a random distance then count up normally, I've looked at a bunch of tutorial but it doesn't seem to solve the issue.
class ViewController: UIViewController, CLLocationManagerDelegate {
#IBOutlet weak var MyMap: MKMapView!
#IBOutlet weak var distanceLabel: UILabel!
let mapManager = CLLocationManager()
var startLocation: CLLocation!
override func viewDidLoad() {
super.viewDidLoad()
mapManager.delegate = self
mapManager.desiredAccuracy = kCLLocationAccuracyBest
mapManager.requestWhenInUseAuthorization()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func startButton(_ sender: Any) {
mapManager.startUpdatingLocation()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let location = locations[0]
let span: MKCoordinateSpan = MKCoordinateSpanMake( 0.01, 0.01)
let myLocation : CLLocationCoordinate2D = CLLocationCoordinate2DMake(location.coordinate.latitude, location.coordinate.longitude)
let region: MKCoordinateRegion = MKCoordinateRegionMake(myLocation, span)
MyMap.setRegion(region, animated: true)
self.MyMap.showsUserLocation = true
if (startLocation == nil) {
startLocation = locations.first
}
else {
let newLocation = location.distance(from: startLocation)
distanceLabel.text = String(newLocation)
}
}
}
The location manager does a couple of rather odd things that you have to allow for.
First, it caches the last location it recorded, and when you first ask for location updates, it may give you a "stale" location (a reading with a very old timestamp.) I haven't looked at the data you get for several OS releases, so I don't know if it still does this. If it does, the way to fix that is to check the date on the location updates you get and reject any that are more than a couple of seconds old.
Second, the first readings you get may be wildly inaccurate. It takes the GPS a while to settle down. You can deal with This problem by checking the horizontal accuracy reading. This is expressed in meters, and smaller is better. It's actually a margin of error. (The name accuracy is a bad choice of words if you ask me.) So you might decide to reject readings with a horizontal accuracy > 100 meters, or 50 meters, or something like that.
If you do those 2 things then you should avoid a huge jump at the beginning. However, you may also find that when you go into areas with poor GPS signal, your horizontal accuracy drops below your threshold and you stop getting updates. You might need to create logic that handles that case too.

Current location in not working in Google Map

I have integrated google map in swift 3, when map screen appear than current location in not showing, i have added two keys in .plist file and also set CLLocationManager delegate and requestAlwaysAuthorization
class MapViewController: UIViewController, CLLocationManagerDelegate, UITextFieldDelegate {
#IBOutlet var mapView: GMSMapView!
var marker: GMSMarker?
override func viewDidLoad() {
super.viewDidLoad()
self.title = "MapVC"
self.doSetupUI()
self.searchLocation()
}
override func viewWillAppear(_ animated: Bool) {
let locationManager : CLLocationManager = CLLocationManager()
locationManager.delegate = self
locationManager.requestAlwaysAuthorization()
}
func doGoogleMapSetup(lat : Double , lng : Double) {
let camera = GMSCameraPosition.camera(withLatitude: lat, longitude:lng, zoom:16)
let mapView = GMSMapView.map(withFrame: .zero, camera:camera)
mapView.isMyLocationEnabled = true
let marker = GMSMarker()
marker.position = CLLocationCoordinate2D(latitude: lat, longitude: lng)
marker.snippet = ""
marker.appearAnimation = kGMSMarkerAnimationPop
marker.map = mapView
let arrPoints : NSMutableArray = NSMutableArray()
arrPoints.add(UserDefaults.standard.object(forKey: "addressPoints"))
for i in 0..<arrPoints.count {
let path : String = (arrPoints.object(at: i)as! NSMutableArray).object(at: 0) as! String
let route : GMSPath = GMSPath.init(fromEncodedPath: path)!
let polyLine : GMSPolyline = GMSPolyline.init(path: route)
polyLine.strokeWidth = 2.0
polyLine.strokeColor = UIColor.red
polyLine.map = mapView
}
}
For showing current location we don't need any location manager in case of GoogleMaps. All we need is to add one of the keys or both in the .plist. So make sure the key is there. I have used NSLocationWhenInUseUsageDescription key.
<key>NSLocationWhenInUseUsageDescription</key>
<string>Allow location</string>
Also make sure that you have called GMSServices provideAPIKey method and replaced with the API_KEY you generated in google developer console. Also all the relevant Google APIs as per requirement should be enabled.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
GMSServices.provideAPIKey("YOUR_API_KEY")
return true
}
So, I am assuming you have done all the settings and things right in google developer console.
By just writing the below line in your controller where you have made the GoogleMap can show the location allow/disallow prompt and take the permission of the user.
mapView.isMyLocationEnabled = true
However this will not animate your map to your current location. But you can manually drag the map to check the current location and you will see a blue dot at your current location.
But now we also want to animate to the current location whenever we load that ViewController. Now the need for CLLocationManager arrives. So that in its didUpdateLocation delegate, we can fetch the current location and can just animate the graph to the current location.
So here is my complete controller.
import UIKit
import GoogleMaps
class ViewController: UIViewController,GMSMapViewDelegate,CLLocationManagerDelegate {
#IBOutlet weak var mapView: GMSMapView!
var locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
mapView.isMyLocationEnabled = true
mapView.delegate = self
//Location Manager code to fetch current location
self.locationManager.delegate = self
self.locationManager.startUpdatingLocation()
}
//Location Manager delegates
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let location = locations.last
let camera = GMSCameraPosition.camera(withLatitude: (location?.coordinate.latitude)!, longitude:(location?.coordinate.longitude)!, zoom:14)
mapView.animate(to: camera)
//Finally stop updating location otherwise it will come again and again in this delegate
self.locationManager.stopUpdatingLocation()
}
}
Another way of doing is not using the didUpdateLocation and not using the location manager is just by using the GMSMapViewDelegate delegate method mapViewDidFinishTileRendering
func mapViewDidFinishTileRendering(_ mapView: GMSMapView) {
let location = mapView.myLocation
let camera = GMSCameraPosition.camera(withLatitude: (location?.coordinate.latitude)!, longitude:(location?.coordinate.longitude)!, zoom:14)
mapView.animate(to: camera)
}
It will be called everytime the map rendering is finished.
But this comes with a limitation, it will always bring you to the current location whenever you drag/pinch/zoom map as the rendering finish everytime you play with map. So, you can just implement some kind of bool variable logic here.
You can get your location by using
let yourCurrentLocation = mapView.myLocation
Make sure to do this on a device rather than simulator. If you are using simulator, you have to choose some custom location and then only you will be able to see the blue dot.
I already gave this type of answer. Check this Link. But that was in Swift 2.x. The one which I posted in this answer is in Swift 3.x
This is a bit detailed, so I'd like to leave it in a full answer. This is the most common reason I have encountered for a nil location, since figuring out the basics, a few years ago. So, you call CLLocationManager.startLocating(), in your viewDidLoad. Then you call the method that sets up your map. Sometimes this works, and sometimes it doesn't, because of a race condition caused by the amount of time it takes the CLLocationManager to set up permissions, on the one hand, and access the user's location, in another part of the code. Let's look at an order of events, where it doesn't work:
1) you call requestAlwaysAuthroization and startLocating
2) User permissions setup is triggered on one thread
3) In your ViewController, you request the user's location, to set up your map
4) It comes back nil
5) NOW, step 2 finishes, and the app has access to the user's location, but it's too late
The core problem, is that the process that starts with requesting permissions and location, takes more than a few milliseconds. And if your view is already set up, it takes few milliseconds for it to go through the methods in your viewDidLoad. By the time you have the location you need, you've already requested it. This has caused me too many crashes, in my location-based apps.
My workaround, has been to craft a singleton CLLocationManager, make my starting view a delegate, and requestAlwaysAuthorization and startLocating, in that view. That way, when I get to the view that needs the location, the app has already started locating, and the locationManager.location is not nil.
This is an approach that will obviously not work for every app. If you need clarification, let me know, and if you need code, as well. I have a few public iO git repos, with projects where I have encountered and fixed this problem.

Resources