Silver Challenge: User's Location


#1

I managed to add a button to the MapViewController and when I set the the mapView’s showsUserLocation property to true, I get this error:

Trying to start MapKit location updates without prompting for location authorization. Must call -[CLLocationManager requestWhenInUseAuthorization] or -[CLLocationManager requestAlwaysAuthorization] first.

When I try calling these methods, nothing happens. Is there something else I need to do?


#2

[quote=“jeffjax”]I managed to add a button to the MapViewController and when I set the the mapView’s showsUserLocation property to true, I get this error:

Trying to start MapKit location updates without prompting for location authorization. Must call -[CLLocationManager requestWhenInUseAuthorization] or -[CLLocationManager requestAlwaysAuthorization] first.

When I try calling these methods, nothing happens. Is there something else I need to do?[/quote]

You need to add something like:

        let locationAuthStatus = CLLocationManager.authorizationStatus()
        
        if locationAuthStatus == .NotDetermined {
            locationManager.requestWhenInUseAuthorization()
        }

Then add a new key called “NSLocationWhenInUseUsageDescription” to the info.plist with a value of the message you want to display when prompting the user to provide permission to use the device location.

When you run, you’ll then see the pop-up that requests permission to use the device location. This should clear the error.


#3

The error message says this:

[code]
class name
|
V
Must call -[CLLocationManager requestWhenInUseAuthorization]
^ ^
| |
instance method method name

  • signifies a class method
  • signifies an instance method[/code]

So, the first thing you might do is lookup CLLocationManager in the docs and read the preamble. Then read the description of the requestWhenInUseAuthorization() method. The description of the method says:

And, as I found out: it is critically important that there are no spaces after the end of the key name. :frowning:

At the bottom of the method description it says:

If you click the link, you go here:

[quote]About Info.plist Keys and Values

…[/quote]
which is a good read. Then scroll down and find this link:

Which will take you to this:

[quote]NSLocationWhenInUseUsageDescription (String - iOS) describes the reason why the app accesses the user’s location normally while running in the foreground. Include this key when your app uses location services to track the user’s current location directly. This key does not support using location services to monitor regions or monitor the user’s location using the significant location change service. The system includes the value of this key in the alert panel displayed to the user when requesting permission to use location services.

This key is required when you use the requestWhenInUseAuthorization method of the CLLocationManager class to request authorization for location services. If the key is not present when you call the requestWhenInUseAuthorization method …, the system ignores your request.

This key is supported in iOS 8.0 and later. If your Info.plist file includes both this key and the NSLocationUsageDescription key, the system uses this key and ignores the NSLocationUsageDescription key.[/quote]


#4

Hi!

I am also currently working on this issue. I was wondering the following: is it really made out that we need to use CoreLocation framework for this task? I see that the MapKit framework itself has the ability to show the user’s location (mapView.showsUserLocation = true), and the challenge’s text pointed us to use the MKMapViewDelegate, where I have found a callback when the MapKit has finished updating the user’s location (func mapView(_ mapView: MKMapView,
didUpdateUserLocation userLocation: MKUserLocation)). I am planning to use this callback method to change the zoom state of the MapView to zoom into the newly found state. Where does it say we’d need an independent CoreLocation object? I see that CoreLocation allows us to request using the location in the first place (the popup that occurs after requesting allowance through CoreLocation) - but how does MapKit use this answer then anyway?

By the way, as interesting as this is, but the challenges in this new edition (I was working part-time through the previous one) are quite hard… and the learning curve appears steep; also, some interesting chapters seem to be dropped (such as the chapter on Instruments, from the previous version) - although the content may still be present somewhere in the later text.

Cheers -
Björn


#5

I don’t think I did, but I don’t really know.

In hindsight, it doesn’t exactly say that.

The answer becomes a permanent setting on the iPhone, which subsequently can be checked with code.

Yes, I think so too.

If it helps, here is the path I forged through the docs:

