Swift Tutorial - make your own iOS MapKit application - SELISE

Swift Tutorial – make your own iOS MapKit application

July 20, 2014

In today’s Swift tutorial, we will explore MapKit. MapKit is a nice little framework developed by Apple. We will learn to integrate this MapKit with the Google Map javascript api and thus develop a complete application using Swift, like the one you can see below:

App Description

The very first view will have a textbox. The user must first input their Area and City. Then the application will give the user a scope to select a category from which to search further. The categories can be things like restaurants, airports, hospitals, mosques, churches, atm’s, banks, and so on.

After a category has been chosen, the app will redirect the user to the mapview, where they can see the different markers/annotations of their individual results. Clicking each marker will open a custom flyout where he will see the name, the address and an icon of that place.

There are, of course, a lot of similar applications out there; many are natively available in Google play and the App Store.

To develop such an app by ourselves, we need to concentrate on the following things:

  • Configuring the Google App Console to use Google Map Javascript API with an API Key

  • Having a basic knowledge about MapKit

  • Google Map geolocation searcher API

  • If required, implementing custom flyout and annotation using MKMapViewDelegate

  • Having a basic knowledge of Swift

Configure Google App Console to use Google Map Javascript Api with a API Key

Go to Google Developers Console ; and create a project. Remember, you need to login first to use Google’s Console!

Once that is done, from your project, go to the Apps and Auth section and enable Google MAP related API.

Now you must create a key inside Credentials/Public API Access section. You are creating this API for the browser, and nothing else. After successfully completing this, you should see an API key. This API key is required to call different google APIs.

MapKit

The Mapkit framework makes it easy working with maps, and is highly customizable. You can pretty much do whatever you like on it. To enable the framework, drag a UKMapView to storyboard and link “MapKit framework” to your project

Start View/Search View

Start View/Search View of the app is very simple as well. I have put in a TextView where the user is supposed to input the area and the city. Upon pressing the return key, it will internally call Google Map API  to search with this area and city and retrieve a GeoLocation (Longitude, Latitude). If the Google API failed to retrieve a valid geolocation, then it is highly likely that the information put in were not valid, or were incorrect.

After obtaining the geolocation, the user will see a list from where they can pick a category, let’s say for example, Restaurants. What the app does after this is basically call another Google API and do a nearby search (5km) for all the places that fit the category, and then display them in a MapView.

Please note, that for displaying the categories, I have used TableView. The code of our SearchView are as follows:

// ViewController.swift
// AddressMap
//
// Created by md arifuzzaman on 7/15/14.
// Copyright (c) 2014 md arifuzzaman. All rights reserved.
//

import UIKit
import MapKit

class ViewController: UIViewController,UITextFieldDelegate, UITableViewDataSource, UITableViewDelegate {

let apiKey = "" //here user your own api key
let mapHelper:MapHelper = MapHelper();
var autoCompleteTableView:UITableView;
var autoCompleteDataSource:Array = [];
var mapData:CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: 0, longitude: 0)
var searchBy:String = ""

init(coder aDecoder: NSCoder!) {
autoCompleteTableView = UITableView(frame: CGRectMake(13, 200, 295, 200), style: UITableViewStyle.Plain);
super.init(coder: aDecoder);
}

@IBOutlet var txtSearch: UITextField
override func viewDidLoad() {
super.viewDidLoad()
txtSearch.delegate = self;
NSNotificationCenter.defaultCenter()?.addObserver(self, selector: Selector("dataLoaded:"), name: "DataLoaded", object: nil);
autoCompleteTableView.dataSource = self;
autoCompleteTableView.delegate = self;
autoCompleteTableView.backgroundColor = UIColor.clearColor();
autoCompleteTableView.separatorColor = UIColor.clearColor();

autoCompleteDataSource.append("Restaurant");
autoCompleteDataSource.append("Airport");
autoCompleteDataSource.append("Atm");
autoCompleteDataSource.append("Bank");
autoCompleteDataSource.append("Curch");
autoCompleteDataSource.append("Hospital");
autoCompleteDataSource.append("Mosque");
autoCompleteDataSource.append("Movie_theater");
autoCompleteTableView.hidden = true;

self.view.addSubview(autoCompleteTableView);

}

override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated);
//autoCompleteTableView.hidden = true;
}

