iOS Programming Recipe 30: Using 3D Mapping

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.

Adding a framework

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.

Setting up initial interface

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 () &lt;UITextFieldDelegate&gt;

@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.

Simulated MKMapView 2D region

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.

Setting keyboard type

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.

Finished Interface

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


1
2
3
4
5
6
7
8
9
10
11
12
13
#import "ViewController.h"
#import <MapKit/MapKit.h>
#import <CoreLocation/CoreLocation.h>

@interface ViewController () &lt;UITextFieldDelegate,MKMapViewDelegate&gt;

//...

@property (strong, nonatomic) NSString *startLat;
@property (strong, nonatomic) NSString *startLong;


@end

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.

MKMapview 3D simulation

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.

About Joe Hoffman

I am An Electrical Engineer that spends a lot of my off time doing web development and other programming. Currently I am trying to expand my knowledge with iOS and I find that writing tutorials helps me learn more thoroughly as well as provide some useful info to others.

Comments

  1. I’ve followed your tutorial and have learned a lot. Can you please tell me how I can make a change to a camera by changing properties? All I really want to do is tip the pitch to a value, like the 3D button on the Maps app. The only way I can manage to make this work is to create a new camera with a different pitch, but what I want is to keep the current heading, coords, etc and just change the pitch. I’m new to this, but my next guess is to find a way to store the current properties and make a new map with those and the pitch I want. Thanks for any insight!

    • Joe Hoffman says:

      HI Chris,

      The easiest way to do this is to simply set the mapview camera pitch inside an action of a button. Eg.

      - (IBAction)changePitch:(id)sender
      {
          self.mapView.camera.pitch = 80;
      }

      But, this will simply change the view and refresh, Which may not look as good as animating it. To animate, create a camera property, change the pitch , and simply set the mapview camera to the camera again.

      @interface ViewController () <UITextFieldDelegate,MKMapViewDelegate>
      /...
      @property (strong, nonatomic) MKMapCamera *camera;
      @end
      /...
      - (IBAction)changePitch:(id)sender
      {

          self.camera.pitch = 80;
         
          [UIView animateWithDuration:5.0 animations:^{
             
             
             
              self.mapView.camera = self.camera;
             
          }];
      }

      Of course you’ll need to allocate and initialize your camera property and change the other values elsewhere like the viewDidLoad method.

  2. Thanks, Joe.

    I got your first method to work, but it also zoomed out. I’ll figure that out.

    But I can’t get the second way to work. I think because I didn’t know how to allocate and initialize the camera property as you describe at the end. I’m getting confused between the camera and the mapView (which was actually my first problem. I kept trying to change the camera property when I should have been changing the mapview property). I’m also confused between the camera property and the camera parameter. I appreciate your previous help and I think if I keep working at it, I’ll figure it out.

    I tried doing a few online courses between jumping into this, but I still don’t really get the basics. Thanks!

    • Joe Hoffman says:

      Hi Chris,

      Just keep at it! That’s how you learn. For the basics, I think you should pick up the Big Nerd Ranch iOS programming book. The best book I’ve seen on the subject. Also, Go try out code school if you haven’t already. It’s like 30 bucks a month, but they have some good iOS tutorials that are explained really well. There were also tons of other tutorials for HTML5, Java, ruby on rails and so on if you’re into those things. You can find the links on the resources page.

  3. Somehow I missed the fact that you’re the same guy that wrote the book I’ve been trying to get some help from from the Amazon previews. 🙂 http://www.amazon.com/iOS-Development-Recipes-Problem-Solution-Approach/dp/1430259590

    But I just bought the ebook and I think that will help get me up to speed.

  4. Matthieu PASCAUD says:

    Apple maps has really been improved. Do you think that would be possible to have the earth in 3D and add satellites animation on top of it? Or are we limited to add information on the ground only?

    Thank you.

    • Joe Hoffman says:

      Hi Matthieu,

      If I get what you’re saying, I think your asking if we can zoom way out like google earth, then I don’t think so. This would probably be a more custom solution.

  5. Hi Joe
    Thanks for posting about this subject. Do you have any sense of whether/when 3D control of apple maps allows/will allow 3rd party apps to show the satellite flyovers that are available in the apple maps app for major cities such as New York, London, San Francisco?
    Thanks
    Mike

Speak Your Mind

*

css.php
Privacy Policy