本文翻译自:
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#import #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让你可以访问这些数据。 这个 可以在网上查看,这里我们就不详细介绍了,我们从一个高层次上解析我们的计划:
我们感兴趣的数据集是 ,通过这个链接,你可以查看详细信息。
为了查询这个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# import @interface MyLocation : NSObject { 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 (idannotation 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{
添加一个新的方法:
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id)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 methodMBProgressHUD *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];
源码
这里是一个完整的