func tableView(tableView: UITableView!, didSelectRowAtIndexPath indexPath: NSIndexPath!){

searchBy = self.autoCompleteDataSource[indexPath.row];
let searchkey = self.autoCompleteDataSource[indexPath.row].lowercaseString;

let serviceHelper = ServiceHelper();
let dynamicURL = "https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=(mapData.latitude),(mapData.longitude)&radius=5000&types=(searchkey)&sensor=true&key=(apiKey)"
print(dynamicURL);
serviceHelper.getServiceHandle(self.dataLoaded, url: dynamicURL);

}

func tableView(tableView: UITableView!, numberOfRowsInSection section: Int) -> Int{
return self.autoCompleteDataSource.count;
}

// Row display. Implementers should *always* try to reuse cells by setting each cell's reuseIdentifier and querying for available reusable cells with dequeueReusableCellWithIdentifier:
// Cell gets various attributes set automatically based on table (separators) and data source (accessory views, editing controls)

func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell!{
var cell:UITableViewCell?;
let cellString = "autocompletecell";
if let cellToUse = cell{

}
else{
cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: cellString);
}

let data = autoCompleteDataSource[indexPath.row];
var img:UIImage?;

if(data == "Restaurant"){
img = UIImage(named: "restaurent.jpg");

}
else if(data == "Airport"){
img = UIImage(named: "airport.png");
}
else if(data == "Hospital"){
img = UIImage(named: "hospital.png");
}
else if(data == "Mosque"){
img = UIImage(named: "mosque.png");
}
else if(data == "Curch"){
img = UIImage(named: "church-icon.png");
}
else if(data == "Atm"){
img = UIImage(named: "atm.png");
}
else if(data == "Bank"){
img = UIImage(named: "bank.png");
}
else if(data == "Movie_theater"){
img = UIImage(named: "cinema.png");
}
else{
img = UIImage(named: "noimage.gif");
}

cell!.imageView.image = img!;
cell!.textLabel.text = data;
cell!.backgroundColor = UIColor(red: 100, green: 100, blue: 190, alpha: 0.5);

return cell!;
}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}

func textFieldShouldReturn(textField: UITextField!) -> Bool{
txtSearch.resignFirstResponder();
self.doSearch()
return true;
}

func doSearch(){

dispatch_async(dispatch_get_main_queue()){
let searchText = self.txtSearch.text.stringByTrimmingCharactersInSet(NSCharacterSet(charactersInString: " "))
if(searchText.isEmpty){
return;
}

self.mapData = self.mapHelper.geoCodeUsingAddress(searchText);
if(self.mapData.latitude <= 0.0 && self.mapData.longitude <= 0.0){
var alert = UIAlertController(title: "Warning", message: "Unable to find any location", preferredStyle: UIAlertControllerStyle.Alert);
alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.Default, handler: nil));
self.presentViewController(alert, animated: true, completion: nil);
self.autoCompleteTableView.hidden = true;
return;
}
self.autoCompleteTableView.hidden = false;

}

}

func dataLoaded(userData:SearchModel[]){
//print(userData);

let storyBoard = UIStoryboard(name: "Main", bundle: nil);
let mapController:MapViewController = storyBoard.instantiateViewControllerWithIdentifier("mapViewController") as MapViewController;
mapController.mapData = userData;
mapController.searchBy = self.searchBy;
self.navigationController.pushViewController(mapController, animated: true);

}

}

That’s it, very simple!

Two very important Google APIs that have been used here are:

1. http://maps.google.com/maps/api/geocode/json?sensor=false&address=[enter your address here]

2. https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=[lat,lon]&radius=5000&types=[category]&sensor=true&key=[enter your API key here]

 MapViewController

MapViewController is responsible for displaying the MapData as returned by Google. I would like to point out an interesting aspect of this controller. As you can see, we implemented MKMapViewDelegate. The reason behind this is, when we click a marker, the app will produce a popup with a custom view with different useful information. I implemented didSelectAnnotationView which is supposed to fire on each click.  The details is given below:

 

// MapViewController.swift
// AddressMap
//
// Created by md arifuzzaman on 7/15/14.
// Copyright (c) 2014 md arifuzzaman. All rights reserved.
//

import UIKit
import MapKit;