[code]
MKMapViewDelegate docs
func mapView(_ mapView: MKMapView,
didUpdateUserLocation userLocation: MKUserLocation)

 |
 V

MKUserLocation docs
var location: CLLocation? { get }

 |
 V

CLLocation docs

 |
 V

CLLocationManager
(the holy grail)[/code]


#6

Hi!

Thanks for Your reply!

So, based on what I read I suppose that there may be a number of working implementations - some more based on CoreLocation directly, some on MapKit’s own abilities (which, I suppose, are based off of CoreLocation (not sure about it)). So, what I did was the following. I made a UIButton in the view controller’s loadView() method, and set its target to a method that first asks (via CoreLocation) for privileges to find the user’s location (not sure this is really required, but I kept it in there for now); then tell the MapKit map to show the user’s location; when MapKit does find the location, it will call the method You mentioned (func mapView(_ mapView: MKMapView, didUpdateUserLocation userLocation: MKUserLocation)) on its delegate (which I set to the view controller programmatically). This delegate method then handles the zoom-in using a MapKit function (setVisibleMapRect animated). Here is my code for those two functions:

func zoomToCurrentLocation(sender: UIButton!) {
        // make sure we have privilege to find user's location; we use CoreLocation for this, then later use user location finder method of MapKit; necessary like this?
        locationManager = CLLocationManager()
        if locationAuthStatus == .NotDetermined {
            locationManager.requestWhenInUseAuthorization()
        }
        mapView.showsUserLocation = true        
        // this triggers MapKit's own user location method, and displays a pin in the map; when the location is found, the MapKit map automatically triggers the mapView(_: didUpdateUderLocation...) method on its delegate (this view controller):
    }
    
    func mapView(mapView: MKMapView, didUpdateUserLocation userLocation: MKUserLocation) {
        print("MapKit has found the user's location, and called the delegate method")
        let zoomToMapPoint = MKMapPointForCoordinate((userLocation.location?.coordinate)!)  // we know we have a coordinate, so we can use force unwrapping here
        let zoomToMapSize = MKMapSize(width: 10000, height: 10000)  // set a rectangle around the user's location
        let zoomToMapRect = MKMapRect(origin: zoomToMapPoint, size: zoomToMapSize)
        mapView.setVisibleMapRect(zoomToMapRect, animated: true)
    }

Curious to hear Your comments!

Best regards,
Björn


#7

Hi,

After responding to your question, I decided to see if I could change my code to use MKMapViewDelegate methods to zoom in on the user’s location–rather than using CLLocationManagerDelegate methods, which is where the docs led me. I ended up getting bogged down in the delegate method:

I had this code:

[code] let currentLocation = mapView.userLocation.location

    if let validCurrentLocation = currentLocation {
          ...
          ...
    }
    else {
        print("No valid user location")
    }

[/code]
and no matter what I tried, my code always informed me:

Super frustrating!!! After reading your post, I realize now that mapViewWillStartLocatingUser() must execute too early in the process. I think the Will* methods execute before the event, and the Did* methods execute after the event, so my Will method was executing before the user’s location was retrieved.

At some point when I was scouring the docs, I read the description for your method, didUpdateUserLocation(), but I was worried that if the user didn’t move away from their location, then there would be no update, so that method wouldn’t get called, so I passed on it. Now that I try it, I see that the description in the docs is misleading, and the method does exactly what we want. The beacon animation showing the current location wasn’t present when I used CLLocationManager delegate methods, so it was neat to see that! If you haven’t tried it, set the user’s location to Hawaii, select the Satellite view, then click your button to zoom in on the user’s location. Beautiful!

After that, the only difference was that I zoomed using mapView.setRegion(). (If anyone cares, auto completion showed me that MKUserLocation has an undocumented property: coordinate, which saved me a step over using the documented location property.) Oh yeah, I also did this:

