您的位置:首页 > 其它

使用MapKit叠加图层(raywenderlich翻译)(上)

2013-03-12 13:41 225 查看
本文是由iOS Tutorial小组成员Chris Wagner撰写,他是一名软件工程爱好者,一直在努力做一名前沿开发者。



使用MapKit在程序中添加一个地图是非常容易的事。不过,如果你希望使用自己的注解和图片来装饰或者定制苹果提供的地图呢?幸运的是苹果提供了非常容易的方法来完成这样的需求:自定义叠加图层。



在本文中,你将为Six Flags Magic Mountain游乐园创建一个程序。如果你在洛杉矶是一个坐过山车的粉丝,你会喜欢这个程序的。



想想,一个来到游乐园的游人都会喜欢这个程序的一些功能:例如具体景点的位置,各种过山车的路线,以及公园的一些特点。这些内容的显示通过定制一个叠加图片是非常好的选择——这真是本文要介绍的内容。


(注意: 根据你的经验水平,学习本文,你有两种选择:

对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,编译并运行。我敢打赌,你对目前这个工程有点失望,因为你将看到的如下内容:


你知道去San Jose的道路吗?—添加一个MapView
为了在程序中添加一个MapView,首先请打开MainStoryboard_iPhone.storyboard文件。选择Park Map View Controller,然后将一个Map View对象拖拽到view中,调整MapView以填满整个view,如下图所示:


现在打开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
下一步,打开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
通过上面的操作,我们完成了插槽,delegate,controller的配置。现在可以在地图中添加一些交互了!



我在这里,你不知道如何从这而到那儿--- 与 MKMapView进行交互



虽然地图默认的视图非常好看,但是这对于只关注主题公园(而不是所有的大陆)的人来说,使用起来太广泛了!当程序启动的时候,将公园的地图视图放置在程序的中间非常好。

获得某个具体位置的位置信息有许多中方法;可以通过web service获取,也可以将位置信息内置在程序中。


为了简单起见,在本文中,我把公园的位置信息打包放在程序中。下载这个工程的资源resources for this project),里面有一个名为MagicMountain.plist的文件,包含了公园的信息。



MagicMountain.plist的内容如下:

01

02

03

04

05 midCoord

06 {34.4248,-118.5971}

07 overlayTopLeftCoord

08 {34.4311,-118.6012}

09 overlayTopRightCoord

10 {34.4311,-118.5912}

11 overlayBottomLeftCoord

12 {34.4194,-118.6012}

13 boundary

14

15 {34.4313,-118.59890}

16 {34.4274,-118.60246}

17 {34.4268,-118.60181}

18 {34.4202,-118.6004}

19 {34.42013,-118.59239}

20 {34.42049,-118.59051}

21 {34.42305,-118.59276}

22 {34.42557,-118.59289}

23 {34.42739,-118.59171}

24

25

26


上面的文件中包含的信息不仅有你现在需要的(将公园放在地图中间);另外,还包含了公园的边界信息—稍后会用到。



文件中所有的信息都是以经度/维度坐标(latitude/longitude coordinates)格式提供的。



把这个文件添加到工程中:通过把这个文件拖拽工程的Park Informatio群组中。



现在你已经有了关于公园的地理位置信息了,下面你应该把这些地理信息模型化为Objective-C对象,以便于在程序中使用。



选择工程中的Models群组,然后选择File > New > File… > Objective-C Class (在Cocoa Touch里面)。将类命名为PVPark,并继承自NSObject。



当新的类创建好之后,将下面的属性和初始化方法添加到PVPark.h:
#import

#import



@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! 现在地图已经显示出了Six Flags Magic Mountain公园的正中心。但是显示的内容并不能让我们非常兴奋,它只是显示了几个米色空白区域,外加边缘上有几条街道。
如果你以前使用过地图程序,你肯定知道里面的卫星地图看起来非常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)overlay



当map view发现在其显示区域内有一个 MKOverlay 对象时,会调用上面这个方法。



概括的说,你不要直接将MKOverlayView添加到map view中; 而是告诉map关于MKOverlays如何显示,以及在delegate方法中需要的时候再将其。
上面就是涉及到的理论知识,是时候结合这些理论来进行编码了!

将你自己放置到地图中 – 添加你自己的信息
如之前看到的,卫星视图仍然不能提供关于公园的更多信息。现在,你的任务就是创建一个对象,该对象代表着整个公园的一个图层(对公园进行一些装饰)。选中 Overlays 群组,然后创建一个继承自 NSObject 的类,名字为PVParkMapOverlay. 然后用下面的代码替换 PVParkMapOverlay.h 中的代码:
#import

#import



@class PVPark;



@interface PVParkMapOverlay : NSObject



- (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



@interface PVParkMapOverlayView : MKOverlayView



- (instancetype)initWithOverlay:(id)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)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)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会按比例进行移动与显示。





来源:破船的博客

原文:http://www.raywenderlich.com/30001/overlay-images-and-overlay-views-with-mapkit-tutorial
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: