Wednesday, September 6, 2017

Zoom to user's location in map view using Swift 3

In this example, there is gonna be only one view controller, and I'm gonna create a map view programmatically for the view controller's view. (I'm using Xcode 8.)

Let me show you how this app works. When you launch the app in simulator, you'll see the map as shown in the image below.


Then, when you click on the My Location button (the blue one) at the top left corner of the screen, you'll get the popup below



After you click the Allow button, the map will zoom to your simulated location as shown in the following image.




1). Create a single view application in Xcode

Suppose you already created a single view application in Xcode, in which there is one view controller in Interface Builder referencing the MapViewController class (in MapViewController.swift file)

2). Add a map view as the view controller's view. 

Open MapViewController.swift file and update it as following:

import UIKit
import MapKit

class MapViewController: UIViewController {
    var mapView: MKMapView!
    
    override func loadView() {
        mapView = MKMapView()
        view = mapView
    }
}

3). Add a button to navigate to user's location


3.1. Add userLocationButton variable to the MapViewController class:

var userLocationButton: UIButton!

3.2. Override viewDidLoad() method and initialize the button in it as below:

override func viewDidLoad() {
        userLocationButton = UIButton(frame: CGRect(x: 8, y: 8, width: 100, height: 20))
        userLocationButton.setTitle("My Location", for: UIControlState.normal)
        userLocationButton.backgroundColor = UIColor.blue
        userLocationButton.addTarget(self, action: #selector(showUserLocation(_:)), for: UIControlEvents.touchUpInside)
        view.addSubview(userLocationButton)
    }

The action parameter of the addTarget() method is of type Selector struct. The Selector struct conforms to ExpressibleByStringLiteral protocol, which means that you can write a #selector expression as a value of the action parameter. The #selector expression lets you access to the selector used to access a method or to a property's getter or setter in Objective-C runtime. The value of a #selector expression is an instance of Selector type.

According to Apple's documentation, there are four types of expressions: prefix expressions, binary expressions, primary expressions, and postfix expressions. The compiler evaluates an expression and then return a value, causes side effects, or both. The #selector expression is a subtype of primary expressions.

3.3. Add the handler method for the click event of the button

func showUserLocation(_ sender: UIButton) {
}

I added this empty method just to avoid the compile-time error because the compiler will evaluate the #selector expression above and check if the method exists.

4). Detecting user's location using Core Location framework


We need to ask for user's permission to access location services. In other words, we need to request when-in-user authorization. Here is the instruction from Apple's developer documentation.


4.1. Open Info.list file in the project directory using Property List editor and add this key "Privacy - Location When In Use Usage Description" under "Information Property List" key. Its value is an additional description that will be added in the popup "Allow 'Viewtest' to access your location while you use the app?"

4.2.  Make the class MapViewController extend CLLocationManagerDelegate and then add the following fields:
    let locationManager = CLLocationManager()
    private var userLocationButtonClicked: Bool = false


4.3. Add the below code to the viewDidLoad() method after the opening bracket.
        mapView.showsUserLocation = true
    locationManager.delegate = self

Setting mapView.showsUserLocation to true tells the map view to show the user's location on the map, but the user might not see it as they might be viewing another location on the screen.

We set the property locationManager.delegate to self to implement the handler methods for the location service events in MapViewController class.

4.4. Update the showUserLocation() method as following:

func showUserLocation(_ sender: UIButton) {
        userLocationButtonClicked = true
     
        switch CLLocationManager.authorizationStatus() {
        case CLAuthorizationStatus.notDetermined, .restricted, .denied:
            locationManager.requestWhenInUseAuthorization()
        case CLAuthorizationStatus.authorizedWhenInUse, .authorizedAlways:
            requestLocation()
        }

}

Calling locationManager.requestWhenInUseAuthorization() method the first time will display a popup to ask the user for giving the app the access to the location service. Any choice the user makes, the authorization status is sent to the location manager's delegate through this method CLLocationManagerDelegate.locationManager(_:CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus).

Note that the popup won't show up next time you run the project. It seems like Xcode remembers the decision. To make it appear again, quit the simulator and remove the key "Privacy - Location When In Use Usage Description" from the Info.plist file and then run the app once. After that exit the app and add the key back and build and run the project again.

4.5. Add a handler method for the event of authorization status change

func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        let authStatus = CLLocationManager.authorizationStatus()
        if authStatus == CLAuthorizationStatus.authorizedWhenInUse
            || authStatus == CLAuthorizationStatus.authorizedAlways {
            requestLocation()
        }
}

private func requestLocation() {
        // check if the location service is availalbe on that device
        if !CLLocationManager.locationServicesEnabled() {
            return
        }
     
        locationManager.requestLocation()
}

The method locationManager.requestLocation() returns immediately. It requests for user's location in a separate thread. If the request is successful, it triggers the didUpdateLocations event and stops the location services automatically (to save power).

4.6. Add a handler method for the event of location update

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        if userLocationButtonClicked {
            userLocationButtonClicked = false
            zoomInLocation(locations.last!)
        }
}

func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        if let err = error as? CLError, err.code == .denied {
            manager.stopUpdatingLocation() // cancel all pending events for getting user's location
            return
        }
}

private func zoomInLocation(_ location: CLLocation) {
        let coordinateSpan = MKCoordinateSpan(latitudeDelta: 0.001, longitudeDelta: 0.001)
       let coordinateRegion = MKCoordinateRegion(center: location.coordinate, span: coordinateSpan)
        mapView.centerCoordinate = location.coordinate
        mapView.setRegion(coordinateRegion, animated: true)
}

The mapView.setRegion() method will display the region of the user's location. MKCoordinateSpan is used to define the distance between the user's location and the area around to be displayed on the screen. The shorter distance, the closer the map zooms in.

Note that you can't detect the real location of the user using the simulator even though you enables the location services in Security & Privacy settings. The user's location is simulated by Xcode and can be modified.

Here is the complete code of MapViewController class. 


No comments:

Post a Comment