if CLLocationManager.locationServicesEnabled() { mapView.showsUserLocation = true //Calls delegate methods }

because it says to do that at the CLLocationManager docs. Here it is in context:

        switch CLLocationManager.authorizationStatus() {
            case .Denied, .Restricted:
                return
            default:
                break
        }

        //The following code could go in the default: for the switch, but
        //I think the extra indenting when you do that makes it harder to read.

        locationManager = CLLocationManager()
        locationManager.requestWhenInUseAuthorization()
                
        if CLLocationManager.locationServicesEnabled() {
            mapView.showsUserLocation = true //calls all MKMapViewDelegate methods
        }
        
    }

I think your solution, using MKMapViewDelegate methods, is what the author’s wanted. Nice. I’ll just point out that doing the following assignment:

is pretty similar to calling:

And guess what the relevant CLLocationManagerDelegate method is named?

The name locationManager(_:didUpdateLocations:) sounds familiar! Here is the description from the docs:

[quote]locations
An array of CLLocation objects containing the location data. This array always contains at least one object representing the current location. [/quote]

======

According to the CLLocationManager docs:


#8

Hi, 7stud!

Thanks. I think that You are right, the method You have used at first (mapViewWillStartLocatingUser) is, in fact, too early in the process; presumably, it can be used to do some setup before the hardware-based (GPS, GSM triangulation (is that still a thing?), WiFi networks, Glonass probably…) even begins; I am not sure what this could be used for, but it is definitely too early - there will be no location available at this point - hence the error message You have received. I can tell You from my own experience playing around with a GPS module years back that finding a location with it (using satellites) can take seconds if not minutes (depending on the positioning of the receiver) - and sometimes doesn’t work at all. Also, since iPhone has other methods available (WiFi networks, GSM triangulation, Glonass), we are working on a high level of abstraction, and the system can take a while before a location is available. All those points are good reasons why the design using delegation and a callback method is really required here. The method You have used earlier (mapViewWillStartLocatingUser) will be executed before the system even tries to find the location for our app, so it won’t even bother before this method has ended running (which it will, depending on the code contained in this method, in very short time; typically within nanoseconds, I presume) - but no matter how long this method runs, the system wouldn’t even bother to look for the location before this method has finished; again, I do not know what this method is for, but presumably it is there for setup purposes required to happen before the system even tries to determine the iPhone’s location - probably, it could be used to define (in detail) the way how the location determination should occur.

The naming is a good hint. I think it is similar to what happens with the view controller methods (viewDidLoad, viewWillAppear, viewDidAppear, etc.).

Thanks for the tip regarding the mapView.setRegion() method; this might be much easier than the method I used to zoom in on a rect; can You define the zoom state using it?

Best regards,
Björn


#9

I used the same number of lines of code as you did. I guess the reason I used setRegion() was because it was first in the table of contents for the MKMapView docs.

Yes. You have to dig down several levels in the docs, but you end up setting the zoom with Double values–the lower the number, the greater the zoom–then you specify a centerpoint when you create “a region”, and the centerpoint can be conveniently obtained with MKUserLocation: (property) coordinate.


#10

I do think the hint for using MKMapViewDelegate is misleading, the right delegation is CLLocationManagerDelegate, which makes the issue much simpler.


#11

Hi, mingliangfeng!

Thanks for Your reply! Could You elaborate why using CoreLocation delegates are much simpler? Not sure I immediately see a reason why this would be simpler. But very curious to hear! Thanks!

Best,
Björn


#12

My goodness, this was quite a challenge! :smiley: I spent two whole days working on just this challenge; the first day was spent trying to just use MKMapKit, but all paths kept leading me back to CoreLocation. At the end of that day I turned to this forum to see how you all had tackled this. I want to thank all of you here, for I found all the comments incredibly helpful and enlightening.
The second day I explored using CoreLocation and by the end of the day had come up with using the following delegate methods to make it work:

