iOS上的MapKit
本文翻译自:MapKit on iOS
MapKit是一个非常整洁的API,可在iPhone上很容易地显示地图,跳到指定坐标,绘制位置,甚至在顶部画出路线和其他形状。
我写这篇文章的原因是,上个月我在Baltimore参加一个叫做 Civic Hack Day的活动,我在那里使用 MapKit来显示一些公共的数据,例如一些地方的位置,犯罪率,逮捕和公交车路线。我觉得这非常有趣,我想其他人也会有兴趣想知道这是如何做到的,或者想在自己的家乡使用相同的技术!
在这篇文章中,我们将创建一个app,可以在Baltimore城市中随意缩放到一个感兴趣的地方。这个app将请求一个Baltimore 的web 服务来获取在该区域附近最近的逮捕数据,然后在地图上标识出来。
在这个过程中,你将学习如何添加一个MapKit 地图到你的app中,缩放到一个指定的地方,通过Socrata API来查询和获取有用的政府数据,创建自定义的地图标注等等!
这个项目使用ARC,Storyboard!
开始
首先创建一个单视图的iOS应用程序,命名为 ArrestsPlotter。确定勾选使用Storyboard和ARC。
拖动一个Map View到视图控制器上,还有一个底部的toolbar,命名为 Refresh:
接着选择Map View,然后在设置面板中,如下设置:
在运行你的应用前,添加MapKit framework 和 CoreLocation framework到你的工程中:
现在运行你的app,将看到以下的内容:
看起来还行,但是我们不想一开始就显示整个美国,我们想指定显示某个区域:
设置可视区域
导航到 ViewController.h 文件,用下面的代码取代原来的:
#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>
#define METERS_PER_MILE 1609.344
@interface ViewController : UIViewController{
}
@end
上面只是简单地添加了一个常量,指定每英里多少米。
下面把Storyboard中的MapView 链接到ViewController.m 文件中,添加一个它的引用:
接下来,我们在ViewController.m文件 的 viewWillAppear: 方法中,把MapView 缩放到一个初始位置:
- (void)viewWillAppear:(BOOL)animated {
// 1
CLLocationCoordinate2D zoomLocation;
zoomLocation.latitude = 39.281516;
zoomLocation.longitude= -76.580806;
// 2
MKCoordinateRegion viewRegion = MKCoordinateRegionMakeWithDistance(zoomLocation,
0.5*METERS_PER_MILE, 0.5*METERS_PER_MILE);
// 3
MKCoordinateRegion adjustedRegion = [_mapView regionThatFits:viewRegion];
// 4
[_mapView setRegion:adjustedRegion animated:YES];
}
这里有很多工作需要做,我一点点说明:
-
挑选出放大的区域。这里我们选择了对于 BPD Arrests API 服务比较完善的地方, Baltimore。
-
当你想告诉地图显示什么时,不能只提供经纬度,你还需要指定一个区域来显示。这里我们指定以用户位置为中心的,宽和长为半英里的区域。
-
在给map view设置一个区域之前,你需要把这个区域缩放到刚好充满屏幕的大小,这个map view提供了一个帮助方法
regionThatFits:
来实现这个功能。 -
最后,告诉map view显示这个区域,map view会自动通过一个动画为缩放当前的视图到指定的显示区域,不需要额外的代码!
再次运行这个app,我们看到缩放到想要的区域了:
获取逮捕信息
下一步就是在地图上显示有趣的逮捕信息。
在Baltimore,我们非常幸运,因为这城市正努力把所有的城市数据同步到网络上, 通过 OpenBaltimore项目。
我们将在这篇文章中使用它提供的数据集。你在阅读完这篇文章后,可以去了解一下你的城市是否也提供类似的在线服务。
总之,Baltimore 城市的数据是通过一个 Socrata公司提供的,这个公司提供了一个API让你可以访问这些数据。 这个 Socrata API documentation 可以在网上查看,这里我们就不详细介绍了,我们从一个高层次上解析我们的计划:
-
我们感兴趣的数据集是 BPD Arrests,通过这个链接,你可以查看详细信息。
-
为了查询这个API,你需要POST一个JSON格式的查询数据到提供的Socrata API。结果以JSON格式返回。
-
我们需要使用的数据在稍微后面,所以我们把这个查询数据保存到一个文本中,方便以后读取和修改。
-
为了节省时间,我们使用ASIHttRequest 网络请求库来发送数据到 web service,SBJSON 库用来解析json数据。
添加库
这个功能中,我们需要
-
SBJSON
-
ASIHTTPRequest
-
MBProgressHUD
你需要在GitHub上下载这些开源的库,然后添加到我们的工程中。同时,还要引入 CFNetwork framework, MobileCoreServices frame,SystemConfiguration framework和 libz.1.1.3.dylib。
注意,ASIHTTPRequest 使用非 ARC,你需要指定一个编译选项: -fno-objc-arc:
获取逮捕数据
首先,从这里下载查询字符串的样本,把解压缩后的command.json 导入工程中。
接下载,我们实现Refresh 按钮的响应事件:
// At top of file
#import "ASIHTTPRequest.h"
// Replace refreshTapped as follows
- (IBAction)refreshTapped:(id)sender {
// 1
MKCoordinateRegion mapRegion = [_mapView region];
CLLocationCoordinate2D centerLocation = mapRegion.center;
// 2
NSString *jsonFile = [[NSBundle mainBundle] pathForResource:@"command" ofType:@"json"];
NSString *formatString = [NSString stringWithContentsOfFile:jsonFile encoding:NSUTF8StringEncoding error:nil];
NSString *json = [NSString stringWithFormat:formatString,
centerLocation.latitude, centerLocation.longitude, 0.5*METERS_PER_MILE];
// 3
NSURL *url = [NSURL URLWithString:@"http://data.baltimorecity.gov/api/views/INLINE/rows.json?method=index"];
// 4
ASIHTTPRequest *_request = [ASIHTTPRequest requestWithURL:url];
__weak ASIHTTPRequest *request = _request;
request.requestMethod = @"POST";
[request addRequestHeader:@"Content-Type" value:@"application/json"];
[request appendPostData:[json dataUsingEncoding:NSUTF8StringEncoding]];
// 5
[request setDelegate:self];
[request setCompletionBlock:^{
NSString *responseString = [request responseString];
NSLog(@"Response: %@", responseString);
}];
[request setFailedBlock:^{
NSError *error = [request error];
NSLog(@"Error: %@", error.localizedDescription);
}];
// 6
[request startAsynchronous];
}
下面解析:
-
获取地图中心位置的经纬度。
-
读取我们的查询字符串数据command.json。
-
创建一个请求访问链接URL
-
使用ASIHTTPRequest post我们的数据。
-
设置ASIHTTPRequest 的两个block,完成block和失败block。
-
开始网络请求。
运行我们的应用,点击Refresh 按钮,会在控制台看到如下的返回数据:
绘制信息
为了创建和使用自定义地图标注,我们有三步:
-
创建一个实现了MKAnnotation 协议的类,这意味着需要返回一个标题,子标题和坐标。
-
对于每一个你想在地图上标注的地点,你可以创建一个上面类的实例,然后通过
addAnnotation
方法把这个标注添加到mapView上。 -
把视图控制器作为map view 的委托,对于你添加的每个标注,都会调用一个委托方法:
mapView:viewForAnnotation
。你需要在这个委托方法中返回一个MKAnnotationView的子类,用于展示标注的虚拟标志。我们这里将使用一个内建的MKPinAnnotationView。
下面是实现:
# import <Foundation/Foundation.h>
# import <MapKit/MapKit.h>
@interface MyLocation : NSObject <MKAnnotation> { NSString *\_name; NSString *\_address; CLLocationCoordinate2D _coordinate; }
@property (copy) NSString *name; @property (copy) NSString *address; @property (nonatomic, readonly) CLLocationCoordinate2D coordinate;
- (id)initWithName:(NSString*)name address:(NSString*)address coordinate:(CLLocationCoordinate2D)coordinate;
@end
下面是MyLocation的实现
#import "MyLocation.h"
@implementation MyLocation
@synthesize name = _name;
@synthesize address = _address;
@synthesize coordinate = _coordinate;
- (id)initWithName:(NSString*)name address:(NSString*)address coordinate:(CLLocationCoordinate2D)coordinate {
if ((self = [super init])) {
_name = [name copy];
_address = [address copy];
_coordinate = coordinate;
}
return self;
}
- (NSString *)title {
if ([_name isKindOfClass:[NSNull class]])
return @"Unknown charge";
else
return _name;
}
- (NSString *)subtitle {
return _address;
}
@end
下面是ViewController.m 中添加标注的实现:
// Add to top of file
#import "MyLocation.h"
#import "SBJSON.h"
// Add new method above refreshTapped
- (void)plotCrimePositions:(NSString *)responseString {
for (id<MKAnnotation> annotation in _mapView.annotations) {
[_mapView removeAnnotation:annotation];
}
NSDictionary * root = [responseString JSONValue];
NSArray *data = [root objectForKey:@"data"];
for (NSArray * row in data) {
NSNumber * latitude = [[row objectAtIndex:21]objectAtIndex:1];
NSNumber * longitude = [[row objectAtIndex:21]objectAtIndex:2];
NSString * crimeDescription =[row objectAtIndex:17];
NSString * address = [row objectAtIndex:13];
CLLocationCoordinate2D coordinate;
coordinate.latitude = latitude.doubleValue;
coordinate.longitude = longitude.doubleValue;
MyLocation *annotation = [[MyLocation alloc] initWithName:crimeDescription address:address coordinate:coordinate] ;
[_mapView addAnnotation:annotation];
}
}
// Add new line inside refreshTapped, in the setCompletionBlock, right after logging the response string
[self plotCrimePositions:responseString];
对于第三步,在ViewController中实现 MKMapViewDelegate 协议:
@interface ViewController : UIViewController <MKMapViewDelegate> {
添加一个新的方法:
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation {
static NSString *identifier = @"MyLocation";
if ([annotation isKindOfClass:[MyLocation class]]) {
MKPinAnnotationView *annotationView = (MKPinAnnotationView *) [_mapView
dequeueReusableAnnotationViewWithIdentifier:identifier];
if (annotationView == nil) {
annotationView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation
reuseIdentifier:identifier];
} else {
annotationView.annotation = annotation;
}
annotationView.enabled = YES;
annotationView.canShowCallout = YES;
annotationView.image=[UIImage imageNamed:@"arrest.png"];
//here we use a nice image instead of the default pins
return annotationView;
}
return nil;
}
每次你添加一个标注,上面的方法都会被调用。
运行结果:
添加一个进度指示
网络请求需要时间,我们可以在此显示一个加载进度标识:
// Add at the top of the file
#import "MBProgressHUD.h"
// Add right after [request startAsynchronous] in refreshTapped action method
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
hud.labelText = @"Loading arrests...";
// Add at start of setCompletionBlock and setFailedBlock blocks
[MBProgressHUD hideHUDForView:self.view animated:YES];
源码
这里是一个完整的源码项目