注:本文由BeyondVincent(破船)翻译首发
转载请注明出处:http://blog.csdn.net/beyondvincent
注意: 根据你的经验水平,学习本文,你有两种选择:
- 对MapKit已经熟悉了? 如果你对MapKit已经熟悉了,并且你想要马上学习叠加图层的内容,你可以忽略(或略读)掉前面的内容,直接跳到“What a View”小节—在这里我为你准备了一个启动项目。
- 对MapKit还是一个新手?如果你对MapKit还一无所知,那么请继续往下阅读,我将从最基础的内容开始在程序中添加一个地图!
苹果地图 VS Google 地图
在开始编码之前,我先来说一下关于苹果地图和Google地图的争议。
在iOS开始之初,苹果就提供了一个地图程序,这个地图程序的数据最初是由Google地图 API提供的。而在iOS 6中一切都改变了,苹果打破了与Google之间的关系,发布了自己的地图程序,并且后端数据是由苹果自己提供的。
这对于博客、媒体、用户,甚至你的妈妈都是一个热议话题。有些说苹果已经完成了一个难以置信的工作,并且放弃Google,而选择自身作为地图提供者是一个正确的选择。而有些人则持想法的态度,他们认为这是苹果自从iPhone在2007问世以来,做的最糟糕的一个决定。
现在,如果你使用MapKit那么是在使用苹果地图。如果以前使用过MapKit,你会发现两个版本的API非常相似。
无论你的位置在哪里,在地图上总会有空间来展现更多的信息!因此,本文中你将学到如何使用苹果流行(无论是有名或者臭名昭著)的地图并添加你自己的相关信息。
入门
为了开始学习,先下载starter project,这个工程提供了基本的一个程序,可以在iPhone和iPad上运行,工程里面有一些基本的导航—但是还没有地图!
在starter project中提供的界面包含一个UISegmentedControl控件,用来切换不同的地图类型(稍后即将实现),此外还有有个动作按钮—用来显示一个table画面(里面是一些选项列表),通过这个table中的选项可以控制那种地图特征会被显示出来。通过轻击table中的选项就可以对选项选中或者取消选中。然后轻击Done按钮就可以把这些选项列表隐藏掉。
PVMapOptionsViewController负责管理选项视图,稍后你将看到,在里面定义了一个非常重要的enum。这个类中剩余的代码则超出了本文的介绍范围。不过,如果你希望了解更多关于UITableView的知识,那么可以看一下这里的内容:UITableView tutorials 。
在Xcode中打开这个starter project,编译并运行。我敢打赌,你对目前这个工程有点失望,因为你将看到的如下内容:
starter project非常的简单!如果你希望地图程序能做任何有用的事情,那么你需要为这个工程添加一个地图!
你知道去San Jose的道路吗?—添加一个MapView
现在打开MainStoryboard_iPad.storyboard文件,跟上面的操作步骤一样,添加一个MapView,然后调整一下这个MapView的位置以填满整个view。
现在你如果编译并运行程序的话,程序会crash掉,并且提示如下信息:
*** Terminating app due to uncaught exception 'NSInvalidUnarchiveOperationException', reason: 'Could not instantiate class named MKMapView'
这是因为你还没有把MapKit.framework链接到你的target中!
为了将其链接到target中,在工程导航栏中选择Park View工程,然后选中Park View target。下一步打开Build Phases选项,然后在Link Library With Binaries下面单击+按钮,如下图所示:
在弹出的窗口中搜索MapKit,选中它,然后单击Add将其添加到工程中,如下截图所示:
现在编译并运行程序,可以看到新的地图了!看起来如下截图所示:
如上所示,在程序中添加一个地图并不需要做太多的工作。
在程序里面有一个地图是非常cool的,如果能让地图做一些实际的事情会更cool!:]下一节中,将介绍如何在程序中获得这个MapView,以进行交互。
又长又曲折的道路—连接到你的MapView
要想用MapView做任何事情,你需要做两件事情—将其与一个outlet关联,将view controller注册为MapView的delegate。
但是首先你需要import MapKit头文件。打开PVParkMapViewController.h 并将下面的代码添加到文件的顶部:
#import <MapKit/MapKit.h>
|
下一步,打开MainStoryboard_iPhone.storyboard 文件,并将Assistant Editor打开,让PVParkMapViewController.h 可见.。然后从map view control-drag到下面的第一个属性,如下图所示:
在弹出的画面中,将outlet命名为mapView,,然后单击Connect。
现在你需要为MapView设置delegate。这样做:在MapView上右键单击,会弹出一个context菜单,然后将delegate连接到Map View Controller上,如下图所示:
现在对iPad storyboard做相同的操作 — 将MapView连接到mapView插槽中(这次只需要将其拖拽到已经存在的插槽上即可,不需要创建一个新的),并将view controller设置为MapView的delegate。
现在已经完成了插槽的连接,下面你还需要修改一下PVParkMapViewController头文件的接口声明,让其遵循MKMapViewDelegate协议。
最终PVParkMapViewController.h中的接口声明如下所示:
@interface PVParkMapViewController : UIViewController <MKMapViewDelegate> |
通过上面的操作,我们完成了插槽,delegate,controller的配置。现在可以在地图中添加一些交互了!
我在这里,你不知道如何从这而到那儿 – 与 MKMapView进行交互
虽然地图默认的视图非常好看,但是这对于只关注主题公园(而不是所有的大陆)的人来说,使用起来太广泛了!当程序启动的时候,将公园的地图视图放置在程序的中间非常好。获得某个具体位置的位置信息有许多中方法;可以通过web service获取,也可以将位置信息内置在程序中。
为了简单起见,在本文中,我把公园的位置信息打包放在程序中。下载这个工程的资源(resources for this project),里面有一个名为MagicMountain.plist的文件,包含了公园的信息。
MagicMountain.plist的内容如下:
- <?xml version="1.0" encoding="UTF-8"?>
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
- <plist version="1.0">
- <dict>
- <key>midCoord</key>
- <string>{34.4248,-118.5971}</string>
- <key>overlayTopLeftCoord</key>
- <string>{34.4311,-118.6012}</string>
- <key>overlayTopRightCoord</key>
- <string>{34.4311,-118.5912}</string>
- <key>overlayBottomLeftCoord</key>
- <string>{34.4194,-118.6012}</string>
- <key>boundary</key>
- <array>
- <string>{34.4313,-118.59890}</string>
- <string>{34.4274,-118.60246}</string>
- <string>{34.4268,-118.60181}</string>
- <string>{34.4202,-118.6004}</string>
- <string>{34.42013,-118.59239}</string>
- <string>{34.42049,-118.59051}</string>
- <string>{34.42305,-118.59276}</string>
- <string>{34.42557,-118.59289}</string>
- <string>{34.42739,-118.59171}</string>
- </array>
- </dict>
- </plist>
上面的文件中包含的信息不仅有你现在需要的(将公园放在地图中间);另外,还包含了公园的边界信息—稍后会用到。
文件中所有的信息都是以经度/维度坐标(latitude/longitude coordinates)格式提供的。
把这个文件添加到工程中:通过把这个文件拖拽工程的Park Informatio群组中。
现在你已经有了关于公园的地理位置信息了,下面你应该把这些地理信息模型化为Objective-C对象,以便于在程序中使用。
选择工程中的Models群组,然后选择File > New > File… > Objective-C Class (在Cocoa Touch里面)。将类命名为PVPark,并继承自NSObject。
当新的类创建好之后,将下面的属性和初始化方法添加到PVPark.h:
#import <Foundation/Foundation.h> #import <MapKit/MapKit.h> @interface PVPark : NSObject @property (nonatomic, readonly) CLLocationCoordinate2D *boundary; @property (nonatomic, readonly) NSInteger boundaryPointsCount; @property (nonatomic, readonly) CLLocationCoordinate2D midCoordinate; @property (nonatomic, readonly) CLLocationCoordinate2D overlayTopLeftCoordinate; @property (nonatomic, readonly) CLLocationCoordinate2D overlayTopRightCoordinate; @property (nonatomic, readonly) CLLocationCoordinate2D overlayBottomLeftCoordinate; @property (nonatomic, readonly) CLLocationCoordinate2D overlayBottomRightCoordinate; @property (nonatomic, readonly) MKMapRect overlayBoundingMapRect; @property (nonatomic, strong) NSString *name; - (instancetype)initWithFilename:(NSString *)filename; @end |
这里的许多属性看起来非常相似,它们将引用到上面的plist文件中。
注意初始化方法initWithFileName,需要给这个方法传递包含坐标信息的一个plist文件,以对这个对象进行初始化。
注意:
你可能已经注意到这个初始化方法返回的类型是instancetype,而不是id。 这是LLVM编译中相对教新的内容。更多相关内容可以参考NSHipster。
现在是时候来实现PVPark.m.文件了。这里将添加两个方法。第一个是initWithFileName, 将plist文件中的所有信息读取到头文件中定义好的属性中。如果你做过文件I/O操作,那么这部分将非常的简单。
将下面的代码添加到PVPark.m:
- (instancetype)initWithFilename:(NSString *)filename { self = [super init]; if (self) { NSString *filePath = [[NSBundle mainBundle] pathForResource:filename ofType:@"plist"]; NSDictionary *properties = [NSDictionary dictionaryWithContentsOfFile:filePath]; CGPoint midPoint = CGPointFromString(properties[@"midCoord"]); _midCoordinate = CLLocationCoordinate2DMake(midPoint.x, midPoint.y); CGPoint overlayTopLeftPoint = CGPointFromString(properties[@"overlayTopLeftCoord"]); _overlayTopLeftCoordinate = CLLocationCoordinate2DMake(overlayTopLeftPoint.x, overlayTopLeftPoint.y); CGPoint overlayTopRightPoint = CGPointFromString(properties[@"overlayTopRightCoord"]); _overlayTopRightCoordinate = CLLocationCoordinate2DMake(overlayTopRightPoint.x, overlayTopRightPoint.y); CGPoint overlayBottomLeftPoint = CGPointFromString(properties[@"overlayBottomLeftCoord"]); _overlayBottomLeftCoordinate = CLLocationCoordinate2DMake(overlayBottomLeftPoint.x, overlayBottomLeftPoint.y); NSArray *boundaryPoints = properties[@"boundary"]; _boundaryPointsCount = boundaryPoints.count; _boundary = malloc(sizeof(CLLocationCoordinate2D)*_boundaryPointsCount); for(int i = 0; i < _boundaryPointsCount; i++) { CGPoint p = CGPointFromString(boundaryPoints[i]); _boundary[i] = CLLocationCoordinate2DMake(p.x,p.y); } } return self; } |
在上面的代码中,CLLocationCoordinate2DMake() 利用经度和维度构建一个CLLocationCoordinate2D结构。在MapKit中CLLocationCoordinate2D 结构代表了一个地理位置。
在初始方法中还创建了一个CLLocationCoordinate2D 数组,并将数组的指针设置给_boundary。这非常重要:之后需要将这样的一个指针传递到CLLocationCoordinate2D 结构的数组中。
有一个属性你可能已经注意到我们并没有对其进行初始化—overlayBottomRightCoordinate。而其它三个角(右上、左上和左下)我们都提供了值,但是为什么右下没有提供呢?
原因是通过其它三个点可以计算出最后的这个点—如果对于一个可以计算出来的值,在提供的话就显得有点多余了。
为了计算出右下角的坐标,将下面的代码添加到PVPark.m中:
- (CLLocationCoordinate2D)overlayBottomRightCoordinate { return CLLocationCoordinate2DMake(self.overlayBottomLeftCoordinate.latitude, self.overlayTopRightCoordinate.longitude); } |
这个方法使用左下和右上坐标可以计算出右下坐标,该方法扮演了getter方法。
最后,你需要根据上面读取出来的坐标创建一个边界框。
将下面的代码添加到PVPark.m中:
- (MKMapRect)overlayBoundingMapRect { MKMapPoint topLeft = MKMapPointForCoordinate(self.overlayTopLeftCoordinate); MKMapPoint topRight = MKMapPointForCoordinate(self.overlayTopRightCoordinate); MKMapPoint bottomLeft = MKMapPointForCoordinate(self.overlayBottomLeftCoordinate); return MKMapRectMake(topLeft.x, topLeft.y, fabs(topLeft.x - topRight.x), fabs(topLeft.y - bottomLeft.y)); } |
这个方法将构造出一个MKMapRect ,代表了公园的边界框。它真的只是一个矩形框,用来表示公园有多大(利用上面提供的坐标),并且是集中在公园的中心位置。
现在时时候使用这个新创建的类了。更新一下PVParkMapViewController.m 文件:
import PVPark.h 并在类扩展中添加一个park 属性:
#import "PVPark.h" @interface PVParkMapViewController () @property (nonatomic, strong) PVPark *park; @property (nonatomic, strong) NSMutableArray *selectedOptions; @end |
然后将下面的代码添加到viewDidLoad:
- (void)viewDidLoad { [super viewDidLoad]; self.selectedOptions = [NSMutableArray array]; self.park = [[PVPark alloc] initWithFilename:@"MagicMountain"]; CLLocationDegrees latDelta = self.park.overlayTopLeftCoordinate.latitude - self.park.overlayBottomRightCoordinate.latitude; // think of a span as a tv size, measure from one corner to another MKCoordinateSpan span = MKCoordinateSpanMake(fabsf(latDelta), 0.0); MKCoordinateRegion region = MKCoordinateRegionMake(self.park.midCoordinate, span); self.mapView.region = region; } |
上面的代码使用MagicMountain 属性列表来初始化 park 属性。接着创建了一个维度增量—这个距离表示从park属性的左上坐标到右下坐标之间的距离。
通过利用维度增量来生成一个 MKCoordinateSpan 结构, 这个结构定义了地图区域的跨度。
然后通过MKCoordinateSpan和 midCoordinate 属性 (就是公园边界区域的中心) 来创建一个 MKCoordinateRegion。
然后将MKCoordinateRegion 结构赋值给map view的region属性,用来定位地图的位置。
编译并运行程序,可以看到地图显示的内容是六七魔术山公园的正中心。如下图所示:
Ok! 现在地图已经显示出了公园的正中心,非常帮!但是显示的内容并不能让我们非常兴奋。它只是显示了几个米色空白区域,外加边缘上有几条街道。
如果你以前使用过地图程序,你肯定知道里面的卫星地图看起来非常cool。其实你也可以很容易就能够在程序中使用卫星地图数据!
伙计,我到处都去过啦 – 切换地图的类型
在 PVParkMapViewController.m中的最下面,你可以看到这样一个方法:
- (IBAction)mapTypeChanged:(id)sender { // TODO: Implement } |
代码中的注释内容是 TODO ! :]
在这个方法中需要写一些代码哦。你注意到map view上面的UISegmentedControl,这个UISegmentedControl实际上会调用 mapTypeChanged, 不过,在上面的方法中还没有任何实现!
将下面的代码添加到 mapTypeChanged 方法中:
- (IBAction)mapTypeChanged:(id)sender { switch (self.mapTypeSegmentedControl.selectedSegmentIndex) { case 0: self.mapView.mapType = MKMapTypeStandard; break; case 1: self.mapView.mapType = MKMapTypeHybrid; break; case 2: self.mapView.mapType = MKMapTypeSatellite; break; default: break; } } |
或许你会难以执行—在程序中添加标准、卫星和混合地图类型是如此的简单—如上代码,只需要根据 mapTypeSegmentedControl.selectedSegmentIndex的索引值进行切换即可。很简单吧?
编译并运行程序,通过屏幕顶部的UISegmentedControl,你就可以切换地图的类型了,如下所示:
虽然卫星视图比标准视图内容更丰富,但是它对于公园的有人仍然不够用。地图里面没有任何的提示 — 你的用户如何找到公园里面的内容呢?
一个很实用的方法就是在MapView上放置一个UIView,不过你可以使用更好的一个方法 — 通过 MKOverlayView 来做相关的处理!
是什么样的一个View – 关于Overlay View
注意: 如果你跳过了本文前面部分的内容,你可以在这里下载到项目代码(this starter project) 同时你还需要下载项目的资源文件(resources for this project),并将其添加到项目中。
在开始创建自己的view之前,我们先来看两个类 –MKOverlay 和 MKOverlayView.
MKOverlay这个类会告诉MapKitis你想在哪里绘制叠加图层。使用它,有三个步骤:
- 创建一个你自己的类,并实现 MKOverlay 协议, 这个协议有两个required属性: coordinate 和 boundingMapRect。 这两个属性定义了叠加图层在地图中的位置,以及图层的大小。
- 在这个类中为希望显示的每个区域创建一个实例,在程序中,可以创建一个过山车图层和餐厅图层。
- 最后,通过调用下面的代码,将图层添加到你的Map View中:
[self.mapView addOverlay:overlay]; |
现在MapView以及知道将图层显示到什么地方了 — 但是,它是如何知道每个区域显示的内容呢?
进入 MKOverlayView. 你创建一个该类的子类,并设置一下你想要显示的内容。在这里的程序中,你只需要绘制一个关于过山车或餐厅的图片即可。
MKOverlayView 实际上是继承自UIView的。不过MKOverlayView 是特殊的UIView,你不能将其直接添加到MKMapView中。这个view是MapKit框架希望你提供的。你提供了这个view之后,这个view将以图层的方式渲染在地图的上方。
还记得如何给MapView设置delegate - 以及在本文中将view controller设置为它的delegate吗?OK,你实现的有个delegate方法会返回一个叠加图层:
- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id<MKOverlay>)overlay |
当map view发现在其显示区域内有一个 MKOverlay 对象时,会调用上面这个方法。
概括的说,你不要直接将MKOverlayView添加到map view中; 而是告诉map关于MKOverlays如何显示,以及在delegate方法中需要的时候再将其。
上面就是涉及到的理论知识,是时候结合这些理论来进行编码了!
将你自己放置到地图中 – 添加你自己的信息
如之前看到的,卫星视图仍然不能提供关于公园的更多信息。现在,你的任务就是创建一个对象,该对象代表着整个公园的一个图层(对公园进行一些装饰)。
选中 Overlays 群组,然后创建一个继承自 NSObject 的类,名字为PVParkMapOverlay. 然后用下面的代码替换 PVParkMapOverlay.h 中的代码:
#import <Foundation/Foundation.h> #import <MapKit/MapKit.h> @class PVPark; @interface PVParkMapOverlay : NSObject <MKOverlay> - (instancetype)initWithPark:(PVPark *)park; @end |
在上面的代码中,导入了MapKit 头文件, 并添加了一个 PVPark 前向声明, 并告诉编译器,该类遵循 MKOverlay 协议. 最后,定义了方法 initWithPark.
下一步,用下面的代码替换 PVParkMapOverlay.m 中的代码:
#import "PVParkMapOverlay.h" #import "PVPark.h" @implementation PVParkMapOverlay @synthesize coordinate; @synthesize boundingMapRect; - (instancetype)initWithPark:(PVPark *)park { self = [super init]; if (self) { boundingMapRect = park.overlayBoundingMapRect; coordinate = park.midCoordinate; } return self; } @end |
在上面,导入了 PVPark 头文件. 然后实现一下协议中定义的的 coordinate 和boundingMapRect 两个属性, 在这里必须明确的将其@synthesize. 接着实现 initWithPark 方法. 这个方法从传入的 PVPark 对象获取出相关属性,并将其赋值给相应的 MKOverlay 属性.
现在你需要创建一个新的view,这个view继承自 MKOverlayView 类.
在 Overlays 群组中创建一个新的类,叫 PVParkMapOverlayView ,该类继承自MKOverlayView.
下面是PVParkMapOverlayView.h的代码,定义了一个方法:
#import <MapKit/MapKit.h> @interface PVParkMapOverlayView : MKOverlayView - (instancetype)initWithOverlay:(id<MKOverlay>)overlay overlayImage:(UIImage *)overlayImage; @end |
PVParkMapOverlayView 的实现包含两个方法,并在类扩展中有一个UIImage属性。
下一步,将下面代码添加到PVParkMapOverlayView.m:
#import "PVParkMapOverlayView.h" @interface PVParkMapOverlayView () @property (nonatomic, strong) UIImage *overlayImage; @end @implementation PVParkMapOverlayView - (instancetype)initWithOverlay:(id<MKOverlay>)overlay overlayImage:(UIImage *)overlayImage { self = [super initWithOverlay:overlay]; if (self) { _overlayImage = overlayImage; } return self; } - (void)drawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale inContext:(CGContextRef)context { CGImageRef imageReference = self.overlayImage.CGImage; MKMapRect theMapRect = self.overlay.boundingMapRect; CGRect theRect = [self rectForMapRect:theMapRect]; CGContextScaleCTM(context, 1.0, -1.0); CGContextTranslateCTM(context, 0.0, -theRect.size.height); CGContextDrawImage(context, theRect, imageReference); } @end |
OK, 我们来看看上面的代码.
initWithOverlay:overlayImage override了基类中的方法 initWithOverlay (第二个参数的类型是overlayImage.)。传递过来的图片存储在类扩展中的属性中,该属性在下面这个方法中会被使用:
- (void)drawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale inContext:(CGContextRef)context |
这个方法才是这个类的重点;它定义了如何渲染这个view,根据给定的参数: MKMapRect, MKZoomScale, 和图像上下文中,以适当的比例绘制叠加图层。
关于CoreGraphics的绘制超出了本文的范围。不过,从上面的代码中,可以看出使用传入的MKMapRect 可以获得一个 CGRect, 这样就可以确定UIImage的CGImageRef绘制到上下文中的位置。如果你想了解更多关于Core Graphics的内容,请看 Core Graphics tutorial series.
OK!现在你已经有两个类了:MKOverlay 和 MKOverlayView, 你可以将他们添加到map view中.
在 PVParkMapViewController.m 文件中导入这两个新创建的类:
#import "PVParkMapOverlayView.h" #import "PVParkMapOverlay.h" |
下一步,添加下面的代码,以定义一个新的方法,并把 MKOverlay 添加到map view中:
- (void)addOverlay { PVParkMapOverlay *overlay = [[PVParkMapOverlay alloc] initWithPark:self.park]; [self.mapView addOverlay:overlay]; } |
addOverlay 方法应该在 loadSelectedOptions 中被调用(如果用户决定显示这个图层).
按照下面的代码更新一下loadSelectedOptions 方法:
- (void)loadSelectedOptions { [self.mapView removeAnnotations:self.mapView.annotations]; [self.mapView removeOverlays:self.mapView.overlays]; for (NSNumber *option in self.selectedOptions) { switch ([option integerValue]) { case PVMapOverlay: [self addOverlay]; break; default: break; } } } |
loadSelectedOptions 每当用户隐藏掉options selection view时,都会调用方法loadSelectedOptions; 该方法会判断出哪个选项被选中,以调用适当的方法来渲染map view。
loadSelectedOptions 还会移除对应的注解和图层,以避免出现重复渲染的效果。这里的方法可能不是高效的,但对于本课程的介绍来将是一个简单的方法。
为了实现delegate方法,添加如下代码(还是在PVParkMapViewController.m中):
- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id<MKOverlay>)overlay { if ([overlay isKindOfClass:PVParkMapOverlay.class]) { UIImage *magicMountainImage = [UIImage imageNamed:@"overlay_park"]; PVParkMapOverlayView *overlayView = [[PVParkMapOverlayView alloc] initWithOverlay:overlay overlayImage:magicMountainImage]; return overlayView; } return nil; } |
当MKOverlay需要被添加到view中时,map view会调用delegate( PVParkMapViewController )的上面这个方法。这个方法会返回与MKOverlay相匹配的一个 MKOverlayView .
在这里,检查一下看看 overlay 的类型是不是 PVParkMapOverlay; 如果是的话,就加载overlay图片,并用这个图片创建一个 PVParkMapOverlayView 实例, 然后将这个实例返回给调用者。
这个PNG文件定义了公园的范围。overlay_park 图片 (来自 resources for this tutorial) 如下所示:
在Images群组中,添加non-retina和retina图片。
编译并运行程序,选择Map Overylay选项,然后看看!这个公园的overlay已经绘制到地图上方了,如下截图所示:
你可以随心所欲的放大、缩小或移动地图——overlay会按比例进行移动与显示。Cool!
如果你喜欢的话那就在地图上放置一个Pin — 注解
如果你用Maps程序搜索过位置信息,那么你肯定看到过在地图上出现的许多Pin。这可以理解为注解(annotation),它是用 MKAnnotationView创建的。你也可以在你的程序中使用注解 — 并使用你想要的任何图片,不仅仅是pin!
在程序中使用注解来标出具体的某个景点,这对游客来说非常有用。注解对象的使用方法跟MKOverlay 和 MKOverlayView非常类似, 只不过需要使用的类是MKAnnotation 和 MKAnnotationView.
在Annotations群组中创建一个名为 PVAttractionAnnotation 新的类,并继承自 NSObject.
然后用下面的代码替换 PVAttractionAnnotation.h 文件中的内容:
#import <Foundation/Foundation.h> #import <MapKit/MapKit.h> typedef NS_ENUM(NSInteger, PVAttractionType) { PVAttractionDefault = 0, PVAttractionRide, PVAttractionFood, PVAttractionFirstAid }; @interface PVAttractionAnnotation : NSObject <MKAnnotation> @property (nonatomic) CLLocationCoordinate2D coordinate; @property (nonatomic, strong) NSString *title; @property (nonatomic, strong) NSString *subtitle; @property (nonatomic) PVAttractionType type; @end |
上面的代码中,首先是import MapKit,然后为PVAttractionType.定义了一个枚举。这个枚举列出了注解的类型: 游乐设施,食物,急救和默认。
接着让这个类遵循 MKAnnotation Protocol. 跟MKOverlay类似, MKAnnotation 有一个 required coordinate 属性. 最后是定义了一些属性。
OK, 下面我们来看看PVAttractionAnnotation的实现。
将 PVAttractionAnnotation.m 按照如下修改:
#import "PVAttractionAnnotation.h" @implementation PVAttractionAnnotation @end |
这可能是本文中最简单的实现了!在里面不需要实现任何内容;只需要了解在头文件定义的一些属性即可!
现在需要创建一个MKAnnotation 实例来使用你的注解了。
在Annotation群组中创建另外一个类:PVAttractionAnnotationView 继承自MKAnnotationView. 头文件中不需要添加任何内容。
用下面的代码替换PVAttractionAnnotationView.h 中的内容:
#import <Foundation/Foundation.h> #import <MapKit/MapKit.h> @interface PVAttractionAnnotationView : MKAnnotationView @end |
将下面的代码添加到 PVAttractionAnnotationView.m:
#import "PVAttractionAnnotationView.h" #import "PVAttractionAnnotation.h" @implementation PVAttractionAnnotationView - (id)initWithAnnotation:(id<MKAnnotation>)annotation reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithAnnotation:annotation reuseIdentifier:reuseIdentifier]; if (self) { PVAttractionAnnotation *attractionAnnotation = self.annotation; switch (attractionAnnotation.type) { case PVAttractionFirstAid: self.image = [UIImage imageNamed:@"firstaid"]; break; case PVAttractionFood: self.image = [UIImage imageNamed:@"food"]; break; case PVAttractionRide: self.image = [UIImage imageNamed:@"ride"]; break; default: self.image = [UIImage imageNamed:@"star"]; break; } } return self; } @end |
上面重载了方法 initWithAnnotation:reuseIdentifier:; 根据注解不同的type属性,为注解设置不同的image属性。
非常棒! 现在你创建好了注解和与其相关的view,下面可以将它们添加到map view中了!
首先,你需要准备在 initWithAnnotation:reuseIdentifier:方法中用到的一些资源,以及经典相关的位置信息(MagicMountainAttractions.plist). 这些资源包含在 resources for this tutorial – 将它们拷贝到工程的Images群组中。
在plist文件中包含了坐标信息以及其它与公园景点相关的一些详细信息,如下所示:
- <?xml version="1.0" encoding="UTF-8"?>
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
- <plist version="1.0">
- <array>
- <dict>
- <key>name</key>
- <string>Goliath</string>
- <key>location</key>
- <string>{34.42635,-118.59712}</string>
- <key>type</key>
- <string>1</string>
- <key>subtitle</key>
- <string>Intensity: 8/10</string>
- </dict>
- <dict>
- <key>name</key>
- <string>Batman</string>
- <key>location</key>
- <string>{34.42581,-118.60089}</string>
- <key>type</key>
- <string>1</string>
- <key>subtitle</key>
- <string>Intensity: 6/10</string>
- </dict>
- <dict>
- <key>name</key>
- <string>Ridler's Revenge</string>
- <key>location</key>
- <string>{34.42430,-118.60074}</string>
- <key>type</key>
- <string>1</string>
- <key>subtitle</key>
- <string>Intensity: 6/10</string>
- </dict>
- <dict>
- <key>name</key>
- <string>X2</string>
- <key>location</key>
- <string>{34.42156,-118.59556}</string>
- <key>type</key>
- <string>1</string>
- <key>subtitle</key>
- <string>Intensity: 10/10</string>
- </dict>
- <dict>
- <key>name</key>
- <string>Tatsu</string>
- <key>location</key>
- <string>{34.42150,-118.59741}</string>
- <key>type</key>
- <string>1</string>
- <key>subtitle</key>
- <string>Intensity: 7/10</string>
- </dict>
- <dict>
- <key>name</key>
- <string>Panda Express</string>
- <key>location</key>
- <string>{34.42126,-118.595637}</string>
- <key>type</key>
- <string>2</string>
- <key>subtitle</key>
- <string>Cost: $$</string>
- </dict>
- <dict>
- <key>name</key>
- <string>Cold Stone</string>
- <key>location</key>
- <string>{34.42401,-118.59495}</string>
- <key>type</key>
- <string>2</string>
- <key>subtitle</key>
- <string>Cost: $</string>
- </dict>
- <dict>
- <key>name</key>
- <string>First Aid</string>
- <key>location</key>
- <string>{34.42640,-118.59918}</string>
- <key>type</key>
- <string>3</string>
- <key>subtitle</key>
- <string>Call 911 For Emergency</string>
- </dict>
- </array>
- </plist>
现在你以及拥有上面这些资源了,当然你也可以使用新的注解!
回到 PVParkMapViewController.m 并import MKAnnotation 和 MKAnnotationView两个类, 如下所示:
#import "PVAttractionAnnotation.h" #import "PVAttractionAnnotationView.h" |
下面定义一个方法,将景点的注解添加到map view中:
添加如下方法(还是在PVParkMapViewController.m):
- (void)addAttractionPins { NSString *filePath = [[NSBundle mainBundle] pathForResource:@"MagicMountainAttractions" ofType:@"plist"]; NSArray *attractions = [NSArray arrayWithContentsOfFile:filePath]; for (NSDictionary *attraction in attractions) { PVAttractionAnnotation *annotation = [[PVAttractionAnnotation alloc] init]; CGPoint point = CGPointFromString(attraction[@"location"]); annotation.coordinate = CLLocationCoordinate2DMake(point.x, point.y); annotation.title = attraction[@"name"]; annotation.type = [attraction[@"type"] integerValue]; annotation.subtitle = attraction[@"subtitle"]; [self.mapView addAnnotation:annotation]; } } |
上面的个方法读取 MagicMountainAttractions.plist 文件,并对字典数组进行枚举. 针对每个条目,都使用相关的景点信息来创建一个 PVAttractionAnnotation 实例,并将它们添加到map view中。
现在需要更新一下 loadSelectedOptions 方法,当被选中时,以匹配新的选项,并执行新的方法。
将下面的代码添加到 loadSelectedOptions 中(仍然在 PVParkMapViewController.m):
- (void)loadSelectedOptions { [self.mapView removeAnnotations:self.mapView.annotations]; [self.mapView removeOverlays:self.mapView.overlays]; for (NSNumber *option in self.selectedOptions) { switch ([option integerValue]) { case PVMapOverlay: [self addOverlay]; break; case PVMapPins: [self addAttractionPins]; break; default: break; } } } |
现在快完成了!不过还差一点,你需要实现另外一个delegate方法,这个delegate方法给map view提供一个MKAnnotationView 实例,这样才能够进行渲染。
将下面的代码添加到 PVParkMapViewController.m:
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation { PVAttractionAnnotationView *annotationView = [[PVAttractionAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:@"Attraction"]; annotationView.canShowCallout = YES; return annotationView; } |
上面的方法会接受一个 MKAnnotation ,并用此来创建一个 PVAttractionAnnotationView. 它的canShowCallout 属性设置为YES,这样当用户触摸到这个注解时,会显示出更多的信息,最后将这个注解返回!
启动景点Pin,看看结果是什么样,如下图:
景点pin在这里看起来有点不圆滑!:]
至此,已经介绍了MapKit许多复杂的知识了,包括overlays和annotation。不过如果你希望绘制一些基本的图形呢:线条、形状和圆等?
使用MapKit framework同样可以在map view上进行绘制!MapKit提供的MKPolyline, MKPolygon 和 MKCircle 就可以做到.
我走的线路 – MKPolyline
如果你去过魔山,你应该知道 Goliath hypercoaster 是一个非常令人难以置信的旅程, 当骑手们跨进了大门,就想一路狂奔!:]
为了帮助这些骑手们,你绘制了一条从公园入口到Goliath的路径。
通过MKPolyline 可以非常方便的画一条连接多个点的线,比如绘制一条从A到B的非直线路径。在程序中用 MKPolyline 绘制路径,Goliath的粉丝们会更加快捷的骑行。!需要一系列的经纬度坐标信息,这些坐标将用来按顺序的进行绘制。在这里顺序非常重要 — 否则,绘制出来的线路会是无用的,对骑手们来说毫无意义!
resources for this tutorial 中包含了一个叫 EntranceToGoliathRoute.plist 的文件,里面有路径信息,将这个文件添加到工程中。
这个plist文件中的内容如下:
- <?xml version="1.0" encoding="UTF-8"?>
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
- <plist version="1.0">
- <array>
- <string>{34.42367,-118.594836}</string>
- <string>{34.423597,-118.595205}</string>
- <string>{34.423004,-118.59537}</string>
- <string>{34.423044,-118.595806}</string>
- <string>{34.423419,-118.596126}</string>
- <string>{34.423569,-118.596229}</string>
- <string>{34.42382,-118.596192}</string>
- <string>{34.42407,-118.596283}</string>
- <string>{34.424323,-118.596534}</string>
- <string>{34.42464,-118.596858}</string>
- <string>{34.42501,-118.596838}</string>
- <string>{34.42537,-118.596688}</string>
- <string>{34.425690,-118.596683}</string>
- <string>{34.42593,-118.596806}</string>
- <string>{34.42608,-118.597101}</string>
- <string>{34.42634,-118.597094}</string>
- </array>
- </plist>
如上,属性列表是一个简单的字符串数组,字符串中包含了路径中每个点的经度和纬度信息。
现在,你需要写一个方法来读取plist文件中的内容,并为骑手们创建出一条路。
将下面的代码添加到 PVParkMapViewController.m:
- (void)addRoute { NSString *thePath = [[NSBundle mainBundle] pathForResource:@"EntranceToGoliathRoute" ofType:@"plist"]; NSArray *pointsArray = [NSArray arrayWithContentsOfFile:thePath]; NSInteger pointsCount = pointsArray.count; CLLocationCoordinate2D pointsToUse[pointsCount]; for(int i = 0; i < pointsCount; i++) { CGPoint p = CGPointFromString(pointsArray[i]); pointsToUse[i] = CLLocationCoordinate2DMake(p.x,p.y); } MKPolyline *myPolyline = [MKPolyline polylineWithCoordinates:pointsToUse count:pointsCount]; [self.mapView addOverlay:myPolyline]; } |
上面这个方法会读取 EntranceToGoliathRoute.plist, 并遍历其中包含的内容,然后将信息单独坐标字符串转换为 CLLocationCoordinate2D 结构.
很明显,在程序中绘制折线是很简单的;你创建一个数组,数组里面包含所有的点,并将这个数组传递给 MKPolyline! 没有比这更简单的了。
下面,你需要添加一个选项,允许用户打开或者关闭折线路径。
用下面的代码更新一下 loadSelectedOptions 方法:
- (void)loadSelectedOptions { [self.mapView removeAnnotations:self.mapView.annotations]; [self.mapView removeOverlays:self.mapView.overlays]; for (NSNumber *option in self.selectedOptions) { switch ([option integerValue]) { case PVMapOverlay: [self addOverlay]; break; case PVMapPins: [self addAttractionPins]; break; case PVMapRoute: [self addRoute]; break; default: break; } } } |
最后,为了将所有的这些内容结合在一起,需要更新一下delegate方法—返回在map view中实际需要渲染的view。
更新一下 mapView:viewForOverlay: 方法,以能够处理折线图层的情况,如下:
- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id<MKOverlay>)overlay { if ([overlay isKindOfClass:PVParkMapOverlay.class]) { UIImage *magicMountainImage = [UIImage imageNamed:@"overlay_park"]; PVParkMapOverlayView *overlayView = [[PVParkMapOverlayView alloc] initWithOverlay:overlay overlayImage:magicMountainImage]; return overlayView; } else if ([overlay isKindOfClass:MKPolyline.class]) { MKPolylineView *lineView = [[MKPolylineView alloc] initWithOverlay:overlay]; lineView.strokeColor = [UIColor greenColor]; return lineView; } return nil; } |
折线图层显示的处理跟之前的overlay view十分类似。只不过,绘制折线的时候,不需要提供自己的view对象。只需要使用 MKPolyLineView 即可—用overlay初始化一个新的实例即可.
MKPolyLineView 同样提供可以修改折线的一些属性。比如,可以把折线的颜色修改为绿色。
编译并运行程序,将route选项打开,出现在屏幕中的内容如下图所示:
Goliath的粉丝们现在可以利用这个绘制的路径去创造新的记录啦!:]
要是能够实际的显示出公园的边界范围就太棒了——因为公园实际上并不是完整的占据整个屏幕。
虽然可以使用 MKPolyline 来绘制公园的边界,不过MapKit提供了另外一个类专门来绘制封闭的多边形:MKPolygon.
不要围着我 – MKPolygon
MKPolygon 跟MKPolyline非常相似(坐标集合中除了第一个点和最后一个点是连接以外)
可以创建一个 MKPolygon ,将其当做显示公园边界的一个overylay。公园的边界坐标已经定义在the MagicMountain.plist中了; 可以返回之前的内容看看 initWithFilename: 方法是如何从plist文件中读取出边界点的。
将下面的代码添加到 PVParkMapViewController.m:
- (void)addBoundary { MKPolygon *polygon = [MKPolygon polygonWithCoordinates:self.park.boundary count:self.park.boundaryPointsCount]; [self.mapView addOverlay:polygon]; } |
addBoundary 方法的实现非常简单。从park实例中制定边界数组和边界点数,这样就可以很快捷的创建一个新的 MKPolygon 实例了!
你能猜猜下一步要做什么吗?跟上面的MKPolyline 操作非常类似.
更新一下 loadSelectedOptions 方法,以能够显示或者隐藏公园的边界,如下所示:
- (void)loadSelectedOptions { [self.mapView removeAnnotations:self.mapView.annotations]; [self.mapView removeOverlays:self.mapView.overlays]; for (NSNumber *option in self.selectedOptions) { switch ([option integerValue]) { case PVMapOverlay: [self addOverlay]; break; case PVMapPins: [self addAttractionPins]; break; case PVMapRoute: [self addRoute]; break; case PVMapBoundary: [self addBoundary]; break; default: break; } } } |
MKPolygon 跟MKPolyline一样,遵循 MKOverlay ,因此也需要更新一下相关的delegate方法。
在PVParkMapViewController.m中更新一下delegate方法:
- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id<MKOverlay>)overlay { if ([overlay isKindOfClass:PVParkMapOverlay.class]) { UIImage *magicMountainImage = [UIImage imageNamed:@"overlay_park"]; PVParkMapOverlayView *overlayView = [[PVParkMapOverlayView alloc] initWithOverlay:overlay overlayImage:magicMountainImage]; return overlayView; } else if ([overlay isKindOfClass:MKPolyline.class]) { MKPolylineView *lineView = [[MKPolylineView alloc] initWithOverlay:overlay]; lineView.strokeColor = [UIColor greenColor]; return lineView; } else if ([overlay isKindOfClass:MKPolygon.class]) { MKPolygonView *polygonView = [[MKPolygonView alloc] initWithOverlay:overlay]; polygonView.strokeColor = [UIColor magentaColor]; return polygonView; } return nil; } |
如上所示,delegate的更新方式跟之前一样简单。创建一个MKPolygonView实例,并将其颜色设置为magenta.
编译并运行程序,将看到公园的边界!
上面就是折线和多边形的绘制。下面我将介绍最后一种绘制——使用MKCircle绘制圆
沙滩上的圈圈 – MKCircle
MKCircle 跟 MKPolyline 和 MKPolygon,同样非常相似。只不过它是根据给定的中心坐标和半径来绘制一个圆。
可以想象一下,在公园里,用户可能希望在地图上做一下标注,跟将这个标注与他人分享。此时,就可以使用一个圆来代表用户的标注。
在本文中,你不会走的很远,不过,最起码的,你可以从文件中加载一些坐标数据,并在地图上绘制出一些圆,来当做用户在地图上的一些标注。
MKCircle overlay 可以很容易的就实现这个功能.
resources for this tutorial 包含了相关标注的位置信息,只需要将其添加到工程中。
每个文件包含了一些坐标信息。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <array> <string>{34.42481,-118.596914}</string> <string>{34.423383,-118.596101}</string> <string>{34.423628,-118.595197}</string> <string>{34.421832,-118.595404}</string> </array> </plist> |
我将用 PVCharacter代表用户的标注。下面就在Models群组中创建一个新类 PVCharacter ,并继承自 MKCircle.
然后用下面的代码替换 PVCharacter.h 中的内容:
#import <Foundation/Foundation.h> #import <MapKit/MapKit.h> @interface PVCharacter : MKCircle <MKOverlay> @property (nonatomic, strong) NSString *name; @property (nonatomic, strong) UIColor *color; @end |
新的这个类遵循 MKOverlay 协议, 并定义了两个属性: name和 color.
这个类的实现非常简单 — 不需要添加任何内容.
#import "PVCharacter.h" @implementation PVCharacter @end |
PVParkMapViewController.m 中import PVCharacter.h。如下代码:
#import "PVCharacter.h"
|
现在,需要一个方法,将plist文件中的数据加载到程序中。将如下代码添加到PVParkMapViewController.m:
- (void)addCharacterLocation { NSString *batmanFilePath = [[NSBundle mainBundle] pathForResource:@"BatmanLocations" ofType:@"plist"]; NSArray *batmanLocations = [NSArray arrayWithContentsOfFile:batmanFilePath]; CGPoint batmanPoint = CGPointFromString(batmanLocations[rand()%4]); PVCharacter *batman = (PVCharacter *)[PVCharacter circleWithCenterCoordinate:CLLocationCoordinate2DMake(batmanPoint.x, batmanPoint.y) radius:MAX(5, rand()%40)]; batman.color = [UIColor blueColor]; NSString *tazFilePath = [[NSBundle mainBundle] pathForResource:@"TazLocations" ofType:@"plist"]; NSArray *tazLocations = [NSArray arrayWithContentsOfFile:tazFilePath]; CGPoint tazPoint = CGPointFromString(tazLocations[rand()%4]); PVCharacter *taz = (PVCharacter *)[PVCharacter circleWithCenterCoordinate:CLLocationCoordinate2DMake(tazPoint.x, tazPoint.y) radius:MAX(5, rand()%40)]; taz.color = [UIColor orangeColor]; NSString *tweetyFilePath = [[NSBundle mainBundle] pathForResource:@"TweetyBirdLocations" ofType:@"plist"]; NSArray *tweetyLocations = [NSArray arrayWithContentsOfFile:tweetyFilePath]; CGPoint tweetyPoint = CGPointFromString(tweetyLocations[rand()%4]); PVCharacter *tweety = (PVCharacter *)[PVCharacter circleWithCenterCoordinate:CLLocationCoordinate2DMake(tweetyPoint.x, tweetyPoint.y) radius:MAX(5, rand()%40)]; tweety.color = [UIColor yellowColor]; [self.mapView addOverlay:batman]; [self.mapView addOverlay:taz]; [self.mapView addOverlay:tweety]; } |
上面的这个方法对每个标注都做了相同的操作。首先,从plist中读出数据,然后随即选取出一个位置。下一步,根据选取出的位置,创建一个 PVCharacter 实例。随即选取一个半径值。
最后,给每个标注都设置一个颜色,并添加到map view中。
现在基本算是完成了 — 你还记得最后几步是如何操作的吗?
没错 — 你同样还需要通过delegate方法,给map view提供 MKOverlayView.
更新一下 PVParkMapViewController.m 中的delegate方法:
- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id<MKOverlay>)overlay { if ([overlay isKindOfClass:PVParkMapOverlay.class]) { UIImage *magicMountainImage = [UIImage imageNamed:@"overlay_park"]; PVParkMapOverlayView *overlayView = [[PVParkMapOverlayView alloc] initWithOverlay:overlay overlayImage:magicMountainImage]; return overlayView; } else if ([overlay isKindOfClass:MKPolyline.class]) { MKPolylineView *lineView = [[MKPolylineView alloc] initWithOverlay:overlay]; lineView.strokeColor = [UIColor greenColor]; return lineView; } else if ([overlay isKindOfClass:MKPolygon.class]) { MKPolygonView *polygonView = [[MKPolygonView alloc] initWithOverlay:overlay]; polygonView.strokeColor = [UIColor magentaColor]; return polygonView; } else if ([overlay isKindOfClass:PVCharacter.class]) { MKCircleView *circleView = [[MKCircleView alloc] initWithOverlay:overlay]; circleView.strokeColor = [(PVCharacter *)overlay color]]; return circleView; } return nil; } |
最后,更新一下 loadSelectedOptions ,让用户可以打开或者隐藏标注信息。
如下是更新后的代码:
- (void)loadSelectedOptions { [self.mapView removeAnnotations:self.mapView.annotations]; [self.mapView removeOverlays:self.mapView.overlays]; for (NSNumber *option in self.selectedOptions) { switch ([option integerValue]) { case PVMapOverlay: [self addOverlay]; break; case PVMapPins: [self addAttractionPins]; break; case PVMapRoute: [self addRoute]; break; case PVMapBoundary: [self addBoundary]; break; case PVMapCharacterLocation: [self addCharacterLocation]; break; default: break; } } } |
编译并运行程序,然后打开character,将看到如下内容:
何去何从?
上面的内容就是本文要介绍的了 — 至此,你已经知道如何使用MapKit绘制自己的overylay image和overlay view。
这里是本文最后完成工程代码: final example project .
恭喜你 — 你已经使用了MapKit提供的许多重要的功能了。并完成了一个地图程序最基本的功能,实现了标注,卫星视图和定制图层!
这个程序的另一个方向就是研究一下创建地图图层的其它方法。
实际上,从简单到复杂的,有许多不同的方法来创建图层。本文使用的方法是图片: overlay_park .
为了生成图层,将卫星视图的截图当做公园的底层。然后绘制一些过山车,树,停车点,以及其它的一些新图层。
当获得了卫星截图后,需要给这个截图的4个角落确定一下经纬度。这些经纬度将用来创建公园的属性列表 — 用在map view上的位置图层信息。
这里还有很多高级的方法来创建图层 — 或许更加高效. 一些可选的方法是使用 KML 文件, MapBox tiles,或者其它第三方提供的资源。
本文的主要目的是介绍关于MapKit framework APIs,因此没有深入介绍overylay类型。如果你是一名地图高级开发者,你可以更加深入的研究一下其它方面的主题。
希望本文对你有用,我也希望能够看到你在程序中使用MapKit overylay。如果你有任何问题或者建议,请反馈给我!