viewDidLoad() // Set up location manager locationManager:didChangeAuthorizationStatus // Obtain user permission to use location services locationManager:didUpdateLocations // Initiate location services locationManager:didFailWithError // Required, but doesn't do anything
Unlike some of you, I am VERY glad this challenge was so difficult, for in the struggling I learned SO much! You will not believe how many "Ah-ha!’ moments I had; this exercise brought together and solidified so many of the concepts we have learned thus far. I also gained a newfound confidence in my ability to research, implement and troubleshoot new capabilities, and this alone was worth way more than the pain and struggles involved.

Git VCS
Let me put in one final plug to you all. If you are not using Git or some other version control system, please, for your own sanity, make the time to learn it. During the course of doing this challenge,e I went down numerous paths of exploration trying different possibilities. Each time I hit a dead-end, I simply went into my favorite Git GUI (Gitkraken from Axosoft) and simply reset my project to the last clean state (the Bronze challenge implementation)) and started fresh again with a clean slate.
Like the struggles with completing the current exercise, Git might b e a pain to learn, but in the end it is worth its weight in gold!


#13

Yeah, ‘didUpdateLocations’ is needed. When I coded that, I then got a message that it couldn’t find ‘didFailWithError’. I still wasn’t getting a location. I added code to didFailWithError to emit the error, but got something like “. . . 0 . . . null . . .”. Searching with that actual string suggested I needed to set up a default location in Xcode that was not “none”, which is what was set when I looked at Product -> Scheme -> Edit Scheme, Run, Options. Here, for Core Location, Allow Location Simulation was checked, and Default Location was “none”. I set that to New York, and things were much better. Now I want to run it on my iPod Touch, which I’ve been doing for all earlier examples/exercises by connecting and selecting it in the Scheme dropdown. But it now does not seem to go to New York anymore, just sitting there with the US. When Build’ing for a device, should I uncheck Allow Location Simulation, go back to location=none, or what? Any thoughts? Thanks.


#14

Maybe I’m misunderstanding the challenge and not doing it correctly, but I thought the implementation of this was pretty simple… even without using delegation. Yes, it required LOTS of reading, but this is how I got it to work:

class MapViewController: UIViewController {

    var mapView: MKMapView!
    let locationManager = CLLocationManager()

    override func loadView() {

        ... previous code ...

        let findMeButton = UIButton(type: UIButtonType.System)
        findMeButton.translatesAutoresizingMaskIntoConstraints = false
        findMeButton.setImage(UIImage(named: "NavigationIcon"), forState: UIControlState.Normal)
        findMeButton.addTarget(self, action: "findUserLocation:", forControlEvents: UIControlEvents.TouchUpInside)
        view.addSubview(findMeButton)

        findMeButton.bottomAnchor.constraintEqualToAnchor(bottomLayoutGuide.topAnchor, constant: -8).active = true
        findMeButton.trailingAnchor.constraintEqualToAnchor(margins.trailingAnchor).active = true
    }

    func mapTypeChanged(segmentedControl: UISegmentedControl) { ... }

    func findUserLocation(button: UIButton) {
        // print("You found me!")

        if CLLocationManager.authorizationStatus() == .AuthorizedWhenInUse {
            mapView.showsUserLocation = true
            mapView.setUserTrackingMode(.Follow, animated: true)
        } else {
            locationManager.requestWhenInUseAuthorization()
        }
    }

#15

Darrell, thanks for this code and insight which is very helpful, however I can’t see any button with your code, although if I click at the place where the button ‘should be’ I do get the expected response from the code…can you please share some insight why is this happening? Thanks.


#16

Hello,

One thing I have found is that you don’t seem to be able to drag and drop a button onto a MapView. At least not onto a MapView that was previously created. Is this true or not?

If you create a MapView programmatically, do you have to create all subviews of that map view programmatically?

I’ve spent two days of frustration trying to get that to work.

The worst thing seems to be that almost all of the documentation and tutorials for MKMapView seem to be written for Objective-C as opposed to Swift and what little I have found for Swift is for Swift 1.0 - and Swift 3 was just announced - Aargh!


#17

Wouldn’t being able to drop a button onto a view require that the view be previously created?

In any case, open up a new project. Drag a MapView onto the storyboard, then drag a button onto the MapView, then add the MapKit framework to your project settings. Does the app show any compile time errors? If not, run your app. Does the app show any runtime errors? Is there a button on your MapView? If not, what happens?

If you create a MapView programmatically, do you have to create all subviews of that map view programmatically?

No. In your new project, delete the MapView so that there is only a button in the storyboard. Create an outlet to the button in your ViewController. Then try this code:

class ViewController: UIViewController {
    
