Everyone may remember when Apple first introduced MapKit to replace Google Maps on iPhones, they ended up to apologizing. However over time, developers have found how easy it is to use MapKit. This API provides features which make using both 2D and 3D maps very easy. More importantly, Google charges for map views over a few thousand views and Apple doesn’t. For many applications requiring a lot of map views or when you have over a few thousand users, MapKit might make better sense for a developer not willing to pay the costs for an external API.
In this lesson, we’ll introduce MapKit, and how display a map in both 2d and 3d. We’ll discuss many of the attribute you have in the UIMapView class to make a great map. We’ll also talk about a small cheat using Google maps if you need only a few map points. Along the way I’ll throw in some Chicago history trivia.
Make a New Project
Start by making a New Single-view project Called MyMapKitDemo. Use a Universal device and Swift as the language. When it loads, we need to turn on MapKit. In the Project Properties, click on the the Capabilities tab.
You will see a list of functions we can use. About halfway down the list you’ll See Maps
Turn on maps and the menu opens.
We won’t be changing anything here, but the framework will load when we need it.
Go to the storyboard. In the Object Library, find the Map Kit View object
Drag it somewhere on the story board.
Select the Map view. Click the button in the Auto Layout menu on the lower right of the storyboard. This will bring up the the pin menu. Click off Constrain to margins. Set the margins to be 0 on all sides. Change the Update Frames to Items of New Constraints. Your pin menu should look like this.
Click Add 4 Constraints. The map view takes the entire view on the story board.
With the map view still selected, look at the attribute inspector. At the top is some attributes specific to map views:
The Type attribute sets the map to be either Standard, Satellite or Hybrid, which is combination of the two (a satellite with street names). The Allows attributes control if the user can use zooming,scrolling rotation or 3D view. By default, the 3D view is on, and we’ll see this is a good thing. The Shows attributes control extra items on the map. You’ll note User Location is off. User location shows a dot where the user is. However that dot only shows up if the map shows a region the user happens to be in. Unless you live in the Lincoln Park or Chinatown neighborhoods of Chicago, in our app you won’t be visible.
We’ll be changing a few of these through properties in code. You can leave them alone for now.
Add seven buttons to the view. Select all seven buttons. In the attributes inspector, find the Text color button, and click the color swatch in the button.
A color palette appears. Using the RGB colors, change the color to a Dark Blue (#000088) color.
In the attributes inspector, scroll down to View. Click the swatch for the background color. Change the Background to White #FFFFFF and set the Opacity to 50%
Change the title on the seven buttons to CPOG, Wrigley, Connie’s, Satellite, 2dMap, FlyOver, and Clean Map. Arrange everything like this.
Select the CPOG,Wrigley and Connie’s buttons. Click the Stack view Button in the auto layout buttons. In the attributes inspector, change the stack view to a Horizontal Axis, Alignment of Fill, Distribution of Fill Equally and Spacing of 0:
Click the pin button . Turn off Constrain to Margins. Set the top constraint to 20 points and the left and right to 0 points, leaving the bottom unchecked. Set Update Frames to Items of New Constraints.
Add the constraints.
Select the Satellite, 2D Map, Flyover, and Clean Map buttons. Click the Stack view Button in the auto layout buttons. In the attributes inspector, change the stack view to a Horizontal Axis, Alignment of Fill, Distribution of Fill Equally and Spacing of 0. Click the pin button
. Turn off Constrain to Margins. Set the bottom constraint to 20 points and the left and right to 0 points, leaving the top unchecked. Update Frames to Items of New Constraints.
Add the three constraints. The final layout looks like this:
We need to wire up the outlets and actions. Go to the ViewController.swift file. Before we do anything else, MapKit is a separate framework than UIKit. Just under the import
UIKit
add import
MapKit
import UIKit import MapKit
Once you do that, change the viewController
class to add all of our outlets and actions:
class ViewController: UIViewController { //MARK: Properties and Outlets @IBOutlet weak var mapView: MKMapView! //MARK: - Actions //MARK: Location actions @IBAction func gotoCPOG(sender: UIButton) { } @IBAction func gotoWrigley(sender: UIButton) { } @IBAction func gotoConnies(sender: UIButton) { } //MARK: Appearance actions @IBAction func toggleMapType(sender: UIButton) { } @IBAction func overheadMap(sender: UIButton) { } @IBAction func flyoverMap(sender: UIButton) { } @IBAction func toggleMapFeatures(sender: UIButton) { } //MARK: Instance methods //MARK: Life Cycle override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. } }
Go back to the storyboard, and open the assistant editor in Automatic. Drag from the circle next to gotoCPOG
to the CPOG button on the storyboard. Do the same from gotoWrigley
to Wrigley, gotoConnies
to Connie’s, toggleMapType
to Satellite, overheadMap
to 2D Map, flyoverMap
to FlyOver, and toggleMapFeatures
to Clean Map. Finally, drag from the outlet mapView
to the mapView.
Build and run. You get a map of the continent you happen to be in.
This is the default setting of a map view – a region that takes in a continent closest to the current location according to the simulator. Since all the attributes were left on, you can pan and zoom on this map. To zoom on a simulator, hold down the Option key and drag with the mouse. I zoomed in on Chicago, where we’ll be in the app.
Getting Sample Location Data From Google Maps
We’ll need some sample location data. I’m going to pick my favorite baseball field and two favorite pizza restaurants to for this. MapKit uses several coordinate systems, but the most important is latitude and longitude. If you need only a few points to test it’s easy to get them by looking up the location in Google Maps. Maps has the location information embedded in the URL for the view.
Go to the web and in Google maps, search for Wrigley Field. If you want the address to search, it’s the one Elwood Blues uses in the Blues Brothers: 1060 W. Addison.
Or you can just go to https://www.google.com/maps/place/Wrigley+Field. When it appears, Click your mouse in the middle of the intersection of Addison and Clark Streets.
If you look at the URL you find something similar to this
https://www.google.com/maps/place/Wrigley+Field/@41.9472901,-87.6565357,21z/data=!4m2!3m1!1s0x880fd3b2e59adf21:0x1cea3ee176ddd646
The important part is from the /@ to the next Slash.
/@41.9472901,-87.6565357,21z
That’s the map coordinates in latitude and longitude of that intersection. For Apple maps we need one other piece of data: what direction we are pointing, known as the heading. To get that, drop the little guy for Street Siew onto the same intersection, pointing towards the big red Wrigley Field sign. You get this data
@41.9471939,-87.6565108,3a,75y,41.73h,90.81t/
The first two are the map coordinates again. They may not match exactly our first pair. the important number for us is the heading 41.73h
which tells us the compass direction we are pointing, 41.73 degrees from north.
The three pieces of data we need are latitude 41.9471939 longitude -87.6565108, and heading 41.73 degrees. You can use this method to get coordinates if you have no other way to get the data. In upcoming lessons, we’ll take coordinate data directly from City of Chicago databases and remove this step.
Core Location Data Types
We represent data for maps in the Core Location data types. Here’s a table to summarize:
CL Type | Type | Description/Notes |
---|---|---|
CLDegrees | Double | A latitude or Longitude |
CLDirection | Double | A heading in degrees based on the distance from North 0 degrees |
CLDistance | Double | A Distance in meters |
CLSpeed | Double | A speed in meters/Second |
CLLocationCoordinate2D | struct { var latitude: CLLocationDegrees, var longitude: CLLocationDegrees,} |
A coordinate on a map based on latitude and longitude |
CLAccuracy | Double | The accuracy of a coordinate value in meters. |
We’ll use most of these as we build our app. We’d like some way of storing the data we collected from Google Maps in some constants. I made a struct to do that. Add this to ViewController
.
//MARK: Location constants struct cameraInfo{ var location = CLLocationCoordinate2D() var heading = CLLocationDirection() init( latitude:CLLocationDegrees, longitude:CLLocationDegrees, heading:CLLocationDirection ){ self.location = CLLocationCoordinate2D( latitude: latitude, longitude: longitude) self.heading = heading } }
We store a CLLocationCoordinate2D
and a CLLocationDirection
. To make location
, we use two CLLocationDegrees
, one for latitude and one for longitude.
We can use this to save our Wrigley data. Add this to ViewController
under the struct.
let wrigleyLocation = cameraInfo( latitude: 41.9471939, longitude: -87.6565108, heading: 41.73)
To save you from looking up the two pizza restaurants, I’ll add them for you. Add this to your code under the wrigleyLocation
.
let CPOGLocation = cameraInfo( latitude: 41.920744, longitude: -87.637542, heading: 338.0) let conniesLocation = cameraInfo( latitude: 41.849294, longitude: -87.6414665, heading: 32.12)
Setting the Map Region
The next step in writing a map app is to set a region. Regions define the visible area based on a center point and a diameter in latitude and longitude. If we were in the real world you think of it as the circle you could visibly see. Since device screens are rectangular, they create a kind of rectangle and set scaling for the map like this.
I said sort of because this is not planar geometry, it’s the spherical geometry of the planet. This region has a type of MKCoordinateRegion
There is an intializer to get this region, but it uses the differences in map coordinates to define the region. The easier one to use for our purposes is the function MKCoordinateRegionMakeWithDistance
which takes the three parameters in the illustration above.
We’ll use this function to get the region defined by a radius from the circle, then assign it to our map. Add this to the code as an instance method:
//MARK: Instance methods func setRegionForLocation( location:CLLocationCoordinate2D, spanRadius:Double, animated:Bool) { let span = 2.0 * spanRadius let region = MKCoordinateRegionMakeWithDistance(location, span, span) mapView.setRegion(region, animated: animated) }
We set the region in our map view with the setRegion
method. Since it can be animated we included a parameter in our function to animate the region change. Add this to viewDidload
:
setRegionForLocation( wrigleyLocation.location, spanRadius: 150, animated: false)
When we start our application, we’ll start the application with a radius of 150 meters. Build and run.
There’s the stadium in the upper right. While most people know about the Chicago Cubs, they don’t know about the other team that used to play there, founded by a guy who missed the boat. In 1915 this guy ran late and missed the boat for his company picnic. The boat, the Eastland capsized in the Chicago River at the Clark Street bridge killing 855 people.
Five years later, this guy would co-found American football’s professional league the NFL. George Halas’ team started playing in Wrigley field in 1922, deriving their name- The Chicago Bears – from the baseball team.
Using Cameras
Besides Papa Bear Halas’ origin story, what you might not know is this is a 3d map. You are just looking at it from overhead. In MapKit we use cameras to look at 3d maps. It allows us to change the angle and perspective we are looking at the object. Change the flyoverMap
action to this:
@IBAction func flyoverMap(sender: UIButton) { let camera = MKMapCamera() camera.centerCoordinate = mapView.centerCoordinate camera.pitch = 80.0 camera.altitude = 100.0 camera.heading = 45.0 mapView.setCamera(camera, animated: true)
The camera
property of MapKit is the view we see of the map. It has four properties, which we set all of them in this method. The centercoordinate
is a coordinate the camera centers its view. pitch
is the angle up or down of the camera. altitude
is how high in the air is the camera. heading
is the compass direction the camera faces. We set this camera 80 degrees from vertical, 100 meters in the air with a heading northeast. Build and run. Tap the Flyover button and you get the following in the simulator.
If you run on a phone instead of a simulator, you get a more robust image, with 3d buildings.
While you can set all three camera properties on your own, you can also use a few methods as well. Change the gotoCPOG
action to this:
@IBAction func gotoCPOG(sender: UIButton) { let camera = MKMapCamera( lookingAtCenterCoordinate: CPOGLocation.location, fromDistance: 0.01, pitch: 90, heading: CPOGLocation.heading) mapView.setCamera(camera, animated: true) }
Here we use the initializer MKMapCamera:lookingAtCenterCoordinate:fromDistance:pitch:heading:
method. We use the CPOGLocation.heading
to get the heading. This method just takes all our properties and makes a camera that is 1 centimeter from the location, pitching the camera at the horizon, and setting the heading according to our constant . This should be close to a human’s eye view.
This location is in front of Chicago Pizza and Oven Grinder, the inventors of the bowl pizza. Ingredients are placed in a ceramic bowl and the crust is placed over the top of the bowl. The bowl is baked and then inverted, making a pizza. There’s something else about this location, but build and run first. Click The CPOG button. On the simulator you get this:
On a phone, you’ll find the iPhone is more robust with graphics than the simulator.:
First one more bit of Chicago history. You’ll notice a inset from the buildings I marked with a red arrow.
There’s a gap between the buildings that’s now a parking lot. There was a garage there once. This was the site of the infamous St. Valentines Day Massacre. The blue arrow is Pizza and Oven Grinder at 2121 N. Clark, and legend has it Jack McGurn, Al Capone’s lieutenant rented a room on the second floor a few weeks before the massacre to scope out the garage.
Okay enough history. You’re probably noticing the big software problem. We should be getting a human’s eye view pf the street. Instead we get a pigeon. This is one of the problems with Apple Maps in 3D: there is a minimum altitude that’s about 30 meters. Apple didn’t call it flyover for nothing. On a phone if you two-finger drag up, and you notice the display does not want to pitch any more.
2D Views Revisited
If the code gets a value that makes no sense from a 3Dview, it places a 2D view instead. We can see this with another way of positioning the camera. This method takes a center coordinate, a coordinate for the camera and an altitude. It finds the pitch by doing math to the eye coordinate and altitude. Add this code
@IBAction func gotoWrigley(sender: UIButton) { let camera = MKMapCamera( lookingAtCenterCoordinate: wrigleyLocation.location, fromEyeCoordinate: wrigleyLocation.location, eyeAltitude: 200) mapView.setCamera(camera, animated: true) }
This code uses the same two coordinates for the location. With the same two coordinates, MapKit makes the camera into a overhead camera. Build and run, then press the Wrigley button
All we’ve done is zoom in on our Wrigley coordinates. Change the action to this:
@IBAction func gotoWrigley(sender: UIButton) { var eyeCoordinate = wrigleyLocation.location eyeCoordinate.latitude += 0.005 eyeCoordinate.longitude += 0.005 let camera = MKMapCamera( lookingAtCenterCoordinate: wrigleyLocation.location, fromEyeCoordinate: eyeCoordinate, eyeAltitude: 50) mapView.setCamera(camera, animated: true) }
We increased our eye coordinate to be 0.005 degrees north and 0.005 degrees west of the center coordinate. Build and run. When we tap the Wrigley button now, we get this in the simulator:
We’ve moved around to the northeast corner of the ballpark. Using this method, the heading will always be looking at the center coordinate, in this case Clark and Addison.
Another way to show a 2Dmap by camera is set the pitch
to 0. Add this code:
@IBAction func gotoConnies(sender: UIButton) { let camera = MKMapCamera( lookingAtCenterCoordinate: conniesLocation.location, fromDistance: 1300, pitch: 0, heading: 0.0) mapView.setCamera(camera, animated: true) }
When you build, run and try the Connie’s button, you get this:
Connie’s Pizza marks mile 21 of the Chicago Marathon, one of the flattest courses and oddly the highest average elevation (590m) of the six major league marathons. If you want to set world records for running 26.2 miles or get that Boston Qualifier, this is the race to do it, and many do.
You can of course set the map view’s camera directly. Add this code to show a 2D map of any point, at an attitude of 1000 meters.
@IBAction func overheadMap(sender: UIButton) { mapView.camera.pitch = 0.0 mapView.camera.altitude = 1000.0 }
Build and run. First show CPOG as a 3D map, then click the 2D Map button.
Clark is a diagonal street heading northwest out of Chicago. It may be a good idea to set heading
to 0 and show north as up. Change the code to this
@IBAction func overheadMap(sender: UIButton) { mapView.camera.pitch = 0.0 mapView.camera.altitude = 1000.0 mapView.camera.heading = 0.0 }
Now you get something that makes more sense as a static map on a phone:
Using Satellite Imagery
In MapKit there are three types of maps: Standard, Satellite and Hybrid. Standard is the default type we’ve been using already. Satellite gives satellite imagery, but no road information. Hybrid combines the road information of standard with the satellite image.
We control the map type with the maptype
property. Add this to the gotoConnies
action.
mapView.mapType = .Satellite
Now run the app, and tap the Connie‘s button. you get a satellite overhead map.
Change .Satellite
to .Hybrid
and you get this
You can easily see both the pizza icon for my favorite pizza in Chicago and the south branch of the Chicago river in this photo. The south branch of the Chicago river is part of one of the marvels of modern engineering: it flows backwards from its natural course. The river flow project, completed in 1900, reverses the river flow so sewage flows down to the Mississippi River entering not far from St. Louis instead of into Chicago’s water supply of Lake Michigan. Just In case you are worried, Chicago now cleans their water before they do that today.
Satellite and Flyover
That’s however is not the whole story. To use flyover 3d on a satellite or hybrid map there are two more types: .SatelliteFlyover
and .HybridFlyover
. Change gotoWrigley
, to this
@IBAction func gotoWrigley(sender: UIButton) { var eyeCoordinate = wrigleyLocation.location eyeCoordinate.latitude += 0.004 eyeCoordinate.longitude += 0.004 let camera = MKMapCamera( lookingAtCenterCoordinate: wrigleyLocation.location, fromEyeCoordinate: eyeCoordinate, eyeAltitude: 50) mapView.mapType = .HybridFlyover mapView.setCamera(camera, animated: true) }
I moved in the coordinates a bit more to get a better look at the ballpark. Build and run. Tap the Wrigley button. You get this on a phone:
So you can experiment with the different types and what they will do, let’s add some code to toggle the type. Change the toggleMapType
to this
@IBAction func toggleMapType(sender: UIButton) { let title = sender.titleLabel?.text switch title!{ case "Satellite": mapView.mapType = .Satellite sender.setTitle("Hybrid", forState: .Normal) case "Hybrid": mapView.mapType = .Hybrid sender.setTitle("Standard", forState: .Normal) case "Standard": mapView.mapType = .Standard sender.setTitle("Satellite", forState: .Normal) default: mapView.mapType = .Standard sender.setTitle("Satellite", forState: .Normal) } }
We use a switch
statement from the titleLabel.text
of the button to toggle between the types. These are the 2d types for .Satellite
and .Hybrid
. We need a little code to to use the 3d types when we turn on flyover. Change the flyoverMap
action to this.
@IBAction func flyoverMap(sender: UIButton) { //change to the correct type for flyover switch mapView.mapType{ case .Satellite: mapView.mapType = .SatelliteFlyover case .Hybrid: mapView.mapType = .HybridFlyover case .Standard: mapView.mapType = .Standard default: break } let camera = MKMapCamera() camera.centerCoordinate = mapView.centerCoordinate camera.pitch = 80.0 camera.altitude = 100.0 camera.heading = 45.0 mapView.setCamera(camera, animated: true) }
Build and run. Try a few combinations of the buttons. Here’s a one minute video if you don’t have a phone handy.
The time to render some of these images is rather long. There’s a lot of processing and data transmission. Each location changes the region dramatically, so everything has to load over again. The rendering time is short only for the .Standard
flyover version. Keep this in mind as you are using these types.
Toggling features
While .Satellite
tuns off all the features like attractions, rendering buildings and the like, there are a few properties you can use in the .Standard
mode to control these. Change toggleMapFeatures
to this
@IBAction func toggleMapFeatures(sender: UIButton) { let flag = !mapView.showsBuildings mapView.showsBuildings = flag mapView.showsScale = flag mapView.showsCompass = flag mapView.showsTraffic = flag }
Play with this a little. Go to Connie’s Pizza and set to .Standard
Mode. Tap Clean Map. Everything but road names disappears.
Tap Busy Map. We get back our attractions, plus the scale and traffic.
You’ll notice the compass doesn’t show. When the .heading
is 0. there is no compass. If the user rotates the map, then the compass will show.
Tap Flyover and we get the 3d view with buildings
Tap Clean Map and we get just the roads and river.
Play around with these. You may find some of the combinations don’t work. Some of the mapType
s set and use these features in specific ways. Check the MKMapView Class Reference for more information.
You can display a lot of information through a map view. We’ve seen that coding a map in 2d or 3d is not that difficult as long as you have some coordinate data. We’ve also seen that Apple provides a lot of attractions such as restaurants and sports stadiums for you. We have not yet added our own locations and features, known as annotations and overlays. In our next lesson, we’ll take a 2d map and annotate it with some data.
The Whole Code
// // ViewController.swift // MyMapKitDemo // // Created by Steven Lipton on 5/4/16. // Copyright © 2016 MakeAppPie.Com. All rights reserved. // import UIKit import MapKit class ViewController: UIViewController { //MARK: Location constants struct cameraInfo{ var location = CLLocationCoordinate2D() var heading = CLLocationDirection() init(latitude:CLLocationDegrees,longitude:CLLocationDegrees,heading:CLLocationDirection){ self.location = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) self.heading = heading } } let wrigleyLocation = cameraInfo(latitude: 41.9471939, longitude: -87.6565108, heading: 41.73) let CPOGLocation = cameraInfo(latitude: 41.920744, longitude: -87.637542, heading: 338.0) let conniesLocation = cameraInfo(latitude: 41.849294, longitude: -87.6414665, heading: 32.12) //MARK: Properties and Outlets @IBOutlet weak var mapView: MKMapView! //MARK: - Actions //MARK: Location actions @IBAction func gotoCPOG(sender: UIButton) { let camera = MKMapCamera(lookingAtCenterCoordinate: CPOGLocation.location, fromDistance: 0.01, pitch: 90, heading: CPOGLocation.heading) mapView.setCamera(camera, animated: true) } @IBAction func gotoWrigley(sender: UIButton) { var eyeCoordinate = wrigleyLocation.location eyeCoordinate.latitude += 0.004 eyeCoordinate.longitude += 0.004 let camera = MKMapCamera(lookingAtCenterCoordinate: wrigleyLocation.location, fromEyeCoordinate: eyeCoordinate, eyeAltitude: 50) mapView.mapType = .HybridFlyover mapView.setCamera(camera, animated: true) } @IBAction func gotoConnies(sender: UIButton) { let camera = MKMapCamera(lookingAtCenterCoordinate: conniesLocation.location, fromDistance: 1300, pitch: 0, heading: 0.0) mapView.mapType = .Hybrid mapView.setCamera(camera, animated: true) } //MARK: Appearance actions @IBAction func toggleMapType(sender: UIButton) { let title = sender.titleLabel?.text switch title!{ case "Satellite": mapView.mapType = .Satellite sender.setTitle("Hybrid", forState: .Normal) case "Hybrid": mapView.mapType = .Hybrid sender.setTitle("Standard", forState: .Normal) case "Standard": mapView.mapType = .Standard sender.setTitle("Satellite", forState: .Normal) default: mapView.mapType = .Standard sender.setTitle("Sat Fly", forState: .Normal) } } @IBAction func overheadMap(sender: UIButton) { mapView.camera.pitch = 0.0 mapView.camera.altitude = 1000.0 mapView.camera.heading = 0.0 } @IBAction func flyoverMap(sender: UIButton) { switch mapView.mapType{ case .Satellite: mapView.mapType = .SatelliteFlyover case .Hybrid: mapView.mapType = .HybridFlyover case .Standard: mapView.mapType = .Standard break default: break } let camera = MKMapCamera() camera.centerCoordinate = mapView.centerCoordinate camera.pitch = 80.0 camera.altitude = 100.0 mapView.setCamera(camera, animated: true) } @IBAction func toggleMapFeatures(sender: UIButton) { let flag = !mapView.showsBuildings mapView.showsBuildings = flag mapView.showsScale = flag mapView.showsCompass = flag mapView.showsTraffic = flag mapView.showsPointsOfInterest = flag if flag { sender.setTitle("Clean Map", forState: .Normal) } else { sender.setTitle("Busy Map", forState: .Normal) } } //MARK: Instance methods func setRegionForLocation(location:CLLocationCoordinate2D,spanRadius:Double,animated:Bool){ let span = 2.0 * spanRadius let region = MKCoordinateRegionMakeWithDistance(location, span, span) mapView.setRegion(region, animated: animated) } //MARK: Life Cycle override func viewDidLoad() { super.viewDidLoad() setRegionForLocation(wrigleyLocation.location, spanRadius: 150,animated: false) // Do any additional setup after loading the view, typically from a nib. } }
Leave a Reply