class MapViewController: UIViewController, MKMapViewDelegate {

@IBOutlet var _mapCtl: MKMapView

var searchBy:String = "";
var mapData:SearchModel[]?;
var customAnotations:CustomAnotation[] = CustomAnotation[]();

init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
// Custom initialization
}

init(coder aDecoder: NSCoder!) {
super.init(coder: aDecoder);
}

func mapView(mapView: MKMapView!, didDeselectAnnotationView view: MKAnnotationView!)
{
for childView:AnyObject in view.subviews{
childView.removeFromSuperview();
}
}

func mapView(mapView: MKMapView!, didSelectAnnotationView view: MKAnnotationView!){
if(!view.annotation.isKindOfClass(MKUserLocation)){
let flyOutView:CustomFlyout = (NSBundle.mainBundle().loadNibNamed("CustomFlyout", owner: self, options: nil))[0] as CustomFlyout;
var calloutViewFrame = flyOutView.frame;
calloutViewFrame.origin = CGPointMake(-calloutViewFrame.size.width/2 + 15, -calloutViewFrame.size.height);
flyOutView.frame = calloutViewFrame;

let customAnotation = view.annotation as CustomAnotation;
let model = customAnotation.searchModel;

flyOutView.lblTitle.text = model!.name;
let url = NSURL(string: model!.icon);
let urlData = NSData(contentsOfURL: url);
let img = UIImage(data: urlData);
flyOutView.lblIcon.image = img;
flyOutView.lblPosition.text = model!.address; //"Longitude: (model!.lon) & Latitude: (model!.lat)";
view.addSubview(flyOutView);
}
}

override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.title = "Search By (searchBy)";
self._mapCtl.mapType = .Standard;
self._mapCtl.delegate = self;
self.customAnotations.removeAll(keepCapacity: false);
for searchModel in self.mapData!{
let customAnotation = CustomAnotation()
customAnotation.coordinate = CLLocationCoordinate2D(latitude: searchModel.lat, longitude: searchModel.lon);
customAnotation.title = "";
customAnotation.subtitle = ""
customAnotation.searchModel = searchModel;
self.customAnotations.append(customAnotation)
}

self.zoomToFitMapAnnotations();

}

func zoomToFitMapAnnotations(){
var topLeftCoord:CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: 0, longitude: 0);
topLeftCoord.latitude = -90;
topLeftCoord.longitude = 180;

var bottomRightCoord:CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: 0, longitude: 0);
bottomRightCoord.latitude = 90;
bottomRightCoord.longitude = -180;

var foundAnotation = false;

for anotation in self.customAnotations{
topLeftCoord.longitude = fmin(topLeftCoord.longitude, anotation.coordinate.longitude);
topLeftCoord.latitude = fmax(topLeftCoord.latitude, anotation.coordinate.latitude);

bottomRightCoord.longitude = fmax(bottomRightCoord.longitude, anotation.coordinate.longitude);
bottomRightCoord.latitude = fmin(bottomRightCoord.latitude, anotation.coordinate.latitude);

self._mapCtl.addAnnotation(anotation);
foundAnotation = true;
}

if(!foundAnotation){
return;
}

var region:MKCoordinateRegion = MKCoordinateRegion(center: topLeftCoord, span:MKCoordinateSpan(latitudeDelta: 0, longitudeDelta: 0));
region.center.latitude = topLeftCoord.latitude - (topLeftCoord.latitude - bottomRightCoord.latitude) * 0.5;
region.center.longitude = topLeftCoord.longitude + (bottomRightCoord.longitude - topLeftCoord.longitude) * 0.5;
region.span.latitudeDelta = fabs(topLeftCoord.latitude - bottomRightCoord.latitude) * 1.1;
region.span.longitudeDelta = fabs(bottomRightCoord.longitude - topLeftCoord.longitude) * 1.1;

self._mapCtl.regionThatFits(region);
self._mapCtl.setRegion(region, animated: true);

}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}

}

That is pretty much all the major parts behind this app. I hope this will help you develop your very own Map Centric iOS app. If anyone is interested to get the source code, please download from here.


Md. Arifuzzaman Md. Arifuzzaman is a Senior Software Engineer at SELISE rockin’ software. He has extensive experience in diverse facets of C#, .NET, BI development, and mobile app development (Objective-C, Android).