    @IBOutlet weak var button: UIButton!
    
    override func loadView() {
        super.loadView()
        
        let mapView = MKMapView()
        view = mapView
        mapView.addSubview(button)
    }

#18

Thanks!

I’ve gotten past that point in the code.

// CHANGED: Added references to MKMapViewDelegate and CLLocationManagerDelegate
class MapViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {
    // Create a locationManager
    var locationManager: CLLocationManager!
    
    // Create an outlet to the mapView on the storyboard
    @IBOutlet weak var mapView: MKMapView!

    // Create an outlet to the button on the storyboard
    @IBOutlet var myCurrentLocation: UIButton!

    // Attach an action to the button on the storyboard
    @IBAction func myCurrentLocationButtonPressed(sender: AnyObject) {
        print("Current Location Button Pressed.")
        // So far the button doesn't do anything except print an acknowledgment
    }

    // CHANGED: use viewDidLoad instead of loadView
    override func viewDidLoad() {
        //  Always call the super implementation of viewDidLoad
        
        super.viewDidLoad()
        
        print("MapViewController loaded its view.")
        
        // CHANGED:  Set it as a subview of this controller
        view.addSubview(mapView)
        
        let segmentedControl = UISegmentedControl(items: ["Standard", "Hybrid", "Satellite"])
        segmentedControl.backgroundColor = UIColor.whiteColor().colorWithAlphaComponent(0.3)
        segmentedControl.selectedSegmentIndex = 0
        
        segmentedControl.addTarget(self, action: #selector(MapViewController.mapTypeChanged(_:)), forControlEvents: .ValueChanged)
        
        segmentedControl.translatesAutoresizingMaskIntoConstraints = false

        // CHANGED: you add the segmentedController to the mapView and not to the viewController.  This makes it a subview of the mapView subview and not a subview of the viewController.
        mapView.addSubview(segmentedControl)
        
        // CHANGED: the margins are now related to the mapView and not to the viewController
        let margins = mapView.layoutMarginsGuide
        
        let topConstraint = segmentedControl.topAnchor.constraintEqualToAnchor(margins.topAnchor, constant: 2)
        let leadingConstraint = segmentedControl.leadingAnchor.constraintEqualToAnchor(margins.leadingAnchor)
        let trailingConstraint = segmentedControl.trailingAnchor.constraintEqualToAnchor(margins.trailingAnchor)
        
        topConstraint.active = true
        leadingConstraint.active = true
        trailingConstraint.active = true
        
        if (CLLocationManager.locationServicesEnabled()) {
            // set up the locationManager
            locationManager = CLLocationManager()
            locationManager.delegate = self
            locationManager.desiredAccuracy = kCLLocationAccuracyBest
            locationManager.requestAlwaysAuthorization()
            locationManager.startUpdatingLocation()
        }

    }

    func mapTypeChanged(segControl: UISegmentedControl) {
        
        // Using a variable here allows us to show the switch parameters before they execute
        let mapTypeIndex = segControl.selectedSegmentIndex
        
        print("Map Type changed to \(mapTypeIndex)")
        
        switch mapTypeIndex {
        case 0:
            mapView.mapType = .Standard
        case 1:
            mapView.mapType = .Hybrid
        case 2:
            mapView.mapType = .Satellite
        default:
            break
        }
    }
    
    func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        print("locationManager")
        let location = locations.last
        print("Location Is: \(location)")
        // NOTE: This code sets the initial location to a specific location - by latitude and longitude
        let center = CLLocationCoordinate2DMake(42.0745, -87.6845)  // Bahá'í House of Worship, Wilmette, IL, USA
        // NOTE: This code sets the initial location of the map to the current location.  IMPORTANT: Since the Mac has no built-in GPS, the IOS simulator defaults to a location in California.  When you run it on a real device, the actual location of the device will be used.
        // let center = CLLocationCoordinate2D(latitude: location!.coordinate.latitude, longitude: location!.coordinate.longitude)
        let region = MKCoordinateRegion(center: center, span: MKCoordinateSpan(latitudeDelta: 0.002, longitudeDelta: 0.002))
        self.mapView.setRegion(region, animated: true)
        self.locationManager.stopUpdatingLocation()
    }
}

#19

So the reason why you probably aren’t seeing the button is this line:

findMeButton.setImage(UIImage(named: “NavigationIcon”), forState: UIControlState.Normal)

If you don’t have an Image in your assets named “NavigationIcon” there is nothing for it to grab, so it just shows up blank (which is why you are still able to press the button, since it is technically there). If you don’t have any images readily available you can just pass in “MapIcon” since this should be in your assets from earlier in the project. Hope this helps.


#20

Spend few hours on this Silver challenge as well… and finally got it works.

