This week we’re gonna check out some of the mapping functionality available in iOS 7. If you haven’t heard by now, developers now have nearly full access to 3D mapping API’s. Now, you can show you’re own 3D maps.
Assumptions
- You’re a person
- You’re rockin Xcode 5 and the iOS 7 SDK, and you know how to use Xcode and interface builder
- You can create actions and outlets
- If bullet points 2 & 3 intimidate you, go check out the quick start guide here
Setting up the Interface
To start with, you’ll want to create a single view application. Call it whatever you want. Since we’ll be using maps here, you will want to add the mapkit framework. you can do this from the general tab with the root node selected from the project navigator. Figure 1 should help guide you in this endeavor.
Figure 1: Adding the mapkit framework
Open up the Main.storyboard file and drag a Map View from the object library on the bottom right hand side. You will also want to drag a couple of text fields, two labels, and a button on to the screen. Arrange them and name them as shown in Figure 2. You can add the gray text to the text field by changing the “Placeholder text” field from the attributes inspector witht he text field selected.
Figure 2 : The finished interface (for now)
Ok, Now we’re ready to head into the code.
Showing a Coordinate on the Map
We’ll start with the simplest case, and that’s just adding a coordinate to the map. Start by creating a outlets for the MKMapView, and the two text fields. Name them “mapView”,”latTextField”, and “longTextField” respectively.
Create a new action for the “GO!” button and give it a name of “changeMapLocation”.
when completed, the interface section should look like Code Chunk 1. Notice how we imported the mapkit framework and the UITextFieldDelegate decleration. This will be needed later to dismiss the keyboard and of course make the map view work.
Code Chunk 1: The starting interface in the ViewController.m file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 #import "ViewController.h"
#import <MapKit/MapKit.h>
@interface ViewController () <UITextFieldDelegate>
@property (weak, nonatomic) IBOutlet MKMapView *mapView;
@property (weak, nonatomic) IBOutlet UITextField *latTextField;
@property (weak, nonatomic) IBOutlet UITextField *longTextField;
@end
@implementation ViewController
//...This bit is ommitted, but there is actually some code here.
- (IBAction)changeMapLocation:(id)sender
{
//TODO
}
@end
Next we’ll fill in the viewDidLoad method so that the map will load with a not-so-random location of my choosing. That is, Breckenridge Colorado (39.4864 N, 106.0436 W). In iOS, south and west are designated by negative numbers, so this coordinate actually becomes (39.4864,-106.0436).
Code Chunk 2, shows how to go to this location region on the map. Here we create a 2D coordinate, then we set the map to the region that is 10000 meters by 10000 meters around the center coordinate we provided. We also added some optional controls that allow the user to zoom and scroll around the map. We’re also setting the view controller as the delegate for both of the text fields. this will make sure the view controller can dismiss the keyboard.
Code Chunk 2: Implementing the viewDidLoad method
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 - (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//setting ViewController (self) class as delegate
self.latTextField.delegate = self;
self.longTextField.delegate = self;
// Optional Controls
self.mapView.zoomEnabled = YES;
self.mapView.scrollEnabled = YES;
//set up initial location
CLLocationCoordinate2D breckenridgeLocation = CLLocationCoordinate2DMake(39.4864, -106.0436);
//self.mapView.centerCoordinate = breckenridgeLocation;
self.mapView.region = MKCoordinateRegionMakeWithDistance(breckenridgeLocation, 10000, 10000);
}
Now if you simulate the app, you will be presented with a map showing Brekenridge at the center of it. As Figure 3 shows.
Figure 3: The Simulated App with the starting location
Well that was easy enough. Now lets fill in the action stub so we can change the map location with coordinates provided. You may also notice that if you click the text fields, you can’t get rid of them. We will address this soon enough. For now, Fill out the changeMapLocation stub as shown in Code Chunk 3.
Code Chunk 3: Implementing the changeMapLocation: method
1
2
3
4
5
6
7
8
9 - (IBAction)changeMapLocation:(id)sender
{
double latFloat = [self.latTextField.text doubleValue];
double longFloat = [self.longTextField.text doubleValue];
CLLocationCoordinate2D newLocation = CLLocationCoordinate2DMake(latFloat, longFloat);
self.mapView.region = MKCoordinateRegionMakeWithDistance(newLocation, 10000, 10000);
}
Ok, in order to make the keyboard go away properly, we’ll need to implement the textFieldShouldReturn: delegate method. This is shown in Code Chunk 4.
Code Chunk 4: Adding the textFieldShouldReturn: delegate method
1
2
3
4
5 - (BOOL)textFieldShouldReturn:(UITextField *)textField
{
[textField resignFirstResponder];
return YES;
}
Now go back to the storyboard and select both of the text fields by command clicking them. From the attributes inspector change the Keyboard dropdown to “Numbers and Punctuation” and the Return Key dropdown to “Done” as shown in Figure 4.
Figure 4: Changing the keyboard type and return key
Now if you run the application, you can enter a value (assuming its a valid lattitude and longitude), and the map will show that new location. For this example, we aren’t error checking the text fields, but in a real application you should probably do that.
Ok,Now that you have a basis. lets move on to 3D mapping.
Maps in 3D!
When using 3D mapping you have to specify a camera. This will serve as the point of view you are observing a point on a map from. A camera needs 4 pieces of information:
- center coordinate – The location on the ground that you want to focus on
- pitch – the angle of the camera, where 0 is looking straight down
- altitude – how high up the camera is
- heading – what direction the camera is looking , where 90 degress is north
Now we could just create a MKMapCamera and add it to the mapView like so:
1
2
3
4
5
6
7
8
9
10
11
12 Create a new MKMapCamera object
MKMapCamera *mapCamera = [[MKMapCamera alloc] init];
//set MKMapCamera properties
mapCamera.centerCoordinate = CLLocationCoordinate2DMake(lat,long);
mapCamera.pitch = pitch;
mapCamera.altitude = altitude;
mapCamera.heading = heading;
//Set MKmapView camera property
self.mapView.camera = mapCamera;
But, There is an easier way using the MKMapCamera cameraLookingAtCenterCoordinate:fromEyeCoordiante:eyeAltitude: class method. Using this method you just need to know where you want to look, and where you want your camera to be above the ground and at what location. The heading and pitch are then calculated for you.
So start by modifying the viewDidLoad method to create your initial view. Since, the 3D view doesn’t work when using satallite and hybrid, the effect is not very neat unless you have big building to look at. So, This time we’ll take a look at the statue of liberty.
The new view did load method looks like Code Chunk 5. Here, we just allow the user to move all around first. then we create a ground and eye coordinate.
Code Chunk 5: Updating the viewDidLoad method for 3D
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 - (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
self.latTextField.delegate = self;
self.longTextField.delegate = self;
self.mapView.delegate = self;
//Set a few MKMapView Properties to allow pitch, building view, points of interest, and zooming.
self.mapView.pitchEnabled = YES;
self.mapView.showsBuildings = YES;
self.mapView.showsPointsOfInterest = YES;
self.mapView.zoomEnabled = YES;
self.mapView.scrollEnabled = YES;
self.mapView.zoomEnabled = YES;
//set up initial location
CLLocationCoordinate2D ground = CLLocationCoordinate2DMake(40.6892, -74.0444);
CLLocationCoordinate2D eye = CLLocationCoordinate2DMake(40.6892, -74.0442);
MKMapCamera *mapCamera = [MKMapCamera cameraLookingAtCenterCoordinate:ground
fromEyeCoordinate:eye
eyeAltitude:50];
self.mapView.camera = mapCamera;
}
For now the text fields and the “GO!” button won’t work, but if you run the application now you should see a nice view of the statue of liberty. While you can do pitch in the simulator, you need an actuall device to see the 3D buildings. This device also needs to be an iPhone 4S and above.
Implementing the Flyover
Ok, Now we’re going to embelish the app just a bit. First you’ll want to add a new button with an action titled setStartCoordinates. Title it whatever you want, I gave the button the title “StartPt”. Your interface should now look like Figure 5.
Now you will create two new properties in the interface as shown in Code Chunk 6. These properties will hold the starting coordinates for the flyover.
Code Chunk 6: Adding new properties to hold start coordinates
The setStartingPoint method will be pretty straight forward, All you will need to do is set the textField values to the new properties we just created as shown in Code Chunk 7 and then create a new camera for these lattitude and longitude values.
Code Chunk 7: Implementing the setStartingPoint action method
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 - (IBAction)setStartingPoint:(id)sender
{
self.startLat = self.latTextField.text;
self.startLong = self.longTextField.text;
double startLatFloat = [self.startLat doubleValue];
double startLongFloat = [self.startLong doubleValue];
CLLocationCoordinate2D ground = CLLocationCoordinate2DMake(startLatFloat, startLongFloat);
CLLocationCoordinate2D eye = CLLocationCoordinate2DMake(startLatFloat, startLongFloat+.020);
MKMapCamera *mapCamera = [MKMapCamera cameraLookingAtCenterCoordinate:ground
fromEyeCoordinate:eye
eyeAltitude:200];
self.mapView.camera = mapCamera;
}
Now all we need to do is update the changeMapLocaton method, This will take the values currently in the text fields, and createa new camera with them and then animate to that new camera’s location. Because a map view is a type of view, we can use a view animation. Now, because the data is being loaded behind the scenes, you should not cover very large distances when using this animation or it will look choppy. If used appropriately, you will get a nice fly over effect.
Code Chunk 8: Implementing the ne changeMapLocation method to account for 3D
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 - (IBAction)changeMapLocation:(id)sender
{
double latFloat = [self.latTextField.text doubleValue];
double longFloat = [self.longTextField.text doubleValue];
CLLocationCoordinate2D ground = CLLocationCoordinate2DMake(latFloat, longFloat);
CLLocationCoordinate2D eye = CLLocationCoordinate2DMake(latFloat, longFloat+.020);
MKMapCamera *mapCamera = [MKMapCamera cameraLookingAtCenterCoordinate:ground
fromEyeCoordinate:eye
eyeAltitude:700];
[UIView animateWithDuration:25.0 animations:^{
self.mapView.camera = mapCamera;
}];
}
Now if you build and run you can reset your starting coordinate by editing the text field values and clicking the start point button, then change the text field values to a new coordinate and press the “GO!” button to go off to the new coordinate. You may want to play with your altitude a bit, if your camera is too far above buildings they wont show up. Figure 6 shows the app with the final coordinates at the empire state building and an altitude of 700 meters.
That’s it for this recipe! while it sucks this same functionality can’t be carried over into satallite view, it is still pretty neat. I encourage you to play with the values, or even define pitch, alitude, and camera heading without using the convenience method we used here.