  1. Setup MapView delegate (partial code shown)

    class Map2ViewController: UIViewController, MKMapViewDelegate {

    // MARK: - Properties

    var mapView: MKMapView!
    var button: UIButton!

    override func loadView() {
    // Create a map view
    mapView = MKMapView()

     // Set it as the view of this view controller
     view = mapView
     
     mapView.delegate = self
     ...
     ...
    
  2. Add button to loadView()

     button = UIButton(type: .contactAdd)
     view.addSubview(button)
     
     button.addTarget(self, action: #selector(Map2ViewController.updateUserLocation), for: .touchDown)
     
     button.translatesAutoresizingMaskIntoConstraints = false
     
     let buttonTrailingConstraint = button.trailingAnchor.constraint(equalTo: margins.trailingAnchor)
     let buttonBottomConstraint = button.bottomAnchor.constraint(equalTo: bottomLayoutGuide.topAnchor, constant: -8)
     buttonTrailingConstraint.isActive = true
     buttonBottomConstraint.isActive = true
    
  3. Create a target action for the button (I am not sure whether we really need the locationManager setup for authorization here, or we just need to set showsUserLocation to true will be enough)

    func updateUserLocation() {
    print(“updateUserLocation is clicked”)

     let locationManager = CLLocationManager()
     locationManager.requestWhenInUseAuthorization()
     
     mapView.showsUserLocation = true
    

    }

  4. Add the MapView delegate method that get called when current location is updated

    func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
    print(“Log: did update user location”)

     let currentLocation = mapView.userLocation.location
     
     if let currentLocation = currentLocation {
         let location = CLLocationCoordinate2DMake(currentLocation.coordinate.latitude, currentLocation.coordinate.longitude)
         
         print("Log: currentLocation is \(location)")
         
         //mapView.setCenter(location, animated: true)
         
         let span = MKCoordinateSpanMake(0.01, 0.01) // 1 degree ~ 0.0175 radian
         let region = MKCoordinateRegion(center: location, span: span)
         
         mapView.setRegion(region, animated: true)
     }
    

    }

  5. And finally, I also add two keys in the Info.plist for Privacy - Location When In Usage Description & Location Usage Description. For the value of the key, just enter the message your want.

Alright, continue with last challenge… :wink: