• 用更简单的方式去使用地图(支持 MapKit、高德地图、百度地图)
  • 因为有个项目要在地图中显示位置,所以用到了MapKit。 记录下来,以免以后忘记。 加入MapKit library 首先得在项目中加入MapKit,如图 MapView 先增加一个ViewController,我这里用的storyboard,这个...

    因为有个项目要在地图中显示位置,所以用到了MapKit。

    记录下来,以免以后忘记。

    加入MapKit library

    首先得在项目中加入MapKit,如图



    MapView

    先增加一个ViewController,我这里用的storyboard,这个玩意还是挺好用的,比以前用xib好多了。


    然后拖一个mapview上去,如:


    给新增加的ViewController绑定一个class。首先得增加一个class,从uiViewController继承下来。这个很简单,如图


    把新增加的ViewController绑定到这个class,也很easy,发现Xcode还是挺牛的。就是在右边Identity inspector里面的custom class里面改成新增加的类,原来是UIViewController。


    然后给map view控件绑定一个变量,类型是MKMapView


    然后就初始化mapview,显示。代码如下:

    - (void)viewDidLoad
    {
        [super viewDidLoad];
    	// Do any additional setup after loading the view.
        
        _mapView.mapType = MKMapTypeStandard;//标准模式
        _mapView.showsUserLocation = YES;//显示自己
    
        _mapView.zoomEnabled = YES;//支持缩放
        
        
        CLLocationCoordinate2D pos = {39.931203, 116.395573};//找个坐标,我是用百度坐标抓取弄的。http://api.map.baidu.com/lbsapi/getpoint/
        
        MKCoordinateRegion viewRegion = MKCoordinateRegionMakeWithDistance(pos,2000, 2000);//以pos为中心,显示2000米
        MKCoordinateRegion adjustedRegion = [_mapView regionThatFits:viewRegion];//适配map view的尺寸
        [_mapView setRegion:adjustedRegion animated:YES];
        
    
    }
    我这里使用百度坐标,找了个坐标(直接搜索“百度 坐标”),然后在我们自己的地图里显示。这样运行一下就可以看到:


    Map view delegate 回调

    可以实现协议MKMapViewDelegate, 这样就会有几个回调。

    - (void) mapViewWillStartLoadingMap:(MKMapView *)mapView//开始从服务器获取地图数据
    {
       
    }
    
    -(void)mapViewDidFinishLoadingMap:(MKMapView *)mapView//获取数据结束
    {
        
    }
    
    - (void) mapViewDidFailLoadingMap:(MKMapView *)mapView withError:(NSError *)error//获取数据失败了。
    {
        
    }

    获取设备当前位置并且在地图中显示

    增加一个按钮,点击这个按钮,将显示设备当前位置。点击上面的按钮将显示某个固定位置。


    CLLocationManager,首先使用CLLocationManager来获取设备的当前位置。

    代码也是很简单

    //获得自己的当前的位置信息
    - (void) getCurPosition
    {
    	//开始探测自己的位置
    	if (locationManager==nil)
    	{
    		locationManager =[[CLLocationManager alloc] init];
    	}
    	
    	
    	if ([CLLocationManager locationServicesEnabled])
    	{
    		locationManager.delegate=self;
    		locationManager.desiredAccuracy=kCLLocationAccuracyBest;
    		locationManager.distanceFilter=10.0f;
    		[locationManager startUpdatingLocation];
    	}
    }
    

    然后实现回调函数

    #pragma mark -- CLLocationManagerDelegate
    - (void) locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
    {
        if ([locations count] > 0) {
            CLLocation* loc = [locations objectAtIndex:0];
            CLLocationCoordinate2D pos = [loc coordinate];
            
            NSLog(@"locationManager, longitude: %f, latitude: %f", pos.longitude, pos.latitude);
            
            if (show == NO) {
                MKCoordinateRegion viewRegion = MKCoordinateRegionMakeWithDistance(pos,2000, 2000);//以pos为中心,显示2000米
                MKCoordinateRegion adjustedRegion = [_mapView regionThatFits:viewRegion];//适配map view的尺寸
                [_mapView setRegion:adjustedRegion animated:YES];
    
                show = YES;
            }
        }
    }
    当设备位置变化时,这个函数会被调用。这样我们就可以根据位置来做一些事情了。这个例子里就在第一次获取位置的时候更新一下地图显示。以设备当前位置为中心,显示2000米。


    完了。贴一下mapview所在的controller代码:

    //
    //  KMapViewController.m
    //  MapDemo
    //
    //  Created by Kevin on 14-2-10.
    //  Copyright (c) 2014年 Kevin. All rights reserved.
    //
    
    #import "KMapViewController.h"
    
    @interface KMapViewController ()
    
    @end
    
    @implementation KMapViewController
    
    - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
    {
        self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
        if (self) {
            // Custom initialization
        }
        return self;
    }
    
    - (void)viewDidLoad
    {
        [super viewDidLoad];
    	// Do any additional setup after loading the view.
        
        show = NO;
        
        _mapView.mapType = MKMapTypeStandard;//标准模式
        _mapView.showsUserLocation = YES;//显示自己
        _mapView.delegate = self;
        _mapView.zoomEnabled = YES;//支持缩放
        
        
        NSString* i = self.Index;
        
        if([i isEqualToString:@"1"])
        {
            CLLocationCoordinate2D pos = {39.931203, 116.395573};//找个坐标,我是用百度坐标抓取弄的。http://api.map.baidu.com/lbsapi/getpoint/
            
            MKCoordinateRegion viewRegion = MKCoordinateRegionMakeWithDistance(pos,2000, 2000);//以pos为中心,显示2000米
            MKCoordinateRegion adjustedRegion = [_mapView regionThatFits:viewRegion];//适配map view的尺寸
            [_mapView setRegion:adjustedRegion animated:YES];
    
        }
        else
        {
            [self getCurPosition];
        }
    
    }
    
    - (void)didReceiveMemoryWarning
    {
        [super didReceiveMemoryWarning];
        // Dispose of any resources that can be recreated.
    }
    
    - (void) dealloc
    {
        
     //   [super dealloc];
    }
    
    //获得自己的当前的位置信息
    - (void) getCurPosition
    {
    	//开始探测自己的位置
    	if (locationManager==nil)
    	{
    		locationManager =[[CLLocationManager alloc] init];
    	}
    	
    	
    	if ([CLLocationManager locationServicesEnabled])
    	{
    		locationManager.delegate=self;
    		locationManager.desiredAccuracy=kCLLocationAccuracyBest;
    		locationManager.distanceFilter=10.0f;
    		[locationManager startUpdatingLocation];
    	}
    }
    
    #pragma mark -- MPMapViewDelegate
    
    - (void) mapViewWillStartLoadingMap:(MKMapView *)mapView
    {
        
    }
    
    -(void)mapViewDidFinishLoadingMap:(MKMapView *)mapView
    {
        
    }
    
    - (void) mapViewDidFailLoadingMap:(MKMapView *)mapView withError:(NSError *)error
    {
        
    }
    
    #pragma mark -- CLLocationManagerDelegate
    - (void) locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
    {
        if ([locations count] > 0) {
            CLLocation* loc = [locations objectAtIndex:0];
            CLLocationCoordinate2D pos = [loc coordinate];
            
            NSLog(@"locationManager, longitude: %f, latitude: %f", pos.longitude, pos.latitude);
            
            if (show == NO) {
                MKCoordinateRegion viewRegion = MKCoordinateRegionMakeWithDistance(pos,2000, 2000);//以pos为中心,显示2000米
                MKCoordinateRegion adjustedRegion = [_mapView regionThatFits:viewRegion];//适配map view的尺寸
                [_mapView setRegion:adjustedRegion animated:YES];
    
                show = YES;
            }
        }
    }
    
    @end
    





    展开全文
  • MapKit 教程: 覆盖物

    2017-11-27 11:29:29
    原文:MapKit Tutorial: Overlay Views 作者:Owen Brown 译者:kmyhy 更新说明:本教程由 Owen Brown 更新至 Xcode 9、iOS 11 和 Swift 4。原文作者是 Chris Wagner。 苹果通过 MapKit 让你轻易地将地图添加...

    原文:MapKit Tutorial: Overlay Views
    作者:Owen Brown
    译者:kmyhy

    更新说明:本教程由 Owen Brown 更新至 Xcode 9、iOS 11 和 Swift 4。原文作者是 Chris Wagner。

    苹果通过 MapKit 让你轻易地将地图添加到你的 app,只是它看起来并不是那么漂亮。幸运的是,你可以用自定义覆盖物让它更引人注目。

    在这篇 MapKit 教程中,你将创建一个展示六旗魔术山的 app。如果你是一个寻求刺激的快车手,这个 app 就是专属于你的了!

    完成这个 app 之后,你将有一个互动式公园地图,显示出景点位置、骑乘路线和人物位置。

    开始

    这里下载开始项目。这个项目已经包含了导航,但没有地图。

    打开开始项目,Build & run,你会看到一个空白页面。你会在上面添加一个地图并允许选择覆盖物的类型。

    添加 MapKit 中的 MapView

    打开 Main.storyboard 选择 Park Map View Controller 场景。

    从 Object Library 中拖一个 Map View 到这个场景中。将它放到导航条下面占满剩余的空间。

    然后,点击 Add New Constraints 按钮,添加 4 个约束,constant 都设置为 0,点击 Add 4 Constraints。

    绑定 MapView

    要使用这个 MapView,你必须做两件事情:(1) 为它创建一个出口 (2) 设置它的 delegate。

    在 file 视图中用 alt+鼠标左键点击 ParkMapViewController.swift,在助手编辑器中打开 ParkMapViewController。

    然后,从 map view 拖一条线到右边:

    在弹出窗口中,将出口命名为 mapView,然后点击 Connect。

    要设置 map view 的委托,右键点击 map view 对象,打开上下文菜单,然后从其中的 delegate 出口拖到 Park Map View Controller:

    你还必须让 ParkMapViewController 适应 MKMapViewDelegate 协议。

    首先,为 ParkMapViewController 添加相应的 import 语句:

    import MapKit
    

    然后,在类声明关闭的大括号后面添加一个扩展:

    extension ParkMapViewController: MKMapViewDelegate {
    
    }

    Build & run,看看你的新地图:

    如果不用这个地图真正地做些什么事情,那有什么意思呢?是时候和地图进行一些交互了!

    和地图交互

    首先是让地图中心指向这个公园。在 app 的 Park Information 文件夹中,你会发现一个 MagicMountain.plist 的文件。打开这个文件,你会看到它有一个坐标,包含了公园的中心和边界信息。

    现在创建一个模型。用于表示这个 plist,以便 app 使用。

    右键点击文件导航器中的 Models 文件夹,选择 New File…,选择 iOS\Source\Swift File 模板并命名为 Park.swift。编辑内容为:

    import UIKit
    import MapKit
    
    class Park {
      var name: String?
      var boundary: [CLLocationCoordinate2D] = []
    
      var midCoordinate = CLLocationCoordinate2D()
      var overlayTopLeftCoordinate = CLLocationCoordinate2D()
      var overlayTopRightCoordinate = CLLocationCoordinate2D()
      var overlayBottomLeftCoordinate = CLLocationCoordinate2D()
      var overlayBottomRightCoordinate = CLLocationCoordinate2D()
    
      var overlayBoundingMapRect: MKMapRect?
    }

    你还需要能够设置公园在这个 plist 文件中的数据。

    首先,添加一个实用方法,用于反序列化 plist:

    class func plist(_ plist: String) -> Any? {
      let filePath = Bundle.main.path(forResource: plist, ofType: "plist")!
      let data = FileManager.default.contents(atPath: filePath)!
      return try! PropertyListSerialization.propertyList(from: data, options: [], format: nil)
    }

    然后,用另外一个方法根据 fieldName 和一个字典对象来解析 CLLocationCorrdinate2D :

    
    static func parseCoord(dict: [String: Any], fieldName: String) -> CLLocationCoordinate2D {
      guard let coord = dict[fieldName] as? String else {
        return CLLocationCoordinate2D()
      }
      let point = CGPointFromString(coord)
      return CLLocationCoordinate2DMake(CLLocationDegrees(point.x), CLLocationDegrees(point.y))
    }

    MapKit API 使用 CLLocationCoordinate 2D 来表示一个地理坐标。

    现在可以创建这个类的初始化方法了:

    init(filename: String) {
      guard let properties = Park.plist(filename) as? [String : Any],
        let boundaryPoints = properties["boundary"] as? [String] else { return }
    
      midCoordinate = Park.parseCoord(dict: properties, fieldName: "midCoord")
      overlayTopLeftCoordinate = Park.parseCoord(dict: properties, fieldName: "overlayTopLeftCoord")
      overlayTopRightCoordinate = Park.parseCoord(dict: properties, fieldName: "overlayTopRightCoord")
      overlayBottomLeftCoordinate = Park.parseCoord(dict: properties, fieldName: "overlayBottomLeftCoord")
    
      let cgPoints = boundaryPoints.map { CGPointFromString($0) }
      boundary = cgPoints.map { CLLocationCoordinate2DMake(CLLocationDegrees($0.x), CLLocationDegrees($0.y)) }
    }

    首先,公园的坐标可以从 plist 文件中获取,然后赋给 properties 变量。然后设置边界数组,以便后面用来显示公园的框线。

    你可能奇怪,为什么不用 plist 来设置 overlayBottomRightCoordinate 坐标?这个不需要在 plist 中提供,因为你可以通过其它 3 个点来算出。将当前的 overlayBottomRightCoordinate 替换成一个计算属性:

    var overlayBottomRightCoordinate: CLLocationCoordinate2D {
      get {
        return CLLocationCoordinate2DMake(overlayBottomLeftCoordinate.latitude,
                                          overlayTopRightCoordinate.longitude)
      }
    }

    最后,你需要写一个方法,根据 overlay 坐标创建一个矩形边框。

    将 overlayBoundingMapRect 的声明替换为:

    var overlayBoundingMapRect: MKMapRect {
      get {
        let topLeft = MKMapPointForCoordinate(overlayTopLeftCoordinate)
        let topRight = MKMapPointForCoordinate(overlayTopRightCoordinate)
        let bottomLeft = MKMapPointForCoordinate(overlayBottomLeftCoordinate)
    
        return MKMapRectMake(
          topLeft.x,
          topLeft.y,
          fabs(topLeft.x - topRight.x),
          fabs(topLeft.y - bottomLeft.y))
      }
    }

    这个 getter 方法返回了一个 MKMapRect 对象,用作公园的边界。这是一个简单矩形,定义了这个公园有多大,以及公园的中心坐标。

    接下来要使用这个类了。打开 ParkMapViewController.swift,添加一个属性:

    var park = Park(filename: "MagicMountain")
    

    然后修改 viewDidLoad() 方法:

    override func viewDidLoad() {
      super.viewDidLoad()
    
      let latDelta = park.overlayTopLeftCoordinate.latitude -
        park.overlayBottomRightCoordinate.latitude
    
      // 将一个 span 想象成电视机的尺码,用对角线长度来测量
      let span = MKCoordinateSpanMake(fabs(latDelta), 0.0)
      let region = MKCoordinateRegionMake(park.midCoordinate, span)
    
      mapView.region = region
    }

    这里创建了一个纬度差,它是从公园左上角的坐标到公园有效较的坐标的距离。你用它来创建一个 MKCoordinateSpan,这在地图上表示了一小块横跨区域。然后用这个 span 和公园的中心坐标创建一个 MKCoordinateRegion,然后将 map view 的定位到这个公园的位置。

    Build & run,你会看到现在地图中央已经是六旗魔术山了!

    OK!你已经将公园放在地图中心,这很好,但也不用那么激动。让我们将地图类型修改为卫星视图!

    切换地图类型

    在 ParkMapViewController.swift 中,你应该看到这个方法:

    @IBAction func mapTypeChanged(_ sender: UISegmentedControl) {
      // TODO
    }

    呃,这里有一个很不好的注释~

    幸好,开始项目已经有许多你需要在这个方法中用到的东西了。看到 map view 上面的那个好像毫无用处的 segmented 控件了吗?

    这个 segmented 框架实际上会调用 mapTypeChanged(_:) 方法,但从上面看来,它还没有任何代码!

    将下列实现填充到 mapTypeChanged() 方法中来:

    mapView.mapType = MKMapType.init(rawValue: UInt(sender.selectedSegmentIndex)) ?? .standard

    无论你相信与否,在你的 app 中添加标准地图、卫星地图和混合地图类型就是这么简单!

    Build & run,点一下 segmented 控件就可以修改地图类型!

    尽管卫星地图比标准地图要好看许多,对于一个公园游客来说也没什么鸟用。什么标注都没有——你的用户怎么找得到公园中的地点呢?

    一个简单的方法就是在 map view 上覆盖一个 UIView,但你可以更进一步,用 MKOverlayRenderer 来为你做大量的工作!

    关于 Overlay View 的一切

    在开始编写自己的 overlay view 之前,来看两个关键的类:MKOverlay和 MKOverlayRenderer。

    MKOverlay 告诉 MapKit 你想将覆盖物画在哪儿。这个类的使用分成 3 个步骤:

    1. 创建一个自定义类,实现 MKOverlay 协议,它必须有两个属性:coordinate 和 boundingMapRect。这两个属性指明了覆盖物在地图中的位置以及大小。
    2. 创建一个这个类的实例,用于你想显示覆盖物的每一个地区。以这个 app 为例,你可以创建一个实例用于作为过山车的覆盖物,用另一个实例作为某个饭店的覆盖物。
    3. 最后,将覆盖物添加到 map view。

    现在 map view 知道它需要在哪个地方显示覆盖物,但它不知道在每个地方需要显示什么?

    再来看看 MKOverlayRenderer。这个子类用于说明你想在一个地点上显示些什么。以这个 app 为例,你会画一张过山车或者饭店的图片。

    一个 MKOverlayRenderer 就像一个特殊的 UIView,因为它继承了 UIView,但是,你不需要直接将一个 MKOverlayRenderer 添加到 mapview。相反,MapKit 需要的是一个 MKMapView。

    还记得之前你设置了 map view 的委托吗?它有一个委托方法允许你返回一个 overlay view:

    func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer
    

    MapKit 会在 map view 在显示时觉得某个区域需要一个 MKOverlay 时调用这个方法。

    总之,你不直接添加 MKOverlayRenderer 对象到 map view;而是告诉地图有 MKOverlay 对象会显示,然后在委托方法需要使用它们的时候返回它们。

    现在你已经有理论基础了,接下来使用这些理论吧!

    添加你自己的信息

    就如你前面所看到的,卫星地图也不会提供关于公园的足够的信息。你的任务是创建一个表示整个公园的一个覆盖物对象。

    在 Overlays 文件夹下新建文件 ParkMapOverlay.swift。编辑内容如下:

    import UIKit
    import MapKit
    
    class ParkMapOverlay: NSObject, MKOverlay {
      var coordinate: CLLocationCoordinate2D
      var boundingMapRect: MKMapRect
    
      init(park: Park) {
        boundingMapRect = park.overlayBoundingMapRect
        coordinate = park.midCoordinate
      }
    }

    实现 MKOverlay 协议的同时必须继承 NSObject。最后,初始化方法直接用 Park 参数的属性赋给对应的 MKOverlay 属性。

    现在你需要创建一个视图类,继承 MKOverlayRenderer。

    在 Overlays 目录下新建文件 ParkMapOverlayView.swift。编辑内容为:

    import UIKit
    import MapKit
    
    class ParkMapOverlayView: MKOverlayRenderer {
      var overlayImage: UIImage
    
      init(overlay:MKOverlay, overlayImage:UIImage) {
        self.overlayImage = overlayImage
        super.init(overlay: overlay)
      }
    
      override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
        guard let imageReference = overlayImage.cgImage else { return }
    
        let rect = self.rect(for: overlay.boundingMapRect)
        context.scaleBy(x: 1.0, y: -1.0)
        context.translateBy(x: 0.0, y: -rect.size.height)
        context.draw(imageReference, in: rect)
      }
    }

    init(overlay:overlayImage:)覆盖了基类方法 init(overlay:),通过定义了第二个参数。

    这个类真正重要的是 draw 方法。它定义了 MapKit 要如何渲染这个 view,同时指定一个 MKMapRect、MKZoomScale 和 图形上下文 CGContext,也就是用对应的缩放比例将覆盖图片会知道该上下文中。

    Core Graphics 的绘图细节超出了本文范围。但是,你可以在上面的代码中看到,它使用了传入的 MKMapRect 来获取 CGRect,为的是在指定上下文中将 UIImage 的 CGImage 绘制在指定的地方。如果你想学习更多 Core Graphics 内容,请阅读我们的 Core Graphics 教程系列

    好了!现在我们有了 MKOverlay 也有了 MKOverlayRenderer,可以将它们添加到 map view 中了。

    在 ParkMapViewController.swift 中,添加方法:

    func addOverlay() {
      let overlay = ParkMapOverlay(park: park)
      mapView.add(overlay)
    }

    这个方法会添加一个 MKOverlay 到 map view。

    如果用户选择显示覆盖物,loadSelectedOptions() 会调用 addOverlay()。将 loadSeletedOptions() 替换为:

    func loadSelectedOptions() {
      mapView.removeAnnotations(mapView.annotations)
      mapView.removeOverlays(mapView.overlays)
    
      for option in selectedOptions {
        switch (option) {
        case .mapOverlay:
          addOverlay()
        default:
          break;
        }
      }
    }

    当用户解散选择选项视图,app 会调用 loadSelectedOptions(),然后判断用户选定的选项并调用对应的方法将这些选项显示在 map view。

    loadSelectedOptions() 还会删除所有大头钉和覆盖物,以避免重复绘制。这样的效率确实不高,但却是一种简单的清除上一次地图内容的方法。

    在 MKMapViewDelegate 扩展中添加下列方法以实现该委托方法:

    func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
      if overlay is ParkMapOverlay {
        return ParkMapOverlayView(overlay: overlay, overlayImage: #imageLiteral(resourceName: "overlay_park"))
      } 
    
      return MKOverlayRenderer()
    }

    当 app 发现视图中有一个 MKOverlay 时,map view 会调用上面的委托方法。

    在这个方法中,你判断了 overlay 的类型是否是 ParkMapOverlay,如果是,加载这个 overlay 图片,将这个对象返回给调用者。

    有一个地方忘记说了——这个 overlay_park 图片是哪来的?

    这是一个 PNG 文件,目的是盖在 map view 上作为公园的边界。overlay_park 图片(在 image assets 中)大概是这个样子:

    Build & run,选择 Map Overlay 选项,啊!在地图上出现了一个公园的样子:

    放大,缩小,随意移动——这个覆盖物会随之缩放和移动。太棒了!

    标注

    如果你曾经在地图 app 中搜索过地名,你肯定见过地图中显示的彩色图钉。这就是标注,用 MKAnnotationView 创建的。你可以在自己的 app 使用标注——你可以使用任意图片而不仅仅是图钉。

    在你的 app 中也会用到标注,为游客指出某些景点。标注对象和 MKOverlay 和 MKOverlayRenderer 类似,但使用的是 MKAnnotation 和 MKAnnotationView。

    在 Annotations 文件夹中新建一个 Swift 文件 AttractionAnnotation.swift,编辑内容如下:

    import UIKit
    import MapKit
    
    enum AttractionType: Int {
      case misc = 0
      case ride
      case food
      case firstAid
    
      func image() -> UIImage {
        switch self {
        case .misc:
          return #imageLiteral(resourceName: "star")
        case .ride:
          return #imageLiteral(resourceName: "ride")
        case .food:
          return #imageLiteral(resourceName: "food")
        case .firstAid:
          return #imageLiteral(resourceName: "firstaid")
        }
      }
    }
    
    class AttractionAnnotation: NSObject, MKAnnotation {
      var coordinate: CLLocationCoordinate2D
      var title: String?
      var subtitle: String?
      var type: AttractionType
    
      init(coordinate: CLLocationCoordinate2D, title: String, subtitle: String, type: AttractionType) {
        self.coordinate = coordinate
        self.title = title
        self.subtitle = subtitle
        self.type = type
      }
    }

    首先定义了一个枚举 AttractionType,用于区分景点类型。这个枚举列出了标注的四种类型:misc、rides、foods 和 first aid。还有一个函数用于返回对应的标注图片。

    然后声明本类使用了 MKAnnotation 协议。和 MKOverlay 一样,MKAnnotation 也有一个必须的 coordinate 属性。在类中定义了一些属性。最后定义了一个初始化函数,允许你直接对这些属性初始化。

    现在必须创建一个 MKAnnotation 实例以用于你的标注。

    在 Annotations 文件夹中再新建一个文件 AttractionAnnotationView.swift,编辑内容为:

    import UIKit
    import MapKit
    
    class AttractionAnnotationView: MKAnnotationView {
      // 必须实现
      required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
      }
    
      override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
        super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
        guard let attractionAnnotation = self.annotation as? AttractionAnnotation else { return }
    
        image = attractionAnnotation.type.image()
      }
    }

    MKAnnotationView 必须实现 init(coder:) 初始化函数。如果不实现这个,会出现编译错误。为此,简单地定义这个方法并调用父类初始化函数。这里,你还覆盖了 init(annotation:resuseIdentifier:) 方法,根据标注的类型,设置不同的图片给这个标注的 image 属性。

    创建完标注和对应的 view,你可以将它们添加到 map view 中了!

    要获得每个标注的坐标,你可以使用 MagicMountainAttractions.plist 文件,它位于 Park Information 文件夹下面。这个 plist 文件包含了公园景点的坐标和相关信息。

    回到 ParkMapViewController.swift,添加方法:

    func addAttractionPins() {
      guard let attractions = Park.plist("MagicMountainAttractions") as? [[String : String]] else { return }
    
      for attraction in attractions {
        let coordinate = Park.parseCoord(dict: attraction, fieldName: "location")
        let title = attraction["name"] ?? ""
        let typeRawValue = Int(attraction["type"] ?? "0") ?? 0
        let type = AttractionType(rawValue: typeRawValue) ?? .misc
        let subtitle = attraction["subtitle"] ?? ""
        let annotation = AttractionAnnotation(coordinate: coordinate, title: title, subtitle: subtitle, type: type)
        mapView.addAnnotation(annotation)
      }
    }

    这个方法读取了 MagicMoutainAttractions.plist,遍历数组中的每个字典。对于每个字典,它都会创建一个 AttractionAnnotation 实例,设置这个景点的信息,然后添加到 map view。

    现在你需要根据新选项修改 loadSelectedOptions() 方法,当用于选择它时,执行你的新方法。

    修改 loadSelectedOptions() 方法中的 switch 语句:

    case .mapPins:
      addAttractionPins()

    这会在必要的时候调用你的 addAttractionPins() 方法。注意 removeOverlays 一句会隐藏覆盖物。

    快完了!但还有一点,你必须实现另一个委托方法,为 map view 提供 MKAnnotationView 对象,这样它才能画出它们。

    在 MKMapViewDelegate 类扩展中添加方法:

    func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
      let annotationView = AttractionAnnotationView(annotation: annotation, reuseIdentifier: "Attraction")
      annotationView.canShowCallout = true
      return annotationView
    }

    这个方法接收指定的 MKAnnotation,用它创建 AttractionAnnotationView。因为 canShowCallout 设置为 true,当用户点击这些标注时会弹出一个气泡。最后,方法返回了 annotation view。

    Build & run,看看你的标注吧!

    打开 Attraction Pins 选项,效果如下图所示:

    这些标注显得非常“显眼”!

    到此为止,你已经学习了许多 MapKit 的高级内容,比如覆盖物和标注。但如果有时候必须绘制几何图形怎么办?比如线、形状和圆?

    MapKit 也允许你在 map view 上直接绘图。MapKit 提供了 MKPolyline、MKPolygon、MKCircle。

    我一往无前——MKPolyline

    如果你去过迷幻山,你会知道歌利亚过山车非常陡峭,有些乘客在一进门的时候就会朝它直奔而去!

    为了帮助这些乘客,你需要从入口绘制一条路线到达歌利亚。

    MKPolyline 是一种绘制路线的好方法,它会连接多个点,比如从 A 点到 B 点绘制一条非线性的路线。

    要绘制多线段,你需要有一系列经纬度坐标,以便代码能够绘制出它们。

    EntranceToGoliathRoute.plist(还是在 Park Information 文件夹)包含了这些路径信息。

    你需要有一种方法读取这个 plist 并创建一条路径,为乘客们导航。

    打开 ParkMapViewController.swift,添加方法:

    func addRoute() {
      guard let points = Park.plist("EntranceToGoliathRoute") as? [String] else { return }
    
      let cgPoints = points.map { CGPointFromString($0) }
      let coords = cgPoints.map { CLLocationCoordinate2DMake(CLLocationDegrees($0.x), CLLocationDegrees($0.y)) }
      let myPolyline = MKPolyline(coordinates: coords, count: coords.count)
    
      mapView.add(myPolyline)
    }

    这个方法读取了 EntranceToGoliathRoute.plist,将每个坐标字符串转换成 CLLocationCoordinate2D 结构。

    值得注意的是,在 app 实现你的多线段非常简单,直接创建一个包含了所有点的数组,把它传递给 KMPolyline 就可!还有什么比这个更简单的呢?

    然后你需要添加一个选项,允许用户打开或关闭多线段。

    修改 loadSelectedOptions() 方法,添加另一个 case 语句:

    case .mapRoute:
      addRoute()

    适时地调用了 addRoute() 方法。

    最后将所有代码串接在一起。你必须修改委托方法,以便返回 map view 要用到的 view。

    修改 mapView(_:rendererForOverlay) 方法:

    func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
      if overlay is ParkMapOverlay {
        return ParkMapOverlayView(overlay: overlay, overlayImage: #imageLiteral(resourceName: "overlay_park"))
      } else if overlay is MKPolyline {
        let lineView = MKPolylineRenderer(overlay: overlay)
        lineView.strokeColor = UIColor.green
        return lineView
      }
    
      return MKOverlayRenderer()
    }

    要改的地方是多出来的 else if 语句,这是针对 MKPolyline 对象的。显示 polyline 的过程非常类似于 overlay 的过程。但是,这里不需要创建任何自定义的视图对象。直接用框架提供的 MKPolylineRenderer,用 overlay 初始化一个新的示例。

    MKPolylineRenderer 也允许你修改多线段的某些属性。在这里,你修改了绘制线段的颜色为绿色。

    Build & run,打开 Route 选项,它会显示成:

    歌利亚迷现在可以用前所未有的速度来找到过山车了!

    在公园真实旁边显示公园的资助人也挺好呀,因为公园并没有完全占满整个屏幕。

    虽然你可以用 MKPolyline 绘制公园边界,但 MapKit 有另外一个类专门用于绘制封闭的多边形:MKPolygon。

    别挡住我进去——MKPolygon

    MKPolygon 和 MKPolyline 非常类似,只不过在它的坐标集中,起点和终点是连接的,创建出来的是一个封闭图形。

    你可以创建一个 MKPolygon 用于表示公园的边界。公园边界坐标已经定义在 MagicMountain.plist 中,回去看一眼 init(filename:) 方法,是从 plist 文件的什么地方获取的。

    在 ParkMapViewController.swift 中添加方法:

    func addBoundary() {
      mapView.add(MKPolygon(coordinates: park.boundary, count: park.boundary.count))
    }

    这个方法非常简单。通过读取 park 对象的 boundary 数组和端点数,你可以快速和轻易地创建一个 MKPolygon 对象!

    接下来要做什么呢?和上面的 MKPolyline 非常类型。

    耶,对了——在 loadSelectedOptions 中添加另外一个 case 语句,以便显示/隐藏公园的边界:

    case .mapBoundary:
      addBoundary()

    和 MKPolyline 一样 MKPolygon 也使用了 MKOverlay 协议,因此你必须再次修改 delegate 方法。

    在 ParkMapViewController.swift 将委托方法修改为:

    func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
      if overlay is ParkMapOverlay {
        return ParkMapOverlayView(overlay: overlay, overlayImage: #imageLiteral(resourceName: "overlay_park"))
      } else if overlay is MKPolyline {
        let lineView = MKPolylineRenderer(overlay: overlay)
        lineView.strokeColor = UIColor.green
        return lineView
      } else if overlay is MKPolygon {
        let polygonView = MKPolygonRenderer(overlay: overlay)
        polygonView.strokeColor = UIColor.magenta
        return polygonView
      }
    
      return MKOverlayRenderer()
    }

    要改的地方和之前类型。你创建了一个 MKPolygonRenderer 对象的 MKOverlayView,然后设置它的绘制颜色为品红色。

    运行app 是这个样子:

    这就是多线段和多边形。最后一个要绘制的图形是圆形,也就是 MKCircle。

    沙滩上的圆圈——MKCircle

    MKCircle 和 MKPolyline 和 MKPolygon 还是很像,只不过它画的是圆,需要指定一个坐标点作为它的圆心,以及一个半径指定圆的大小。

    将每个公园的人像的常见位置标记出来也很好啊!将这些人像在地图上用小圆点画出来。

    MKCircle 就能非常轻易地实现这个功能。

    在 Park Information 文件夹包含了这些人物的坐标文件。每个文件是一个数组,包含了用户看见过的人像坐标。

    在 Models 文件夹下新建一个文件 Character.swift。编辑内容为:

    import UIKit
    import MapKit
    
    class Character: MKCircle {
    
      var name: String?
      var color: UIColor?
    
      convenience init(filename: String, color: UIColor) {
        guard let points = Park.plist(filename) as? [String] else { self.init(); return }
    
        let cgPoints = points.map { CGPointFromString($0) }
        let coords = cgPoints.map { CLLocationCoordinate2DMake(CLLocationDegrees($0.x), CLLocationDegrees($0.y)) }
    
        let randomCenter = coords[Int(arc4random()%4)]
        let randomRadius = CLLocationDistance(max(5, Int(arc4random()%40)))
    
        self.init(center: randomCenter, radius: randomRadius)
        self.name = filename
        self.color = color
      }
    }

    这个类实现了 MKCircle 协议,定义了两个可空属性:名字和颜色。这个便利初始化方法有一个 plist 文件名和一个颜色作为参数用于绘制圆点。然后从 plist 文件中读取数据,然后从文件中的 4 个坐标中选择一个随机的坐标。然后设置 MKCircle 并返回,准备放到地图中去吧!

    现在你需要一个添加每个人像的方法。打开 ParkMapViewController.swift 添加方法:

    func addCharacterLocation() {
      mapView.add(Character(filename: "BatmanLocations", color: .blue))
      mapView.add(Character(filename: "TazLocations", color: .orange))
      mapView.add(Character(filename: "TweetyBirdLocations", color: .yellow))
    }

    这个方法在每个人像进行了同样的操作。每个都传入一个 plist 文件名和颜色,然后在地图上添加一个覆盖物。

    快大功告成了!你还能想起后续的步骤吗?

    对,你仍然要给 map view 提供 MKOverlayView,这是通过委托方法来进行的。

    修改 ParkMapViewController.swift 中的委托方法:

    func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
      if overlay is ParkMapOverlay {
        return ParkMapOverlayView(overlay: overlay, overlayImage: #imageLiteral(resourceName: "overlay_park"))
      } else if overlay is MKPolyline {
        let lineView = MKPolylineRenderer(overlay: overlay)
        lineView.strokeColor = UIColor.green
        return lineView
      } else if overlay is MKPolygon {
        let polygonView = MKPolygonRenderer(overlay: overlay)
        polygonView.strokeColor = UIColor.magenta
        return polygonView
      } else if let character = overlay as? Character {
        let circleView = MKCircleRenderer(overlay: character)
        circleView.strokeColor = character.color
        return circleView
      }
    
      return MKOverlayRenderer()
    }

    最后,修改 loadSelectedOptions() 方法允许用户打开/关闭人像位置:

    case .mapCharacterLocation:
      addCharacterLocation()

    你还可以删除默认的 default: 和 break 语句,因为你已经穷举了所有 case。

    Build & run,打开人像看看每个角色人物都藏在了什么地方?

    接下来做什么?

    恭喜你!你已经体验过 MapKit 所提供的大部分功能了。通过几个简单功能,你就编写好了一个完整可用的地图 app,包括了标注、卫星地图和自定义覆盖物。

    这里可以下载最终的示例项目。

    生成覆盖物的方法有许多,可以非常简单,也可以非常复杂。在本教程采用了 overlay_park 图片的方式是一种简单——但乏味的方式。

    有更好——但更高效的做法来创建覆盖物。其中一种就是使用 KML 文件,MapBox 瓦片或者其它第三方资源。

    希望你喜欢这篇教程,希望在你的 app 中看到你使用 MapKit 的覆盖物。有任何问题或评论,请去论坛中发言。

    展开全文
  • GitHub上Swift开源项目!

    2017-02-04 19:10:16
    swift-open-project这里汇集了目前为止最为流行的Swift开源项目,选取SwiftGuide中的开源部分,这里将每周对项目进行总结,如果大家有推荐的可以联系thinkloki@gmail.com,或者issues,欢迎Star、Fork。感谢...

    swift-open-project

    这里汇集了目前为止最为流行的Swift开源项目,选取SwiftGuide中的开源部分,这里将每周对项目进行总结,如果大家有推荐的可以联系thinkloki@gmail.com,或者issues,欢迎Star、Fork。感谢SwiftGuide

    1. 实用类库

    以下实用类库做为快速参考,更多实用类库需求,也可以去这里(Swift toolbox is a community-supported catalog
    of iOS and OSX libraries)
    (By Adam Leonard from swiftcast.tv), 这个网站收藏了 GitHub 开源社区中优质 Swift 资源库,并提供了分类及查询服务。

    类库 相关文章 备注
    Alamofire/Alamofire Alamofire 为其著名的 AFNetworking 用 Swift 重写的网络基础库
    mattt/Euler Swift Operators 这样写是否很数学、很逻辑?∛27÷3±5, ∑[3,1,2], ~0⊻1
    mattt/Surge 基于苹果 Accelerate 高性能计算框架库,计算效率提升惊人
    mattt/Literally Swift Literal Convertibles 常用数据类型的使用及转换工具库

    * 工具类
    * Dollar:尤其在 Array 和 Dictionary 的支持上(实现类库仅一个 Dollar.swift 文件)。用过 Javascript 版的 Lo-Dash 或 underscore 就知道其实用程度。它是一套函数化编程的工具库。另一个类似的更完整的版本是 ExSwift,它的实现基于对象化扩展的。
    * ExSwift:实用类扩展库。另一个 Lo-Dash 或 underscore 的 Swift 版本实现,相对于 Dollar.swift,此版本有更完整的 API 实现。包括了对 Array, Int, String, Float, Range, Dictionary, NSArray 的扩展及其它实用方法。完全遵循面向对象的扩展原则。
    * lingoer/SwiftyJSON:这是解析 JSON 字符串封装类。实现功能与 Javascript中 的 JSON.parse 相近,使用方便。By @lingoer
    * SwiftyJSON/Alamofire-SwiftyJSON :简单地整合 Alamofire 和 SwiftyJSON 后,远程 JSON 使用方便了。
    * swift-serializeswift object serialize/deserialize of json json序列化跟反序列化
    * matthewcheok/JSONCodable:基于 Swift 2.0 新特性(Protocol Extensions and Error Handling)的JSON 解析类。
    * typelift/Basis:这个实用类库支持范围很宽范,封装主要以简化及方便调用系统API为主,使程序代码看上去更优雅。遗憾地是,目前作者并没有提供 API 文档或示例代码,对于初学者使用会额外带来一点点学习成本。
    * owensd/json-swift:功能与 Swifty 几乎一致,使用上要更方便。
    * gfx/Swift-JsonSerializer:JSON 解析又多了一种选择。
    * Ahmed-Ali/JSONExport:一款实用工 具,可以将 JSON 转换并输出为 Swift 语言类模型(也支持Android上的Java),支持简单的定制。
    * hubertr/Swell: 同时支持 Swift 和 OBJC 的 Log 实用类。喜欢 Log4j 风格的日志管理类的同学可以收入。
    * RSBarcodes_Swift:”RSBarcodes allows you to read 1D and 2D barcodes using metadata scanning capabilities introduced with iOS 7 and generate the same set of barcode images for displaying and sharing.” By @yeahdongcn Swift 语言版二维码识别及生成类库。
    * SwiftP2P/SwiftSSL:常用 Digest 和 HMAC 的封装。如此封装后,使用加密算法确实很方便。很不错的一个国产”轮子”!By @你全家都快到碗里来
    * Hearst-DD/ObjectMapper:对象与 JSON 互转实用类库。需要的同学可以收一下。
    * DaveWoodCom/XCGLogger : 这是一款功能比较健全日志输出框架(Log4Swift)。之前推荐过一款类似的 hubertr/Swell,但它的功能明显没有这款强大。
    * isair/JSONHelper :又一款JSON转对象类库,算上,lingoer/SwiftyJSON,owensd/json-swift,gfx/Swift-JsonSerializer,已经有四款了。同学们根据喜好及需求,择优使用吧。
    * kylef/Stencil:通过在模板中支持 if, for, ifnot 等语法标签,实现更强大、更灵活的数据及格式输出。
    * kylef/PathKit: 简单、实用的文件路径操作类,常用功能一个也不少。
    * thoughtbot/Argo:一个带有实战说明的 JSON 解析方案及类库。
    * typelift/swiftz: 一个帮助实现函数式编程的类库,定义了一系列相应的数据结构。
    * dongri/OAuthSwift:Swift 版 OAuth(1/2) 授权类库。对于作者提供的 API,看着舒服,用着方便。
    * p2/OAuth2:独立又纯粹的 OAuth2 认证授权类库,作者所提供的示例及流程说明清晰又简洁。
    * tadija/AEXML:简单又易于使用的XML解析类。作者提供了完整的示例程序。
    * duemunk/Async:一个后台执行代码封装库,它可以让你的后台代码封装语法更简洁。
    * jquave/EasyCast:这是一个自动强制转换操作符左右类型及常用数学函数参数类型无障碍使用的类库。@onevcat几个月前也写过类似功能库 onevcat/Easy-Cal-Swift,不过,停滞更新很久了。
    * ArtSabintsev/Siren:当应用更新时,通知用户并提供 App Store 链接供用户更新的开源库。P.S. 该开发者也曾提供 Objectvie-C 语言版ArtSabintsev/Harpy
    * kishikawakatsumi/KeychainAccess:这套Keychain管理比较完整。支持本地应用,网络应用,以及与Touch ID的集成应用。
    * nori0620/SwiftFilePath:功能强大的文件及路径管理封装类库。
    * onevcat/RandomColorSwift:”刚刚开源了一个自动生成好看的颜色的 Swift 库,这是 randomColor.js 项目的 Swift 移植版本。在学习 Swift 的童鞋不妨一看,如果可以对于大家在 Swift 实际使用中的某些概念和写法有所启迪的话就再好不过了。” By @onevcat
    * beltex/SMCKit:Mac 系统管理控制器 SMC(System Management Controller)Swift 语言版封装库。通过该类库,简单的几行代码就可知 CPU 温度,风扇转速,电池状态等硬件状态或活动状态。
    * radex/SwiftyUserDefaults:轻量级数据存储类 NSUserDefaults 扩展类,它使类型数据访问和存储更为便捷、直观。
    * akosma/SwiftMoment:时间和日历函数封装库,也是经典的 moment.js 的 Swift 语言版本实现。
    * imaginary-cloud/CameraManager:相机管理封装类库。看着极好用的样子。
    * yannickl/QRCodeReader.swift:一款简单的 QR 二维码阅读组件及示例,提供前后相机切换功能。
    * aaronabentheuer/AAFaceDetection:简单、实用的面部识别封装库。虽然该技术从 iOS 5 发展,不过真正有趣的应用还不多。例如:在视频直播中可以检测观众情感(微笑或眨眼),或注意力(看着屏幕或没有)。因此,作者对其抱以很高的期待,并已经着手开发实验室类大项目。
    * zenangst/Versions:版本比较小工具。
    * NorthernRealities/Rainbow:旨在提高代码可读性及易用性的 UIColor 扩展,它使原先有限的预定义颜色(方法)选择,扩展至超过 1200 种。
    * jpotts18/swift-validator:基于规则的输入验证类库。项目良好的面向对象设计思想,使规则的扩展及自定义非常方便。P.S. 对于规则,若有更复杂的需求,则需要有更专业的规则引擎(甚至是基于自然语言的规则配置)解决方案。比如:开源的 Drools,商用的 ILOG 等。
    * nghialv/Transporter:短小、精悍、易用的多文件(并发或顺序)上传和下载传输库。还支持后台运行、传输进程跟踪、暂停/续传/取消/重试控制等功能。
    * krzyzanowskim/CryptoSwift:纯 Swift 版 Crypto 算法及相关功能类库。是一套易用、易扩展、完整、不可或缺的功能库。
    * nomothetis/SemverKit:针对符合『语义化版本规范 2.0.0』版本号的解析、比较运算类库。不仅支持 Major, Minor, Patch,还支持 Alpha 和 Beta 预发布版本,以及相应地递增运算扩展。
    * harlanhaskins/Punctual.swift:通过一系列基础数据类型扩展,使日期访问及操作更加自然语言化。若没有动态语言化地超强扩展及定制能力,如此优雅的日期操作是无法实现的。
    * ayanonagon/Parsimmon:小而美的语言学类库封装工具包。提供分词、标记词性、词形归并、朴素贝页斯分类、决策树等自然语言分析小工具。P.S. 英语分词效果好于中文,感兴趣的同学可以针对中文做一些优化开发。参考译文 NSHipster - NSLinguistic​Tagger
    * soffes/X:等不及苹果 iOS/OS X 平台统一的开发同学(Sam Soffes),启了一个项目,先将常用类库统一了。该项目刚开始,感兴趣的同学可以考虑参与其中。
    * naoty/Timepiece:直观(自然语言化)的日期类扩展。它使日期加减运算、初始设置、日期变更以及基于指定格式解析转换变得方便、易读。P.S. 相比前一段时间推荐另一个类似日期库(Punctual.swift O网页链接)功能更丰富。
    * macoscope/SwiftyStateMachine:应用好状态机,不仅提高程序可维护性,更大大增强了程序健壮性。
    * benzguo/MusicKit:一套音乐编辑及创作框架。懂音乐、玩音乐的同学值得拥有。
    * ArtSabintsev/FontBlaster:调用自定义字体封装类。很实用、很方便。
    * phimage/Prephirences:这个库可以让开发者更方便管理、读写应用的配置信息。很实用。
    * radex/SwiftyTimer:一个更友好、更简单的时间操作 NSTimer 扩展。P.S. 类似针对日期有 Punctual.swift 和 Timepiece。
    * liufan321/SwiftQRCode:简单、实用的 QR Code 扫描识别及生成库。
    * emaloney/CleanroomLogger:相当于 CocoaLumberjack 或 Log4j 的 Swift 版本,功能上甚至更强大。另外,源代码中已经内含了完整的 API 文档,使用非常方便。
    * Khan/Prototope:一款轻量的 Javascript 桥接 Swift 库。对于 JS 控,或许可以从玩这个项目开始 iOS 原型开发。
    * malcommac/SwiftDate:特别完整、强大的日期操作管理类库。它几乎涵盖了已知开源日期类库所有优秀特性。是一款值得毫不犹豫收录的类库。
    * nghialv/Future:基于微框架设计思想的异步执行及结果响应类。由示例发现,代码即简单又干净。
    * alexeyxo/protobuf-swift:Protocol Buffers 的 Swift 语言实现库。P.S. Protocol Buffers 是 Google 开源项目,主要功能是实现直接序列化结构化的对象数据,方便跨平台快速传递,开发者也可以直接修改 protobuf 中的数据。相比 XML 和 JSON,protobuf 解析更快,存储更小。
    * soffes/SyntaxKits:TextMate 风格语法高亮类库(基于 Swift 2.0)。
    * glock45/swifter:一款极其轻量的 HTTP 服务器引擎。#Swift 2#
    * andreacremaschi/GEOSwift:可轻松地处理地理对象模型和相关地形操作,是一个极其易用的地理封装库(集成 MapKit 和 Quicklook)。绝佳好轮子,不容错过。

    • 图片
    • kaishin/ImageScout:最小网络代价获得图片大小(宽和高)及类型,它对于很多场合,比如:布局和排版都有积极作用。
    • kaishin/gifu:高性能 GIF 显示类库 Swift 版本。
    • indragiek/DominantColor:采用YUV色域编码规则及K均值聚类算法提取图片代表色开源项目。很实用,很赞!
    • iSame7/Panoramic:该项目通过 Core Motion 实现了变换角度移动手机时,使图片显示产生对应位置及角度的变换。从而达到了具有现场感的图片浏览功能。
    • gavinbunney/Toucan:小而美的图片变换及处理类(支持:resize, clip, crop, scale, Ellipse Mask, Rounded Rect Mask, Image Mask)。功能略有超预期的感觉。
    • schwa/SwiftGraphics:一个易学、易用的绘图工具库封装。作者很勤劳,项目正在持续更新中。
    • contentful-labs/Concorde:一个可用于下载和解码渐进式 JPEG 的库。特定场合很有用。类库的调用方法也很自然、方便。
    • FlexMonkey/SnapSwift:一款 Snapseed 风格滤镜及参数编辑菜单。弹出菜单项后,上下、左右滑动即可调整滤镜参数。
    • kiavashfaisali/KFSwiftImageLoader:极高性能、轻量、低功耗网络图片异步加载扩展库(支持 iOS 和 WatchKit)。良好的缓存管理机制保证的作者设计初衷。P.S. 作者曾开发过 KFWatchKitAnimations:一款为  Watch 提供 60 帧动画显示效果解决方案及示例。
    • yannickl/DynamicColor:强大的颜色操作扩展类。通过该类,你可以通过扩展方法基于某个颜色得到不同深浅、饱和度、灰度、色相,以及反转后的新颜色。是不可多得的好类库。
    • Jan0707/SwiftColorArt:根据图片色系决定所在界面背景色及字体显示颜色。类库及示例代码也很简洁且易于使用。这样的项目真的带给我惊艳。何况,该开发者备注了这是他的第一个Swift项目。
    • jathu/UIImageColors:又一款根据图片获取主色、次色、背景色、详细色的类。P.S. 另一款功能相同的类是 SwiftColorArt

    • 存储

    • SQLiteDB in Swift:”This is a basic SQLite wrapper for Swift. It is very simple at the moment and does not provide any advanced functionality. Additionally, it’s not pure Swift at the moment due to some difficulties in making all of the necessary sqlite C API calls from Swift.”
    • Alecrim/AlecrimCoreData:更容易地访问 CoreData 对象封装类库。除了 CRUD,还提供指针定位,强大的排序、筛选,异步数据获取,以及独立线程后台存取数据。
    • Realm - a mobile database:Realm主打移动数据库。除了更轻量,甚至还可以应用到可穿戴。低耦、面向对象的设计风格也是非常亮丽的。
    • nerdyc/Squeal:一套非常完善的 SQLite 数据库访问 API,无论建库,建表,CRUD 等常用数据库操作命令都进行了很好的封装,另外,版本管理、事务管理、并发管理、命令执行及 Prepare Statement 等都提供相应支持。它是一套不可多得的SQLite管理访问库。
    • stephencelis/SQLite.swift:简单、轻量,或是使用上最 SQL 的 SQLit e封装库。
    • BjornRuud/Swiftache:Mustache 的 Swift 语言实现版本。
    • Haneke/HanekeSwift:貌似这个轻量地带缓存图片组件还不错。主要功能包括图片二级缓存、异步加载、后台执行、自动缩放等。感兴趣的同学可以试用一下。
    • SugarRecord/SugarRecord:相对于 SQL,CoreData 可以更方便、高效存储数据,而 SugarRecord 类库可以让你更方便的使用 CoreData,同时作者已经封装好 iCloud,使你的应用开发更如虎添翼。
    • michaelarmstrong/SuperRecord : Swift 版 CoreData 框架扩展类库。相对于同类型 CoreData 框架库 SugarRecord/SugarRecord,SuperRecord 要简单、轻量得多。很棒的快速开发类库。
    • aschuch/AwesomeCache:Swift 语言写的高效能缓存对象存储及管理,定义及使用简单、易于理解。支持为每个 Cache 对象设定缓存有效期。
    • daltoniam/Skeets:网络图片的获取、缓存及显示类库,支持缓存的清理及时效性管理。
    • evermeer/EVCloudKitDao:一套使用 CloudKit 简单封装库。作者还提供了示例项目及程序供开发者参考使用。
    • andrelind/Breeze:轻量 CoreData 管理封装库。支持 iCloud 。
    • arkverse/SwiftRecord:基于 Core Data 极为轻量、易用的对象持久化工具库,相当于 ActiveRecord 的 Swift 版本。
    • JohnEstropia/CoreStore:提供高可读性,一致性及安全性的 Core Data 管理类库。 其中事务管理及查询是其比较大的亮点,整套 API 功能完整,风格传统又实用。非常值得收入。

    • 远程访问

    • Minimalistic Swift HTTP request agent for iOS and OS X:一个简单、小巧、实用的 HTTP 请求 Swift 语言实现类(仅一个 Agent.swift 类)
    • lingoer/GRequest:”@李洁信:个人认为 AFNetworking 这种较重的第三方网络库将逐渐淡出舞台,而像楼主这种基于 NSURLSession 并充分利用 Swift 特性写出来的小而美的库会受到更多人的青睐!” 作者解读《GRequest for HTTP Request》
    • daltoniam/SwiftHTTP:Thin wrapper around NSURLSession in swift. Simplifies HTTP requests。感兴趣的可以看看代码,比较与 lingoer/GRequest 差别。
    • AshFurrow/Moya:如果你需要如此功能的 Swift 网络 API,可以考虑使用它。它基于 Alamofire, swfitz 等优秀开源类实现。
    • aleclarson/emitter-kit:用 EmitterKit 代替 NSNotificationCenter。这个库貌似短小、精悍、实用的样子。更重要是语法精练。
    • daltoniam/starscream:Swift 版本 WebSocket 客户端类库,支持 iOS/OS X 。 使用方便,跨平台开发不可或缺的“轮子”。
    • shuoli84/SocketIOCocoa:又一款 WebSocket 实用客户端类库,作者提供了详细的测试用例可以做为示例代码使用。
    • socketio/socket.io-client-swift:WebSockect 客户端类库。开放的通讯协议,有利于构建强大地跨平台应用。
    • marcelofabri/BigBrother:网络请求活动状态监视器类。由于 Alamofire 库并无此项特性,因此它可以做为 Alamofire 的一个扩展功能。
    • ishkawa/APIKit:为不同来源或类型 Web 服务,提供安全地可定制化网络请求 API 的基础类库。很好的面向对象设计思路实例。
    • contentful-labs/Stargate:通过 iPhone 桥接实现 Mac 与 Watch 的即时通讯。Stargate 通过封装两个优秀的基础类库 MMWormhole 和 PeerKit 实现高效的通讯应用。
    • JustHTTP/Just:小而美的 HTTP 类。功能简单、直接、完整且健壮性高。
    • CloudKitSpace/CKSIncrementalStore:基于 CloudKit 服务器实现多终端数据同步。附示例动态图。

    • 社交网络

    • lingoer/SwiftWeiboKit:”封装了整个 OAuth 2.0 的授权流程,并提供了几个简便易用的请求方法”
    • A Twitter framework for iOS & OS X written in Swift:一套很完整的 Twitter 访问框架类库,支持 iOS/OS X

    • 框架

    • robb/Cartography:Set up your Auto Layout constraints declaratively. 这是有关自动布局约束一个实用的 Swift 项目,代码看似简单清晰,不过由于设计巧妙。代码解读 By @kaiyuz
    • railsware/Sleipnir:一个基于 Swift 的行为驱动开发框架(BDD-style framework)。API 安装,示例及说明相对比较齐全。
    • Quick/Quick:另一款基于 Swift 的行为驱动开发框架。
    • inamiy/SwiftTask: 一个很标准的任务及其生命周期管理类库。作者还附上了一个基于 Alamofire 库完成的网络文件下载的任务管理示例。
    • inamiy/SwiftState:Swift 版本 State Machine,这是 SwiftTask 的姐妹篇。开发过 Workflow 类应用的同学有没有很亲切。
    • colemancda/NetworkObjects:基于Swift的轻量版HttpServer框架,可以做为iOS/OS X分布式对象的替代。可惜缺少演示或示例代码。
    • Masonry/Snappy:一个基于 AutoLayout 封装,语法简洁的轻量级布局框架。当然,用代码还是 storyboard,全凭开发者喜好。若偏好可视化自动布局,@DevTalking 的两篇译文值得参考《Swift 自适应布局教程》《为 iPhone 6 设计自适应布局》
    • jpsim/PeerKit:基于事件驱动的多端对等连接开源框架。作者提供了该组件使用案例jpsim/CardsAgainst,具体引用代码详见这里
    • Quick/Nimble:功能更强于Xcode自带测试框架(XCTest)的测试类库。
    • venmo/DVR:针对网络请求的测试框架,超实用的工具。且支持 iOS, OSX, watchOS 全平台。
    • owensd/swift-perf:针对 Swift 的性能测试方案。通过抽样场景关键代码与 C 或 Objective-C 的比较做为其测试基准。
    • nghialv/MaterialKit:Swift 版 Google Material Design 组件库。虽然个人并不推荐在 iOS 平台上使用非 iOS 风格界面,不过,对于 Android 强迫症“患者”,多一种选择也是友好的。顺带附上 Google 提供的开源Material Design 图标集,可以一起收入。
    • facebook/AsyncDisplayKit:它是由 Facebook 开发并用于其 Paper 应用的 iOS UI 框架,它主要解决问题是提供界面的高流畅性切换及更灵敏的响应,解决思路是将部分 UI 渲染及复杂计算交由独立于主线程的处理程序负责。中译教程《AsyncDisplayKit 教程 - 达到 60 FPS 的滚动帧率 By @nixzhu》,教程原文《AsyncDisplayKit Tutorial: Achieving 60 FPS scrolling》。
    • mutualmobile/MMWormhole:提供了 iOS 应用与扩展之间的消息传递,其最有价值之处当属 iPhone 与  Watch 间的消息传递,从而实现相互操控的功能。
    • Thomvis/BrightFutures:漫长或复杂计算由独立线程异步来完成。
    • Khan/Prototope:虽然看不懂这套用户交互层面的轻量级原型框架用处在哪里(哪位同学能给俺提示一下)?不过,作者将这套框架的 Swift API完整桥接为 JS 接口倒是很有意思。
    • MHaroonBaig/MotionKit:为核心运动框架(The Core Motion framework)提供友好的类库封装,以更方便使用三轴陀螺仪和加速感应器特性。
    • uber/jetstream-ios:一款 Uber 的 MVC 框架。它同时提供了多用户实时通讯支持,一旦启动 JetStream 后端服务,通过 WebSocket 协议可以分分钟建立多用户实时通讯应用。
    • jessesquires/JSQNotificationObserverKit:一款轻量、易用的通知发送及响应框架类库。作者是知名开源项目 JSQMessagesViewController(Objective-C 版即时聊天)的作者 Jesse Squires ,也是一位知名博主,收录过其文章『An analysis of sorts between Objective-C and Swift』。
    • morizotter/TouchVisualizer:实用的多点触摸可视化组件。扩展并作用于 UIWindows,结构上提供了简单地针对触摸显示定制,比如触摸点的颜色。这个组件对于需要做应用演示的同学尤其有用。
    • FutureKit/FutureKit:“是一个 100% Swift 实现的 Future/Promises 库,由 Swift Generic 带来的类型安全、由 GCD 带来的线程安全,通过 FutureKit 可以在 iOS/OS X 下灵活玩转 Future/Promises 式编程了”。推荐者:@图拉鼎
    • SwiftBond/Bond:使用简单、易于理解的对象绑定框架。比如:你可以使用代码 textField ->> label 将输入框文本映射到标签中的文字属性,当文本变化时,标签文字随动变化。 真的很方便。
    • nabilfreeman/ios-universal-webview-boilerplate: 快速封装 WebApp 到 iOS 应用,即便你毫无 Swift 编程经验。
    • FabrizioBrancati/BFKit-Swift:作者说这套工具库可以提高应用开发效率。不知效果如果?有兴趣的同学可以试用一下。

    • UI组件

    • jcavar/refresher:一个常用的下拉即刷新列表工具类,提供开放接口定制刷新动态变换效果。
    • ariok/BWWalkthrough:让你的页面切换动起来,示例效果杠杠的。作者还提供了比较完整的开发文档及示例解说。
    • vikmeup/SCLAlertView-Swift:动画效果弹出框封装库(管理于 CocoaPods),使用也足够方便。试着运行了一下,效果还不错。
    • varshylmobile/MapManager:地图管理封装库(默认支持Google和Apple地图服务)。
    • varshylmobile/LocationManager:位置管理封装库(默认支持 Google 和 Apple 地图服务)。
    • neoneye/SwiftyFORM:表单输入框架(亮点是表单验证规则引擎),是我见过地最易用的 Swift 表单组件。
    • ortuman/SwiftForms:这个表单递交库简单实用,支持主要数据类型及定制。快速开发利器。
    • kentya6/KYCircularProgress:这个进程条类库实现及使用都很简单,支持进程路径的定制,灵活方便。
    • coolbeet/CBStoreHouseRefreshControl:这个下拉刷新库,提供完全可定制进度变换效果,且极具艺术感。建议有能力的同学把 Objective-C 版本改写为Swift版本。
    • kevinzhow/PNChart-Swift:PNChart 的 Swift 版实现。PNChart http://t.cn/zRWXJCZ 是饼图、折线图、柱状图等常用可视化图表工具类库。开发者:@周楷雯Kevin
    • exchangegroup/Dodo:一款轻量地可定制信息栏小组件。包含 success, info, warning 及 error 类型消息展现。提供基本的按扭及事件绑定。
    • codestergit/SweetAlert-iOS:该弹窗控件设计简单直接,功能完整,动画效果恰如其分。实在不可多得,必须收录。
    • Ramotion/animated-tab-bar:一款灵动的动画标签栏类库。可在 storyboard 中直接使用,非常方便。Demo 效果非常漂亮。连续三天 GitHub 分类排名第一,且迅速过 600 星。它的代码并非复杂或特别,相信大家中意它的还是其设计感。
    • Ramotion/paper-switch:类库实现了当 Switch 组件开/关切换时,平滑过渡到父视图的变换效果功能(核心类继承了 UISwitch)。P.S. 该开发者也是这几天热门项目 Ramotion/animated-tab-bar 的作者。
    • FahimF/FloatLabelFields: 基于”Float Label Pattern“的内嵌浮动标签输入效果 Swift 版实现类库。
    • cemolcay/ReorderableGridView:可以拖拽排列 Grid 框格的 UI 组件。功能说明完整,使用方便,应该是一个不错的 UI “轮子”。
    • zhxnlai/ZLBalancedFlowLayout:一个通过调整 cell 大小来利用空间的 UICollectionViewFlowLayout 子类。
    • uacaps/PageMenu:分页式菜单控制及动态页视图切换。这款控件比较实用,且提供 CocoaPods 安装。
    • Yalantis/Side-Menu.iOS:一款带动画效果可定制 Slide Menu,可以学习其动画实现思路。P.S. 对于 Hamburger 式菜单,虽然很常用,不过,苹果并不鼓励使用,甚至有开发小组对其弊病用自家上线应用前后数据对比进行了抨击,仅做参考。
    • Mozharovsky/CVCalendar:开源 Swift 版日历组件及示例。该项目开发者充分使用了面向对象设计思想,让开发者可以更容易扩展及定制功能。同时,该开发者还提供了详细的组件使用方法(Storyboard 和代码方式指南)。
    • dekatotoro/SlideMenuControllerSwift:这是一个侧滑菜单弹窗 UI 组件,它提供高度可定制参数及完整示例。另外,组件代码集中,易用,仅一个核心类 SlideMenuController.swift
    • rhcad/ShapeAnimation-Swift:功能强大、使用简单的矢量动画框架。支持多层级,多形状,渐变以及各种常见的动画变化效果。绘图核心基于该作者另一个近期很火的开源项目 schwa/SwiftGraphics
    • raulriera/TextFieldEffects:具有不同输入风格及变化效果的标签和文本框组件库。它的原型来自 codrops 的 JS/CSS 项目。P.S. 我曾经还推荐过另一款基于 “Float Label Pattern” 风格类似项目 FloatLabelFields,与之一样优秀。
    • MengTo/Spring:不仅具有极简制作动画的 API ,还提供 Demo 界面调试动画效果,并生成相应代码。建议同学们在 Xcode 中运行体验一次,相信这个开源项目足够让你惊艳。已将其收入《Swift 开源项目精选》。
    • icanzilb/SwiftSpinner:这个图文结合进程条功能完整,整体效果还很酷。当然,使用也很简单。值得收录。
    • michaelbabiy/RMParallax:在应用启动时,提供简单帮助向导步骤的界面组件及示例程序。
    • ggamecrazy/LoadingImageView:简单实用的异步图片加载类库及示例。使用方便,功能独立、完整(离线存储也在计划中)。
    • pixyzehn/MediumScrollFullScreen:上下滚动时,全屏显示内容,并自然消隐上下菜单。由此项目感知,作者是一位很注重细节的开发者,的另外几个菜单类项目也都不错,值得参考,比如:PathMenu, MediumMenu 等。
    • MoZhouqi/KMPlaceholderTextView:可显示多行 placeholder 的 textView,实现逻辑简单、巧妙。
    • prine/ROStorageBar:存储器使用情况指示条组件,显示参数丰富,易于使用。开始以为它也包含了计算功能,可惜并没有,只是一个纯界面的小组件。不过,仍然有一定的实用性。
    • Yalantis/Persei:动画隐藏或显示顶部菜单支持库及示例项目。P.S. Yalantis 这个开发商,在 GitHub 上展示开发能力,Dribbble 上展示交互设计能力,并在 Behance 推广项目。整体展现了其出众的软件过程管理及综合开发实力。非常值得国内开发者学习。
    • KyleGoddard/KGFloatingDrawer:一款适合于大屏手机或平板的浮动抽屉式导航界面组件。效果很赞。
    • zemirco/swift-linechart:功能完整、实用的折线图组件。使用方便,参数配置简单。是不可多得的优质组件。
    • khawars/KSTokenView:带搜索、快捷输入、分段显示关键词的增强输入组件。作者提供了丰富地配置参数。主体功能同群发短信时选择、编辑好友群。
    • andreamazz/GearRefreshControl:这款下拉刷新动画是我所看到最没违和感的一个。
    • micazeve/MAGearRefreshControl:同样是下拉即刷新,看看人家花了多大的心思写代码。另一位开发者也实现在类似动画效果 andreamazz/GearRefreshControl,可以感受一下编程的多变性。
    • gali8/Tesseract-OCR-iOS:OCR 文字识别也有相应 iOS 项目。虽然是 Objective C 版,不过,提供 CocoaPods 三方库引用。与 Android 版师出同门。属于主流开源项目。
    • Akkyie/AKPickerView-Swift:AKPickerView O网页链接:一款小而美的 3D 效果选择器。P.S. 作者分别提供了 CocoaPods 和 Carthage 两种方式的三方接口对外引用和指南,Carthage 获得越来越多主流开发者重视了。
    • danielgindi/ios-charts:一款优秀 Android 图表开源库 MPAndroidChart 的 Swift 语言实现版(支持 Objective-C 和 Swift 调用)。缺省提供的示例代码为 Objective-C。P.S. 当然 @周楷雯Kevin 同学的 PNChart-Swift 也不容错过,同学们可以各取所需比较使用。
    • scotteg/LayerPlayer:一款全面展示核心动画 API 示例项目(上架应用)。包括 CALayer, CAScrollLayer, CATextLayer, AVPlayerLayer, CAGradientLayer, CAReplicatorLayer, CATiledLayer, CAShapeLayer, CAEAGLLayer, CATransformLayer, CAEmitterLayer 等使用的互动演示。RayWenderlich 有针对该项目的 撰文解读中译文 By @TurtleFromMars),与开发者 Scott Gardner 的 视频讨论
    • ReactiveCocoa/ReactiveAnimation:“ReactiveCocoa 推出了一个叫 ReactiveAnimation 的子项目,直接用完全用 Swift 来实现了。国外开发者社区就是拽啊! By @图拉鼎”。
    • larcus94/BRNImagePickerSheet:图片或视频选择器(可多选)组件及其示例项目。P.S. 作者推荐低藕、非侵入式三方库依赖引用方案 Carthage,从 iOS 8 开始,越来越多开发者推荐 Carthage 做为标配了。
    • JayGajjar/JGTransitionCollectionView:基于集合视图扩展实现完成自动布局及单元项 Flip 式动画效果(效果很赞)。组件使用方便、自然(只需设置集合视图数据源的标准方式即可)。
    • knutigro/COBezierTableView:通过编辑 Bezier 曲线四点位置设置 TableView 内 Cell 及对应按扭位置。实验效果很赞。#为开源点赞#
    • jozsef-vesza/ExpandingStackCells:采用 UIStackView 实现表格单元格扩展内容显示示例及解决方案。
    • szk-atmosphere/SAHistoryNavigationViewController:通过继承 UIViewController 并扩展 UICollectionView,UINavigationController,NSLayoutConstraint 等界面及布局控制核心类,应用内模拟实现 iOS 任务管理界面及切换组件。
    • szk-atmosphere/SABlurImageView:支持渐变动画效果的图像模糊化类库。P.S. 与前几天推存类库 SAHistoryNavigationViewController 是同一位作者。
    • cocoatoucher/AIFlatSwitch:一款带平滑过渡动画的 Switch 组件类。记得之前收录过两款相同风格的 Hamburger - Menu/CloseHamburger - Menu/Back。都是精细之作。
    • GabrielAlva/Swift-Prompts:一款漂亮、实用的提示弹窗。使用简单,参数定制完整,还支持窗口拖拽操作。
    • inFullMobile/WobbleView:当界面组件过渡时,带波纹摆动效果的组件扩展实现类及示例项目。
    • cjwirth/RichEditorView:一套可定制富文本编辑器组件及示例。功能完整、代码简练、实现逻辑巧妙(编辑器核心与 WebView 结合,采用 HTML5 contentEditable 编辑模式,执行JS 配套命令 execCommand 实现富文本编辑功能)。
    • SemperIdem/MKMapView-Extension:“在用 MapKit 的时候发现MKMapView缺少了极其重要的 ZoomLevel 这一个功能,因此便参照了网上的一些OC例程写了一个Swift版本的扩展,允许开发者获取当前地图的缩放级别和设置缩放级别。By @星夜暮晨”。
    • didierbrun/DBPathRecognizer:基于路径模型的手势识别工具(仅一个核心类)。妙处在于作者通过示例项目设置好 A-Z 字母的路径模型后,可以进行相应的手写识别。尽管它可能无法替代专业的手写识别输入,不过对于普通的手势识别还是搓搓有余了。
    • Yalantis/GuillotineMenu:正如其项目名字一样,这是一款极具创意及突破精神的动画演示项目。P.S. 这家公司(Yalantis)做动画,尤其界面特效出神入化。且 iOS / Android 通吃。
    • aryaxt/ScrollPager:一款小巧、易用的页滚界面组件。结合 storyboard 及滚动视图,轻易实现拖拽页滚功能。
    • wltrup/iOS-Swift-Circular-Progress-View:小而精悍地可定制类似 Apple Watch 圆型进程条组件。该组件同时支持代码及 Interface Builder 两种使用方式。
    • haaakon/SingleLineShakeAnimation:用一行代码给视图加上抖动效果。
    • indragiek/MarkdownTextView:非常轻量、简洁、高效的 Markdown 编辑组件及示例。这样的开源项目太喜欢了。
    • nickoneill/PermissionScope:向用户请求 iOS 权限的封装组件。封装库 API 调用非常方便,调用代码也很精简。一个很好的“轮子”,值得收录。
    • zhxnlai/UIColor-ChineseTraditionalColors:中国传统颜色引用 UIColor 扩展。“UIColor.桃红(),UIColor.竹青() …”,如此引用是否感觉亲切多了。P.S. 好的项目,源于用心,技术次之。这位同学已经深谙其道了。
    • zhxnlai/ZLSwipeableViewSwift:多变的卡片切换转场动画封装类库。
    • FlexMonkey/ShinpuruLayout:通过水平和垂直分组模块实现简单、快速布局的组件库。作者提供了多种不同类型的布局示例。
    • ziligy/JGFlipMenu:Flip 式菜单及其对应动画组件。使用也很简单直接 Interface Builder 。赞不赞自己看。
    • Draveness/DKChainableAnimationKit:高可读、链式代码风格动画框架库。
    • icanzilb/EasyAnimation:又一款使用简单、功能强大的动画类库。
    • andreamazz/BubbleTransition:以气泡膨胀和缩小的动画效果来显示切换界面。代码实现很简单,效果很突出。
    • KittenYang/KYFloatingBubble:实现iOS7 Game Center浮动气泡的一种思路。示例代码不多,效果还真得赞。
    • i-schuetz/SwiftCharts:基于层架构设计思想的图表类库。基于层架构,无论定制新的图表类型还是已有图表的叠加,都变得非常的容易。
    • tristanhimmelman/ZoomTransition:通过手势操控图片的放大、缩小、旋转等自由变化效果的组件及示例。
    • Yalantis/PullToRefresh:刷新动画可定制的下拉数据请求更新组件(类库非常简洁易用)。该组件由知名开源开发商 Yalantis 开发,并且还提供了完整动画定制演示项目 PullToMakeSoup
    • mikaoj/BSImagePicker:图片多选组件。接口简单,使用很方便。
    • skywinder/GaugeKit:模拟实现  Watch 风格运动进程表盘示例。P.S. 顺附  Watch 另一个圆型进程条参考项目 iOS-Swift-Circular-Progress-View
    • wongzigii/Butterfly:一款轻量、优雅的缺陷报告及反馈组件。与应用集成极为简单(仅需 2 行代码)。这个“轮子”实在太实用了,强烈推荐。附演示示例动态图。作者:@Wongzigii

    • 测试相关

    • kylef/Mockingjay:在单元测试时,模拟匹配条件的任意 HTTP 请求,该类库集成于 XCTest 框架内。是一套不可多得的快速开发工具库。
    • practicalswift/swift-compiler-crashes:旨在收集使 Swift 编译器崩溃的测试用例。这是一个疯狂又非常有意义的开源项目。可以定期关注一下。P.S. 该开源项目作者最新提示:Xcode 6.3 beta 与 Swift 1.2 已修复了其已发现编译器崩溃级缺陷的 83% 。
    • czechboy0/Buildasaur:自动测试 GitHub Pull Requests,以提高团队生产力。Buildasaur 安装条件:Xcode 6+, Xcode Server 4+ 和 GitHub 仓库证书。
    • czechboy0/XcodeServerSDK:非官方 Xcode Server SDK 封装库。 P.S. 该 SDK 分离自之前推荐的由该作者开发的自动测试框架 Buildasaur
    • realm/SwiftCov:Realm 开发的 Swift 代码测试覆盖率命令行工具。大家可以用用看,试试效果如何?

    2. 示例项目

    • 官方示例:”比起 GitHub 上的开源项目来说,官方的代码我觉得是更有参考价值的,比如 Session 406 的代码,Lister,就用一个 Swift 实现的包含 OSX 和 iOS 的” By @晨钰Lancy
    • 官方示例 - Lister:“Lister 这个官方 demo 也跑起来了。Apple 还是很用心的,不仅做了 iOS、Mac 和 Apple Watch 版本,并且为这个 demo 分别用 Objective-C 和 Swift 实现了一遍,最后还配了整整 30 页的文档!By @图拉鼎”。官方示例一份顶十份,学走路从官方文档及示例开始。
    • tsunghao/learn-swift:该项目是 nettlep/learn-swift 项目的(正体)中译版。最难能可贵的是原项目最后更新是 6 个多月前了。而中译项目已经更新至 Xcode 6.2(6C131e) ,且下一版(Swift 1.2)正式发布后,作者还会继续更新。
    • VirtualGS教程示例:以下示例程序来源于林泰前老师微博博客发布,为方便大家学习 Swift 编程,有幸获得林老师的准许在这里发布。
    • Easy-Cal-Swift:”实在忍不了 Swift 的数字计算时候的好麻烦的强制转换了,重载了一下加减乘除之类的操作符,这样就不用显式地转换类型了…(对于像我这样现在连补全都没有的孩子来说,能省好多时间啊- -)” By @onevcat
    • An app to list the available fonts on iOS:用 Swift 语言调用 UIKit,列出设备内所有字体名称的小程序。
    • Swift版的知乎日报:学习一门新语言,光看是没有用的,想要快速的掌握它,就得投入到真实项目的开发中,仿照 @YANGReal 的糗事百科,做了一个 Swift 版的知乎日报
    • Swift RSS Sample:用 Swift 语言开发的 RSS 阅读器
    • Swift版PM2.5的例子:一个很好的 Swift 与 Objective C 协同工作 GitHub 开源项目。用到的开源类库有 TFHpple:以 XPath 方式解析 HTML,SCLAlertView:使用 Swift 写的 AlertView SVProgressHUD 进度条 By @荧星诉语
    • lexrus/LTMorphingLabel:实现文字飘入飘出的效果。效果非常赞!
    • android1989/CharacterText:相比lexrus/LTMorphingLabel 的各种酷炫效果,这个版本比较简单实用。
    • practicalswift/Pythonic.swift:用 Swift 语言实现 Python 标准库的一部分。然后,用 Swift 写一段 Python 风格的程序,这是 Python 程序员想要的吗?相信这不过是一个实验,以此说明 Swift 语言的多变性、动态性的能力。
    • AshRobinson/GoogleWearAlert:Swift 语言实现模拟 Google Wear 风格 Alert 窗口。
    • rafaelconde/ios8-ui-kit:IOS 8 UI KIT + FOR SKETCH — 最新IOS8免费设计资源
    • gemtot/iBeacon:Swift 版 iBeacon 简单项目(支持最新Beta 6编译)。感兴趣的同学可以学习一下。有关于 Passbook 应用及 Passkit 框架编程知识这里有一篇网友较早发布的《iOS 6 - PassKit 编程指南》
    • evnaz/ENSwiftSideMenu:一个简单的 Slide 侧拉菜单实现。使用很方便:sideMenu = SideMenu(sourceView: self.view, menuData: [“UIDynamics”, “UIGestures”, “UIBlurEffect”])
    • iluuu1994/Pathfinder:一个有趣的算法类项目。虽然目前只是个演示项目,不过,作者有计划加入更多算法优化程序,同时,也有计划支持3D地图。
    • KhaosT/HomeKit-Demo :HomeKit 演示项目,可以与HomeKit模拟器协同工作。由此进一步思考:HomeKit 配合 iBeacon、蓝牙,甚至更具DIY潜力的树莓派(Raspberry Pi)组成未来家居智控中心,这个方向上的开发潜力巨大。感兴趣的同学可以关注一下。
    • mathewsanders/Animated-Transitions-Swift-Tutorial:结合 Xcode 开发步聚介绍如何开发动画过渡 Prototyping Animatted Transition in Swift(Part I)
    • mathewsanders/Custom-Menu-Transition-Swift-Tutorial:结合Xcode开发步聚介绍如何开发动画过渡 Transition in Swift(Part II)](http://mathewsanders.com/custom-menu-transitions-in-swift/)
    • vandadnp/iOS-8-Swift-Programming-Cookbook:来自《 O’Reilly’s iOS 8 Swift Programming Cookbook》的配套示例。书买不买另说,如此即时、完整、丰富的新书示例项目真不多见。悟性好的同学,开发时参考一下示例是不是就不用买书了?
    • ide/UIVisualEffects:Swift 实现模糊化等视觉效果示例代码。代码详见这里
    • sxyx2008/FoodPin:如此完整的 Swift 示例项目,可以帮助学习到不少Swift编程技巧。”用Swift写的一个功能比较完善的App,参照 Beginning iOS 8 Programming with Swift 一书。功能比较完善。AutoLayout、Core Animation、Core Data、国际化等。” By @荧星诉语
    • PhotoBrowser:”展示 Instagram 授权用户发布的照片的 App,参考了Ray Wenderlich 的 Alamofire 教程,还用了 SwiftyJSON 和 FastImageCache。” By @Kyrrr
    • dekatotoro/SlideMenuControllerSwift:这是一个侧滑菜单弹窗 UI 组件,它提供高度可定制参数及完整示例。另外,组件代码集中,易用,仅一个核心类 SlideMenuController.swift
    • mslathrop/SwiftNote:一款简单的笔记应用及其 Today 插件。可惜开发者还不知道如何与 iCloud 进行数据同步,有兴趣的同学可以参与一下。
    • gcollazo/mongodbapp:实现在Mac系统菜单栏上快捷启动/停止 MongoDB 的 Swift 项目。代码简单、易读,且核心代码全部在 AppDelegate.swift
    • 441088327/SYKeyboardTextField:“开源一个我自己写的键盘附随输入框,为Swift做些贡献”。 作者:@余书懿
    • dasdom/BreakOutToRefresh:下拉加载刷新时,嵌入了用 SpriteKit 框架写的有趣的小游戏播放页。P.S. 此例中作者嵌入的或许并没有让你耳目一新,甚至感到有点牵强,不过,若嵌入更合适的呢?
    • eleks/rnd-apple-watch-tesla:Apple Watch 操作 Tesla 汽车。可惜是 Objective C 版本的,否则,Swift 同学又是一次很好的学习机会。
    • kenshin03/Cherry:定时器应用,功能包括在手表上新建活动,启动/停止活动,持久化活动数据在 iPhone,在 Glances 中查看活动状态并可直接切换至该应用。P.S. 该应用由作者之前开发的 Objective-C 项目采用 Swift 重写后获得。
    • slazyk/SINQ:该项目把来自 C#和VB 的 LINQ 集成查询功能吸收了进来。是不是又一次证明了 Swift 吸纳各语言精华的能力很强大,是不是又一次向”大一统”的编程/编译环境迈进了一步?
    • MengTo/DesignerNewsApp:一款针对 Designer News 网站的完整 iOS 应用,最具特色地是开发者对动画技术得心应手的应用。P.S. 该开发者发布过一套非常完整、实用的优秀动画封装库及演示工具 Spring
    • MoZhouqi/VoiceMemos:“语音笔记本应用。用到了一些 iOS 8 新技术,比如 UISplitViewController、UISearchController、UIAlertController、自定义 Presentations、可视化开发,还有一些语音相关的用法。界面开发的新技术介绍可以参考 @onevcat这个帖子”。
    • schwa/TimingFunctionEditor:贝塞尔曲线编辑器,编辑后可以预览或拷贝代码片段直接使用。P.S. 该项目采用更简单的依赖管理器 Carthage
    • CarlosButron/Swift:60 多个 Swift 示例项目及代码集锦,技术及品类几乎涉足方方面面。
    • lizyyy/Homeoff:“用swift写了一个模仿Launcher通知中心快捷方式的应用。支持 20 个应用,并增加了一个返回到桌面来解放Home键的功能 By @Swift攻略”。
    • MakeZL/ZLSwiftRefresh:下拉刷新/上拉加载组件扩展及示例。“支持自定义动画,集成简单,兼容 UITableView,CollectionView,ScrollView,WebView”。
    • philcn/Auto-Layout-Showcase:几项常用自动布局技巧示例代码及演示。搭建简单应用够使了。
    • FlexMonkey/MetalParticles:超强计算、极致渲染,发挥 GPU 最高效能 Metal 框架演示项目。P.S. Metal 框架是并驾于 OpenGL ES 的底层图形计算接口。相比于 OpenGL,它甚至更底层、更接近 GPU。它是需求超强计算及渲染性能要求的游
      戏平台不二之选。
    • Mav3r1ck/Project-RainMan:一款很酷的天气预报应用。Forcast.io 提供的天气预报 API 很赞。
    • kongtomorrow/WatchTransition:模拟 Apple Watch 上的动画过渡效果。Objective-C 和 Swift 混搭开发学习参考。
    • jessesquires/JSQWebViewControlle:一款轻量的 WebKit 浏览器视图控制封装库(使用 WKWebView)。
    • radex/DiffyTables:提供在 WatchKit 应用中有效使用表格的实例。完整实现思路作者已经撰写长文 Practical and efficient WatchKit tables with view model diffing。充分理解该文章作者推荐阅读 Reducing WatchKit Traffic With View Models
    • WWDC 2015 watchOS应用示例:1. Lister(大家比较熟了); 2. PotLoc:使用 CoreLocation 定位;3.WatchKitMoviePlayer: 使用 WKInterfaceMovie播放视频。
    • The-Swift-2.0-Programming-Language-playground:“自己整理的对应最新发布《The Swift Programming Language》Swift 2.0 一书中的内容的Playground,基本在Playground中包含了书中描述的知识点 By @孟祥月”。
    • alskipp/Swift-Diagram-Playgrounds:面向协议的编程示例 Playgrounds 。P.S. 编程技术真是日新月异啊。
    • jstart/UIStackView-Playground:采用 UIStackView 平铺式自动布局演示示例 Playground。#为开源点赞# P.S. 顺附 UIStackView 官方文档译文 By @潇湘TT
    • phpmaple/Stick-Hero-Swift:“开源小游戏Stick-Hero,和大家一起学习Swift2和Spritekit,基础功能完成,后续添加一些细节方面设计 By @KooFrank”。#Swift 2#

    3. 完整项目

    • WWDC app for OS X:去年还只是一个会后的脑补演示项目。而今年已然成为追剧观看的超实用项目。连编译都懒得做的同学直接下载应用即可。
    • fullstackio/FlappySwift:用 Swift 语言实现的 FlappyBird
    • JakeLin/SwiftWeather:天气预报iOS项目,新界面还不错,简单,还带点卡通的味道
    • akeFiveInteractive/WeatherMap:基于地图的天气预报(上架)应用。比较适合长途自驾游的同学。
    • tnantoka/edhita:edhita 是一款用 Swift 重写并完全开源的文本编辑器。AppStore 上已经有更新版下载。试用后感觉还不错。它甚至支持 Markdown, HTML 等文件编辑后的预览显示。
    • jurre/TravisToday:一个功能相对完整的服务于 Travis CI 系统的 OS X 应用扩展 Today 项目案例。
    • stakes/Frameless:一款基于 WKWebView 的最小化全屏浏览器,主要功能是方便开发者采用HTML5做原型设计或 App 发布。对于测试工程师或喜欢极简、全屏浏览用户,可以直接在AppStore下载使用。
    • mozilla/firefox-ios:Swift 写的 iOS 版 Firefox。P.S. 第三方库用到了 Alamofire, Snappy, swift-json 等,内置阅读器还使用了 Readability。
    • lexrus/VPNOn:一款驻在通知中心内的基于 Today 扩展的 VPN 开关应用。
    • victor/whereami:一款用 Swift 语言写的通过命令行获得当前设备地理位置程序。虽然很简单,不过,实用性及学习性俱佳。
    • Ahmed-Ali/RealmObjectEditor:Realm 数据库 Swift 版开源编辑器。提供针对实体,属性及关系的编辑功能,完成后可以将实体结构导出为 Swift, Objective-C 或 Java 类模型。非常实用。
    • jpsim/DeckRocket:在相同 WiFi 网络环境内,通过 iPhone 控制并播放 Mac 中的 PDF 文档。ft 编译器崩溃的测试用例。这是一个疯狂又非常有意义的开源项目。可以定期关注一下。P.S. 该开源项目作者最新提示:Xcode 6.3 beta 与 Swift 1.2 已修复了其已发现编译器崩溃级缺陷的 83%。
    • Carthage/Carthage:更简单的方式来管理 Cocoa 第三方框架。有了 CocoaPods 为何还要使用 Carthage,作者在 README.md(中译) 已经做了充分说明。
    • artsy/eidolon:艺术品拍卖的投标亭平台。作者提供了一个很励志的开发故事:从商业创意到最终实现。心路历程从不开源到开源,从 Objective-C 到 Swift(去年夏天的事,现在当然是 Swift),以及选择时髦的反应式编程框架 ReactiveCocoa……
    • mortenjust/androidtool-mac:“一款用 Swift 写的可在 Mac 上使用的 Android 工具, 支持一键截屏, 视频录制, APK 安装等功能”。
    • lexrus/JSCoreDemo:“演示了一下怎样在 Swift 里用 JavaScriptCore 把原生功能桥接给 JS 用,代码少很容易理解”。简单、易懂的基础演示代码。P.S. 希望进阶学习的同学,这里有一个完整项目供参考 RichEditorView:基于 HTML 5 的可定制富文本编辑器组件及示例。
    • gilesvangruisen/Swift-YouTube-Player:用不了的同学就当学习吧。或参考并试着写个 Swift-(…)-Player。
    • Imperiopolis/WatchScreenshotMagic:快速生成干净、漂亮的 Apple Watch 截图的实用工具应用。
    • beltex/dshb:OS X 系统状态(CPU、电池、内存、温度传感器、风扇及杂项)监视器应用。每秒刷新文本界面,极低系统开销。
    • nscoding/sleep-osx:在 Spotlight 中输入 sleep 即休眠你的 Mac。实现特别简单,几行代码而已。
    • dasdom/Tomate:这个圆盘式计时器让你更专注于工作或学习。P.S. App Store 上架收费应用(0.99 欧),开发者福利呀。

    3. 项目评测

    • 【Workflow 测评】Workflow 是款什么软件?(作者:@JailbreakHum):”除了圣诞降价以外最近最火的软件话题应当是 #Workflow# 了。如果你没接触过此类软件,或许会纳闷它有何魅力。我们针对这款软件的测评系列的第一篇就是来向你概述它是什么软件的,所以文中给出大多是基本知识,不过相信也会给老手一定的启发。”

    相关工具

    1. 开发工具

    • Xcode 6 beta下载:苹果应用集成开发环境。支持 C/C++, Objective C, Swift 等。不用购买开发者计划,直接下载。
    • Textmate:Mac OS X 上一个可高度自定义的编辑器,尤其在我想做出一个快速改变但又不想等待 Xcode 加载的时候。该工具目前已经开源
    • Mou:OS X 上一款 Markdown 的编辑器。非常适用于编写自述文件、变更日志以及其他方面的内容。作者:罗晨
    • Sublime Text ($):Mac OS X 上另一款非常受欢迎的轻量级,可高度自定义的编辑器。
    • RunSwift:正在犹豫是否入手苹果电脑开始一段 Swift 编程旅程的同学们,或仅仅为了试验一段简单 Swift 代码又懒得打开 Xcode,可以试试这款 Web 版 Swift 编译环境 RunSwift。
    • InfinitApps - Bezel:“嫌 Xcode 6 目前提供的 Watch 模拟器不够直观?Bezel 是一个用于视觉预览 WatchKit 所开发程序效果的小工具,前提是你安装了 xScope 软件(Mac端)或 xScopeMirror(iPhone端)。 By @WatchKit开发
    • Markdown -> Playground:该开源项目可将内含有 Swift 代码的 Markdown 自动转换为 Xcode Playgrounds 文件。喜欢用 Markdown 编辑的同学很激动吧。P.S. 这款工具写于 NodeJS,原因作者有交待。
    • iOS/Mac Autolayout Constraints:这个工具不错,很直观,布局时可以省不少工夫。推荐者:@荧星诉语
    • 在线生成 AppStore 审核用截图:便捷的生产力工具。免费,易操作,可自定义。

    2. 代码管理

    • GitHub:声望日盛的资源分享之地。
    • GitHub for Mac:一个设计的非常美观的 git 客户端,不能取代你从命令行获得的所有功能,但使用起来非常简单。
    • GitCafe:GitCafe is a source code hosting service based on version control system Git。国内的代码托管服务,基于 Git,值得一提的是最近也推出了和 GitHub Pages 类似的服务 Gitcafe Pages。因为是在国内,所以相比较 GitHub 有速度优势,在网络环境差的情况下也许可以作为 GitHub 的备用。
    • Bitbucket:国外的代码托管服务,不同于 GitHub 的是,Bitbucket 可以免费建立 private 项目。
    • Git:分布式版本控制系统和源码管理系统,其优点是:快和简单易用。对于新手来说,可在此查看免费电子书籍。

    3. Xcode 插件

    4. 管理工具

    • HomeBrew:OS X 上非常出色的包管理工具。
    • Transmit ($):一个Mac OS X 上 FTP 客户端,有着非常漂亮的用户界面和有用的功能。

    5. 调试工具

    6. 参考文章

    • iOS 开发工具:”这是我们多篇 iOS 开发工具系列篇中的一篇,此前的文章比如:那些不能错过的 Xcode 插件,iOS 开发者有价值的工具集,iOS/OS X 开发:各种工具快到碗里来!,App 原型设计工具使用心得(上)& App 原型设计工具使用心得(下),你用哪种工具进行 iOS app 自动化功能测试?iOS 开发者必知的 75 个工具” By @CocoaChina
    • IOS 各种调试技巧豪华套餐:讲得很细。对于初学 Xcode 开发的同学值得参考,对于有经验的同学可以略过。作者:@David戴未来
    • 详解Xcode 6的视图调试:教程非常实用,值得学习。另外,教程选用的开源项目(jessesquires/JSQMessagesViewController)也很经典。来源:Ray Wenderlich,译者:@CocoaChina 翻译组

    7. 示例项目

    8. 实用资源

    • google/material-design-icons :对于喜欢 Material Design 风格的同学,这是难得好资源。Google 提供了极为完整的各种图标(包含 iOS 各种精度及 SVG)设计资源。
    • 全唐诗数据库:SQLite 数据库脚本。
    展开全文
  • 原文链接 : Building a Coffee Shop App with Swift, Foursquare API and Realm 原文作者 : De Vries Reinder 译文出自 : APPCODA 译者 : kmyhy 人們常說,程序員能將咖啡變成電腦程式。接下來,我們將編寫一...

    人們常說,程序員能將咖啡變成電腦程式。接下來,我們將編寫一個 App,列出距離你最近的咖啡屋!

    在本教程中,你將使用到如下技能:

    • Swift、Xcode 和 Interface Builder (自動佈局、約束和故事板)
    • Realm,一個輕量級的 Core Data 封裝,用於本地數據庫。
    • Foursquare,用 “Das Quadrat” 庫來訪問 Foursquare 的 REST API。
    • CocoaPods 和 Geolocation

    這個 App 會以你當前位置為中心,從 Foursquare 抓取以此為中心 500*500 米範圍內的地標信息。然後用一個地圖視圖(MKMapView)和表格視圖(UITableView)來顯示這些數據。并使用 Realm 來篩選數據,并在閉包中對數據進行排序。

    你可以從 GitHub 上下載完整的的源代碼和 Xcode 項目:reinderdevries/CoffeeGuide

    如此簡單?讓我們快快進入正題吧!

    設置 Xcode 項目

    首先,我們需要在 Xcode 進行一些設置。打開 Xcode,選擇 File\New\Project…

    在 iOS\Application 類別下選擇 Single View Application。然後,填写這些内容:

    • Product Name: Coffee
    • Organization Name: 任意
    • Organization Identifier: 任意,用反域名格式:com.mycompanyname
    • Language: Swift
    • Devices: iPhone
    • 反選 Use Core Data、Include Unit Tests 以及 Incluse UI Tests

    然後選擇項目存放路徑,“創建本地庫”(create a local Git repository)一項可選可不選。

    接下來,需要創建一個 Podfile 文件。在項目導航窗口,在項目名稱上右鍵,選擇 New File…,如下圖所示,選擇 iOS\Other 下的 Empty 模板。

    Four square api demo - 2

    將文件命名為 Podfile(沒有擴展名),然后將它保存在 .xcodeproj 文件的同一目錄下。最後,確認一下 Tagets 列表下 Coffee 前面的選擇框已被正確勾選。

    four square api demo - 3

    然後,在 Podfile 文件中輸入以下內容:

    source 'https://github.com/CocoaPods/Specs.git'
    platform :ios, '8.0'
    use_frameworks!
    
    pod 'QuadratTouch', '>= 1.0'
    pod 'RealmSwift'
    

    在本項目中,我們要用到兩個外部庫:Realm 和一個用於 Foursquare REST API 的 Swift 庫 Das Quadrat。

    然後,關閉項目,關閉 Xcode。打開 OS X 的終端窗口,將目錄切換到你的項目目錄。如果你不知道怎麼做,請遵循如下步驟:

    1. 打開終端程式
    2. 輸入 cd (c、d、空格)
    3. 打開 Finder
    4. 在 Finder 中找到你的項目目錄,但不要進到目錄裡面
    5. 從 Finder 窗口中將項目目錄拖拽到終端窗口
    6. 項目的絕對路徑將自動填寫到 cd 命令之後
    7. 敲回車
    8. 這樣你就將當前工作目錄切換到項目目錄下了

    然後在終端窗口中輸入:

    pod install
    

    命令的執行大約要個幾分鐘時間,同時屏幕上將有大量信息滾動顯示。Cocoapods 將為你的項目安裝所需的庫。同時將你的項目轉換為工作空間(由多個項目組成)。

    然後,在 Finder 中找到新生成的 .xcworkspace 文件并打開它。這個文件就位於你的項目的根目錄下。

    注意:當你在 Xcode 中打開這個工作空間時,你的項目很可能是處於折叠狀態。你可以將項目文件恢復到原來的打開狀態 ── 關閉工作空間,然後再打開工作空間。這會導致項目文件不再是折叠狀態。

    這就是你需要為本 App 的 Xcode 項目所進行的所有設置。如果一切順利,你應該擁有了一個包含了兩個項目的工作空間。其中 Pods 項目包含了 Realm 和 Das Quadrat 庫的代碼。

    用故事板創建 UI

    Coffee 的 UI 非常簡單。它只有兩個 UI 元素:一個地圖視圖和一個表格視圖。

    Xcode 已經為我們做了大量的工作。Single View Application 模板包含了一個故事板文件 Main.storyboard,它是 App 的入口。

    要創建地圖視圖,需要:

    1. 打開 Main.storyboard
    2. 在 Xcode 右下角的 Object Library 窗口中,找到 Map Kit View (MKMapKitView)
    3. 將它拖到故事板編輯器中,并放到 View Controller 的左上角。讓它的高度大致等於 View Controller 的一半,寬度則完全佔滿。
    4. 在 Object Library 窗口中找到 Table View (UITableView) 并將它拖到故事板編輯器的 View Controller 上。讓它的寬度完全佔滿,高度則佔據 View Controller 的下半部。

    接著為這兩個 View 設置自動佈局約束。首先,選中地圖視圖,點擊故事板編輯器右下角的 Pin 按鈕,這個按鈕位於右邊第二個位置,形如星戰中鈦式戰機 …

    然後會彈出一個小窗口,接下來你需要做:

    1. 反選“Constrain to margins”。
    2. 點擊左、上、右三個 I 形線,讓它們依次變成紅色。
    3. 每個 I 形線旁邊都有一個輸入框,將它們分別設置為 0。
    4. 點擊“Add 3 constraints”

    Setting Layout Constraints

    接下來,在表格視圖上重複同樣的動作。但是將上面的 I 形線替換成下面的 I 形線(同時還有左和右)。同樣地,反選“Constrain to margins”選項,然後點擊“Add 3 constraints”按鈕。

    我們讓兩個 View 分別相對於上對齊和下對齊,寬度都是父 View 的百分之百。還有一件事情,就是讓兩個 View 的垂直高度都等於整個屏幕高度的 50%。

    我們可以用多個約束來實現這點,但這是最簡單的:

    1. 選中地圖視圖和表格視圖(按住 Command 鍵,然後分別點擊這兩個 View)。
    2. 點擊 Pin 按鈕。
    3. 勾選 Equal Heights 選項。
    4. 點擊“Add 1 constraint”。

    現在 Xcode 可能會報錯說“有佈局衝突”。別擔心,我們會來修復它。

    1. 點擊地圖視圖,然後點擊 Pin 按鈕。
    2. 反選 “Constrain to margins” 選項,然後點擊下面的 I 形線,將它的值修改為 0。
    3. 點擊“Add 1 constraint”。

    現在紅線消失了,IB 又開始顯示黃線。這表示有部份約束當前顯示不正確。所有的約束都是對的,僅僅是 IB 在顯示上不正確。

    要解決這個問題,點擊黃色的帶圈的箭頭,這個圖標位於故事板編輯器 Document Outline 窗口的右上角。

    點擊這個圖標后,會顯示一個新的界面。點擊黃色的三角,然後點 Update frames,再點擊 Fix misplacement。在每個黃色的三角上重複同樣步驟。當然,Update frames 的辦法并不是每次都有效,因此確保你的約束都創建正確,再讓你的 frame 也正確。

    不幸的是,佈局約束經常會出現大量錯誤。如果你搞錯了某些事情,你可以從 Document Outline 窗口中將約束刪除,然後重建。

    Fix Layout Issue

    編譯 App 并修復錯誤

    讓我們來運行一下 App。在開發的時候,我們經常需要運行 App,以檢驗我們的改動是否正確。

    當你已經非常熟練的時候,你可以修改很多內容而不用檢查這些修改是否正確。通過本能來判斷自己有沒有做錯。但如果你還是一個新手,則儘量不要步子邁得太大。一次只解決一個問題,然後就檢查 App 是否能正常工作。如果出錯,你就會知道剛才修改的地方出錯了。這個道理很簡單。

    要運行 App,這樣做:按下 Command + B 或者 Command + R。一個是編譯,一個是編譯并運行。在 Xcode 的左上角,你可以選擇在什麼樣的 iPhone 上運行 App。如果你將 iPhone 或者 iPad 連上 Mac,同時這些設備已經可以用於開發,則你也可以在這個地方選擇它們。

    看一下 App 能運行嗎?

    答案是不能運行!讓我們來找出問題并解決它。找到 Debug 窗口,它位於 Xcode 窗口的底部。在 Debug 窗口的右邊你會看到有一個錯誤。

    Debug Console

    如果你不能看到上圖的畫面,點擊右上角的按鈕和右下角的按鈕讓它顯示。

    這個錯誤是:

    2015-11-04 14:37:56.353 Coffee[85299:6341066] *** Terminating app due to uncaught exception 'NSInvalidUnarchiveOperationException', reason: 'Could not instantiate class named MKMapView'
        *** First throw call stack:
        (
            0   CoreFoundation                      0x0000000109fdff65 exceptionPreprocess + 165
    

    編程中錯誤信息經常不是很直觀,甚至有時候根本就算不上是錯誤信息。大部份運行時錯誤都會由一個異常,一條消息和一個調用堆棧構成。

    這三個部份都有利於你找出錯誤的原因。例如,你可以用這個異常去定位拋出該錯誤的代碼片段。而調用堆棧會列出在錯誤出現時的類以及方法的調用列表。也就是所謂的“回溯”,以倒序列出錯誤發生之前執行過的代碼。

    現在,先看一下錯誤信息,因為這是最容易利用的部份。它說的是:

    不能實例化 MKMapView 類
    

    MKMapView 這個詞我們是知道的。我們曾經在 IB 中使用了它。它是一個地圖視圖,位於 UI 的上半部份。初始化這個詞有點程序員的意思,它是說編譯器(Xcode 的一部份,用於將代碼轉換成 App 的二進制)無法創建 MKMapView 的副本。總之就是說:我無法創建地圖視圖。

    不幸的是,99% 的錯誤信息不會告訴你怎麼解決問題。它只會告訴你發生了什麼,但問題的原因卻無法看到。

    現在怎麼辦?你需要干兩件事情:

    1. 回家洗洗睡吧,再也不做 App 了
    2. 複製、粘貼錯誤信息,然後用 Google 進行搜索 (建議你這樣做)

    好吧,拷貝這段錯誤信息,然後在 Google 上搜索。這下你可能會搜索出這樣的結果:

    google-search

    點擊最上面的超鏈接。它會帶你到 StackOverflow,這是一個關於編程的 Q&A 網站。上面會有這個星球上幾乎所有語言的問題,以及這些問題的答案。

    這就是你要做的,在 StackOverflow 上瀏覽答案:

    1. 查看這個問題是否有一個以上的回答。如果一個都沒有,繼續檢查另一個問題(通過 Google)。如果你找到了一個答案,能夠回答還先前沒有答案的問題時,你可以將這個答案順便添加到進去。
    2. 掃一眼最上面的原始的問題。看看標題,瀏覽一下內容以及下面的評論。這些評論經常會包含一些額外的信息。
    3. 挑出已經認可的答案,這些答案會用綠色的勾子進行標記。它的評論還是要看的,有時候評論比答案還有用。很多時候,已經認可的答案並不是最好的答案。檢查左邊的上下箭頭之間的數字,這個數字表示投贊成票的數字(即投“這是一個好答案”票)。已認可的答案不一定是最有用的,因此繼續深挖網頁上的其它內容。
    4. 對於這些答案,建議不要盲從,理解它的原理。學習編碼是一件很費時間的事情,但它對你的未來會有無窮的好處。幾乎每個程序員都會有知識上的盲區或者技術上的缺陷。如果你學會如何避免在未來犯錯誤,以及為什麼犯錯誤,你就會和全世界最頂尖的 1% 的程序員站在一起。

    好了,到底是什麼原因出現的問題?答案是 MapKit 框架沒有添加到項目中。顯然,MKMapView 的代碼是放在某個外部框架中了。這些代碼必須被加到項目中來,雖然我們沒有直接在代碼中使用過地圖視圖。

    如果你看完了網頁,你會發現还有大量的反面例子,也會引發這個錯誤。

    好了,該我們來解決這個問題了:

    1. 回到 Xcode,在项目导航窗口中点击項目屬性。項目屬性位於項目導航窗口的頂部。
    2. 點擊 Build Phases 標籤欄.
    3. 然後,點擊 Link Binary With Libraries 左邊的箭頭。這會顯示一個列表。
    4. 點擊列表底部的 + 號按鈕,會彈出一個窗口。
    5. 在文本框中輸入: mapkit,對列表進行篩選。
    6. 最後,雙擊 MapKit.framework。

    這會將這個框架添加到 Link Binary With Libraries 列表和項目中。

    Link with MapKit Framework

    使用用戶的地理座標

    你接下來的任務是 Geolocation,即將用戶的位置顯示到地圖上。

    首先,需要將故事板中的地圖視圖連接到代碼中。在我們新建項目時,會創建一個文件 ViewController.swift,這就是故事板中的 View Controller 放置代碼的地方。

    接下來檢查一下故事板和代碼之間的這種連接是否存在:

    1. 打開 ViewController.swift 并查找這行代碼:class …,也就是類的定義。這行代碼指定了類的名字,它的父類以及它所實現的協議。在這裡,類名顯示為 ViewController。
    2. 然後打開 Main.storyboard 并點擊 Document Outline 窗口最上面的項目。這個項目就是“View Controller Scene”。
    3. 在右上角,點擊 Identity 檢查器。即從左到右第三個圖標。
    4. 最後,找到 Class 欄,查看它的內容

    這表明故事板和 ViewController 的代碼是有聯繫的,幸虧我們有 class 關鍵字可用。如果你向故事板中加入另一個 View Controller,你可以為它指定另一個類名。

    為地圖視圖創建一個出口

    現在我們知道 ViewController 和代碼其實是連接在一起,我們需要創建一個出口連接到地圖視圖。在你用自己的代碼引用這個地圖視圖之前,需要為地圖視圖的創建一個連接。

    打開 ViewController.swift 在第一行的 { 括號之後加入以下代碼:

    @IBOutlet var mapView:MKMapView?
    

    這是什麼意思?

    • 在 Swift 中,變量在使用之前需要事先聲明。在聲明變量的同時,你可以初始化它。上面的這句代碼,變量并沒有初始化,因此它的值為空(nil)。
    • 剛剛這行代碼也創建了一個實例屬性。這個屬性是和 ViewController 的每個實例綁定的,並且對每個實例來說,這個屬性都是單獨的。
    • 這個屬性的名字叫做 mapView,類型是 MKMapView。這個類來自於 MapKit 庫。
    • 關鍵字 @IBOutlet 用於告訴 Xcode,我們想把這個屬性當成一個出口。出口的概念其實是一個連接,用於表示代碼和故事板(或者 xib)中的UI對象之間的連接。
    • var 關鍵字表明這是一個變量,它的值可以被修改。與之相反的是 let 關鍵字,表示一個常量,值無法改變。
    • 類型名后面的符號 ? 表示這個變量是可空的。可空是 Swift 中特有的概念。它表示變量除了存放非空的值,也可以存儲空值。nil 表示空,什麼都沒有。可空在 Swift 中出現,目的是為了讓代碼更安全和更易於使用。 關於可空,更多內容見後。
    • 為什麼要將代碼寫在這一行?這行代碼寫在了類的頂部。即類聲明的內部,這表明變量在整個類中都是可用的。如果放在方法的內部,則表示變量是一個局部變量,只能在聲明變量的方法內使用這個變量。如果放在類的外部聲明,則表明變量是全局的,在(幾乎)任何地方都能使用這個變量。

    是否分不清變量和屬性的概念?一個變量是一個簡單的容器,用於存放數據。屬性也是一種變量,但它附屬於某個類。有兩種屬性:實例屬性和類屬性。

    是否分不清類、實例和類型?可以把類看成是一個“死的”(就像是壓鑄出來的)模板,用於創建一堆複製品。這些複製品就是實例。而類型的概念有點模糊,但你可以簡單地把它看成和類的概念差不多。

    是否分不清聲明、初始化和實例化?好吧,首先來說聲明:聲明僅僅是告訴編譯器,我要使用某個變量,它的名字是什麼,它的類型是什麼。初始化的意思則是我要給這個變量一個初始值。也就是在聲明一個變量的同時,將一個值賦給這個變量。如果你不初始化變量,則變量的值就是 nil。實例化的意思則是你將一個實例(類的一個複製品)賦給這個變量。專業點講,一個變量就是一個實例化對象的引用。

    回到項目。在添加上面的代碼后,Xcode 會拋出另一個錯誤。這個錯誤說的是:

    MKMapView 類型未聲明
    

    這是因為你還沒有將 MapKit 引入到當前文件!因此,在類定義之上添加一個 import 語句。在 import UIKit 之後加入:

    import MapKit
    

    現在,我們再來創建 outlet 連接。

    1. 首先, 打開 Main.storyboard。
    2. 然後, 在 Document Outline 窗口中選中 View Controller Scene 。
    3. 打開 Connections 檢查器。
    4. 檢查在列表中是否包含了 mapView 屬性。
    5. 最後,將 mapView 右邊的小圓圈拖到故事板編輯器中的地圖視圖上。

    Creating The Outlet Connection

    編寫第一個方法

    好了,讓我們來編寫使用地圖視圖代碼。首先,在 ViewController 類中加入:

    override func viewWillAppear(animated: Bool)
    {
        super.viewWillAppear(animated)
    
        if let mapView = self.mapView
        {
            mapView.delegate = self
        }
    }
    

    寫在什麼地方?任何地方!只要在類聲明的一對大括號 {} 之內。

    所有方法都必須寫在類的範圍內。一個類的範圍從第一個左大括號 { 開始,到最後一個右大括號 } 結束。

    這必須是平衡的,即每個左大括號 { 都必須配有一個對應的右大括號 }。而且,程序員需要使用縮進來表示範圍的級別。通常,每當一個左括號 { 之後就要使用一個 Tab 符或者四個空格表示一個縮進(在右括號之後則進行反縮進)。

    Adding Code To ViewController

    我們來看看這個方法:

    • 一個方法是一個代碼塊,它被類所擁有。它通常由多個單獨的代碼行構成,每個代碼行負責完成一個指定的動作。方法既可以在類的內部被調用,也可以在 App 的任何代碼中調用。
    • 方法的名字叫做 viewWillAppear,它帶有一個參數。一個參數是一個變量,用於在調用方法時傳遞數據給這個方法。參數能夠在方法的範圍內即這個方法的任何地方使用。在上面的方法中,這個參數 animated 是一個 Bool 類型(布爾類型,yes 或 no),它會在調用父類的 viewWillAppear 方法時用到。
    • 所有的方法用關鍵字 func 開頭,func 是 “function” 的縮寫。在這裡,方法應當被覆蓋,因此使用了 overriden 關鍵字修飾。overriden 表示用自己寫的方法替換父類的同名方法。父類和覆蓋的概念屬於面向對象編程中的範疇。這裡我們不展開討論,因為這實在是太枯燥乏味了。
    • 在接下來的代碼中,我們首先將 self.mapView “可空綁定”到一個常量 mapView。“可空綁定”可以讓你檢查某個可空對象是否為 nil,如果不是,if 語句才會被執行。同時,mapView 變量只會在 if 語句的範圍內有效。
    • 在 if 語句中,mapView 的 delegate 屬性被設置為 self。也就是說,如果 self.mapView 不為空,mapView 的 delegate 就會被設置為 self。簡而言之,如果 mapView 不為空,這個類將充當 mapView 的委託。關於委託,後面再論。

    現在 Xcode 又向我們拋出了一個錯誤。這次它說的是“無法將 self 賦給 delegate 屬性,因為 ViewController 並不是一個 MKMapViewDelegate 類型”。我們來解決這個問題。

    修改類的聲明如下:

    class ViewController: UIViewController, MKMapViewDelegate
    

    獲取用戶位置

    現在地圖視圖已經創建,我們可以來獲取用戶的位置了。

    為 ViewController 類加入下列屬性:

    var locationManager:CLLocationManager?
    let distanceSpan:Double = 500
    

    第一個變量是 locationManager,它是一個 CLLocationManager 類型。它是可空的,因此它可以保存 nil。第二個屬性是一個 Double 類型的常量,它的值是 500。Double 類型是一種雙精度浮點數(即長度是 Float 的兩倍)。

    在類中加入下列方法。可以在 viewWillAppear 方法后添加這個方法:

    override func viewDidAppear(animated: Bool)
    {
        if locationManager == nil {
            locationManager = CLLocationManager()
    
            locationManager!.delegate = self
            locationManager!.desiredAccuracy = kCLLocationAccuracyBestForNavigation
            locationManager!.requestAlwaysAuthorization()
            locationManager!.distanceFilter = 50 // Don't send location updates with a distance smaller than 50 meters between them
            locationManager!.startUpdatingLocation()
        }
    }
    

    呃,這些代碼是什麼意思?

    1. 首先,我們用 if 語句判斷 locationManager 是否為空。
    2. 然後實例化一個 CLLocationManager 對象并賦給 locationManager。換句話說:locationManager 現在保存了一個新的 CLLocationManager 對象的引用。我們只是創建了一個 CLLocationManager 對象,這個對象可以用於獲取用戶的位置。
    3. 然後,我們設置了 locationManager 的幾個屬性。delegate 屬性設為當前類,然後設置了 GPS 精度并調用 requestAlwaysAuthorization() 方法。這個方法會顯示一個窗口,用於向用戶獲得訪問 GPS 位置信息的權限。
    4. 最後,調用 startUpdatingLocation 方法。這會導致 locationManager 去獲取 GPS 位置,并調用委託對象的方法來通知 GPS 位置所發生的變化。這樣,在委託方法中,我們就可以拿到用戶的位置數據了!
    有沒有注意到在 locationManager 後面的感嘆號?我們知道 locationManager 是一個可空,它的值有可能是一個 nil。當我們想用這個對象的時候,我們需要對它進行解包操作。這是一個基本準則。解包有兩種方式:
    • 可空綁定. 也就是 if let definitiveValue = optionalValue { ….這樣的形式
    • 強制解包. 也就是 optionalValue! 這樣的形式
    之前我們見到過可空綁定。它是用一個 if let 語句在可空對象不為空的時候將它的值賦給一個新的變量。 強制解包是一種更粗暴的方法。簡單地在變量名后加一個感嘆號,即可將變量從一個可空對象變成非可空對象。但是,在你將一個可空對象強制解包時,如果該對象為空,則 App 會崩潰。 當一個可空對象為 nil 時,你不能對它強制解包。在上面的代碼里,強制解包是可行的。為什麼?因為我們在強制解包之前,locationManager 已經被明確地賦值了。也就是說,這時我們已經能夠肯定 locationManager 對象不可能為空。 回到代碼里來。我們添加了一個新方法,然後 Xcode 報了一個錯…。讓我們來搞定它! 這個錯誤說,我們想讓 self 成為 locationManager 的委託,但我們沒有實現正確的協議。修改類的聲明,讓它遵循這個協議:
    class ViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate
    
    現在,在 ViewController 中增加如下方法。這個方法是一個委託方法,你可以將它放在前面的方法之後。
    func locationManager(manager: CLLocationManager, didUpdateToLocation newLocation: CLLocation, fromLocation oldLocation: CLLocation) {
        if let mapView = self.mapView {
            let region = MKCoordinateRegionMakeWithDistance(newLocation.coordinate, distanceSpan, distanceSpan)
            mapView.setRegion(region, animated: true)
        }
    }
    
    這些代碼是什麼意思?
    • 首先,這個方法的簽名是 locationManager:didUpdateToLocation:fromLocation。這個方法使用了命名參數,也就是說參數的名字(在方法內部的變量)與調用時的參數名字不同。長話短說,這個方法有三個參數:調用這個方法的 location manager 對象,新的 GPS 座標,以及老的 GPS 座標。
    • 在這個方法中,首先將 self.mapView 用可空綁定進行解包。如果 self.mapView 不為空,則將解包后的值賦給 mapView,然後執行 if 語句。
    • 在 if 語句中,根據新的 GPS 座標和前面指定的跨度劃定一個範圍。 具體的說,我們創建了一個以新座標為中心的 500*500 大小的矩形範圍。
    • 最終,調用 mapView 的 setRegion 方法。animated 參數設置為 true,讓區域的改變呈現為動畫式的。也就是說,地圖會進行平移和縮放,并顯示用戶位置周邊 500*500 方塊內的地圖。
    還有一件事情。為了訪問用戶的位置,我們需要在 Xcode 中設置一個特殊的選項。這個選項是一串文本,用於描述你為什麼要使用用戶的位置。當向用戶獲取授權的時候(也就是我們調用 requestAlwaysAuthorization() 方法的時候),iPhone 會顯示這串文本。 要設置這串文本,你需要:
    1. 在項目導航窗口中,打開 Info.plist 文件。
    2. 在列表窗口中點擊右鍵,然後選擇 Add Row。
    3. key 輸入 NSLocationAlwaysUsageDescription。
    4. type 選擇 String。
    5. value 則輸入你在獲取權限時要顯示的文本!

    Edit Info.plist

    運行 App

    現在我們來運行一下 App。確認你的 target 中選擇了一個 iPhone 模擬器,然後按下 Command + R。App 將啟動,并彈出一個需要授權的窗口,點擊“允許”。

    Permission Popup

    當你點擊“允許”后,地圖視圖並不會刷新。因為模擬器沒有 GPS,我們需要模擬一個。

    當 App 在模擬器中運行時,請使用以下倆個方式之一來模擬 GPS 位置數據:

    • 在 iPhone Simulator 窗口,點擊菜單: Debug -> Location -> Apple。
    • 在 Xcode 窗口,點擊菜單: Debug -> Simulate Location,然後從列表中選擇一個位置。

    當你選擇了一個位置后,地圖視圖將刷新并將指定的位置放大顯示。

    看到了嗎?干得漂亮!

    從 Foursquare 獲取地標數據

    接下來 App 會變得更有趣!你將使用 Das Quadrat 庫從 Foursquare 獲取數據,然後用 Realm 將數據保存到本地。

    在使用 Foursquare API 之前,你需要在它的開發者網站上註冊 App。這個步驟其實很簡單。

    • 首先,你需要有一個 Foursquare賬號,你可以在 foursquare.com 註冊一個。
    • 接著,跳到 developer.foursquare.com ,點擊上方的藍色菜單條中的 My Apps。
    • 然後,點擊右邊綠色的 Create a new app 按鈕。
    • 然後,填寫如下內容:
      • App Name: Coffee
      • Download / Welcome page URL: http://example.com

    • 最後,點擊 Save Changes。

    保存完成后,網頁將跳轉到 App 頁面。注意 Client ID 和 Client Secret,我們將在後面用到它們。

    Foursquare

    創建 Foursquare API Connector

    現在,我們要編寫連接 Foursquare 的代碼了。你將用單例模式來實現。單例模式適合于我們現在的場景。

    一個單例是一個類實例,它是類僅有的實例。使用單例時,我們不能為一個類創建兩個實例。為什麼我們要用單例模式?對於單例的使用,一直有很大的爭議,但它對於這種情況是非常適用的:避免對一個外部資源出現多個併發的連接。

    你可以想一下。如果你向一個 web 服務器發送兩個請求,同時這倆個請求都是向一個文件進行寫操作?數據將變得混亂不堪,除非 web 服務器讓其中一個請求比另一個請求優先執行。

    一個單例保證只從 App 的一個地方請求外部資源。在單例模式下,大量的實現是為了保證不出現資源衝突。請求隊列和序列就是其中的一種實現,但這卻不在本教程的討論範圍。

    你需要這樣做:

    1. 在項目導航窗口中右鍵點擊 Coffee 文件夾。
    2. 選擇 New File …。
    3. 選擇 iOS -> Source 下面的 Swift File 模板,點擊 Next。
    4. 將文件命名為 CoffeeAPI.swift,選中 Target 列表中的 Coffee,點擊 Create,保存文件。

    哇,創建了一個空文件!讓我們來編輯它。在文件開頭的第一個 import 語句后,加入:

    import QuadratTouch
    import MapKit
    import RealmSwift
    

    然後加入代碼:

    struct API {
        struct notifications {
            static let venuesUpdated = "venues updated"
        }
    }
    

    這段代碼很簡單,不是嗎?首先我們導入了所需的庫(Quadrat, MapKit, Realm),然後定義了一個“結構的結構”,叫做 venuesUpdated,它裡面有一個靜態常量。然後,我們就可以像這樣調用這個結構:

    API.notifications.venuesUpdated
    

    然後,繼續編寫代碼:

    class CoffeeAPI
    {
        static let sharedInstance = CoffeeAPI()
        var session:Session?
    }
    

    這段代碼負責完成這些事情:

    • 告訴 Xcode 編譯器,我們定義了一個類 CoffeeAPI,它是一個純粹的 Swift 類,因此不需要子類化 NSObject。
    • 聲明一個靜態的類常量,叫做 sharedInstance,類型指定為 CoffeeAPI。這個“共享的實例”只能通過 CoffeeAPI 類來訪問,當 App 一啟動這個實例就會被實例化(預先加載)。
    • 聲明一個類屬性 session, 類型為 Session? (來自 Das Quadrat 庫).

    然後,我們將通過 CoffeeAPI.sharedInstance 的方式來訪問 Coffee API。無論在任何地方,你都可以這樣調用,你都會引用到同一個對象,這就是單例的特點。

    然後是構造函數。在上述屬性聲明后,在類的大括號之內輸入:

    init()
    {
        // 初始化 Foursquare client
        let client = Client(clientID: "...", clientSecret: "...", redirectURL: "")
    
        let configuration = Configuration(client:client)
        Session.setupSharedSessionWithConfiguration(configuration)
    
        self.session = Session.sharedSession()
    }
    

    構造函數是一個方法,當類實例被實例化之後這個方法就會被調用。這是在一個實例被創建之後會自動調用的第一個方法。

    還記得你在 Foursquare 開發者網站上拷貝的 Client ID 和 Client Secret嗎?將它們粘貼到構造函數的 … 中,然後將 redirectURL 參數設為空白。變成這樣:

    let client = Client(clientID: "X4I3CFADAN4MEB2TEVYUZSQ4SHSTXSZL34VNP4CJHSJGLKPV", clientSecret: "EDOLJK3AGCOQDRKVT2GK5E4GECU42UJUCGGWLTUFNEF1ZXHB", redirectURL: "")
    

    好了,還要做一件事情。將下列代碼拷貝到 CoffeeAPI.swift,記得粘貼在 CoffeeAPI 類之外。也就是說放在文件最後的一個大括號 } 之後。

    extension CLLocation
    {
        func parameters() -> Parameters
        {
            let ll      = "\(self.coordinate.latitude),\(self.coordinate.longitude)"
            let llAcc   = "\(self.horizontalAccuracy)"
            let alt     = "\(self.altitude)"
            let altAcc  = "\(self.verticalAccuracy)"
            let parameters = [
                Parameter.ll:ll,
                Parameter.llAcc:llAcc,
                Parameter.alt:alt,
                Parameter.altAcc:altAcc
            ]
            return parameters
        }
    }
    

    這段代碼是什麽意思?這是一個擴展,擴展會為某個基類增加一些額外的功能。它不用創建新的類,你可以擴展基類 CLLocation,讓它增加一個新的方法 parameters()。每當代碼中用到 CLLocation 對象的時候,就會加載你的擴展,你都可以調用這個對象的 parameters 方法。哪怕這個方法在原來的類中根本不存在。

    注意:不要將 Swift 中的擴展和編程術語“擴展”(即繼承)相混淆。前者是用新的功能增強某個基類,而後者則表示在父類基礎上創建一個子類。

    parameters 方法返回一個 Parameters 對象,Parameters 可以簡單地看成是一種鍵值類型的字典,它用於包含參數化的信息(GPS 座標和精度)。

    向 Foursquare 發送請求并進行處理

    好,讓我們來從 Foursquare 獲取數據吧。Foursquare 有一個 HTTP REST 風格的 API,返回的數據是 JSON 格式。幸好我們不用和它們打交道,它們都已經封裝到了 Das Quadrat 庫中。

    向 Foursquare 請求數據只需要用到 session (就是我們剛剛創建的那個)的一個屬性,并調用該屬性的一個方法。這個方法返回一個 Task 對象,這個對象引用了一個異步的後臺任務。我們可以為該方法提供一個完成閉包;這樣當任務完成時就可以執行閉包中的代碼:

    let searchTask = session.venues.search(parameters)
        {
            (result) -> Void in
    
            // 對 "result" 進行處理
        }
    

    session 的 venues 屬性包含了所有從 Foursquare API 返回的與“地標”有關的信息。你需要向 search 方法傳遞一個 parameters 參數,第二個參數則是一個閉包,這個閉包在任務完成時會被調用。這個方法會返回一個引用,這個引用是一個耗時的後臺任務。通過這個引用,你可以在任務完成之前停止該任務,或者在其它地方用它來了解任務進度。

    接下來是下面的這段代碼。將它複製粘貼到你的代碼中,放到 init 構造方法下面,類的右大括號 } 前面。然後我們再細細講解這些代碼有些什麽作用。

        func getCoffeeShopsWithLocation(location:CLLocation)
        {
            if let session = self.session
            {
                var parameters = location.parameters()
                parameters += [Parameter.categoryId: "4bf58dd8d48988d1e0931735"]
                parameters += [Parameter.radius: "2000"]
                parameters += [Parameter.limit: "50"]
    
                // 開始搜索,即異步調用 Foursquare,并返回地標數據
                let searchTask = session.venues.search(parameters)
                {
                    (result) -> Void in
    
                    if let response = result.response
                    {
                        if let venues = response["venues"] as? [[String: AnyObject]]
                        {
                            autoreleasepool
                            {
                                let realm = try! Realm()
                                realm.beginWrite()
    
                                for venue:[String: AnyObject] in venues
                                {
                                    let venueObject:Venue = Venue()
    
                                    if let id = venue["id"] as? String
                                    {
                                        venueObject.id = id
                                    }
    
                                    if let name = venue["name"] as? String
                                    {
                                        venueObject.name = name
                                    }
    
                                    if  let location = venue["location"] as? [String: AnyObject]
                                    {
                                        if let longitude = location["lng"] as? Float
                                        {
                                            venueObject.longitude = longitude
                                        }
    
                                        if let latitude = location["lat"] as? Float
                                        {
                                            venueObject.latitude = latitude
                                        }
    
                                        if let formattedAddress = location["formattedAddress"] as? [String]
                                        {
                                            venueObject.address = formattedAddress.joinWithSeparator(" ")
                                        }
                                    }
    
                                    realm.add(venueObject, update: true)
                                }
    
                                do {
                                    try realm.commitWrite()
                                    print("Committing write...")
                                }
                                catch (let e)
                                {
                                    print("Y U NO REALM ? \(e)")
                                }
                            }
    
                            NSNotificationCenter.defaultCenter().postNotificationName(API.notifications.venuesUpdated, object: nil, userInfo: nil)
                        }
                    }
                }
    
                searchTask.start()
            }
        }
    

    這段代碼好多!在這個方法中主要完成了五個任務:

    1. 首先,向 API 配置和發起了一個請求。
    2. 請求完成塊 (即閉包)。
    3. 解析請求返回的數據,并開始 Realm 事務。
    4. 用 for 循環遍歷所有“地標”數據。
    5. 在完成塊的最後,發送一個通知。

    讓我們來逐行分析上述代碼:

    構建請求
    首先,用一個可空綁定判斷 self.session 是否為空。如果不為空,將 self.session 解包到 session 中。

    然後,調用 location 的 parameters 方法。這個 location 是哪來的?在 getCoffeeShopsWithLocation 方法中有一個參數 location。每當調用這個方法時,都需要向 location 參數傳遞一個位置參數。另外,parameters 方法則來自於我們前面創建的擴展。

    然後,向 parameters 字典中加入一個新對象,鍵名設為 Parameter.CategoryId,值設為字符串 “4bf58dd8d48988d1e0931735”。這個字符串是 Foursquare 中的類別 ID,表示“Coffeeshops”的意思。

    設置請求
    然後來創建請求。調用 session.venues.search() 方法。這個方法需要兩個參數(并不是一個參數):我們創建的 parameters 對象,以及尾隨其後的閉包。這種寫法是典型的尾隨閉包的寫法。如果閉包是方法的最後一個參數,則可以不把它寫在調用方法的圓括號內,而放到圓括號()之後并用一對大括號{}包裹住塊。search 方法返回一個引用,指向耗時的搜索線程。搜索線程創建后並不會自動開始,我們需要在後面啟動它(就在方法的最後一句)。

    編寫完成閉包
    然後,我們進入到閉包內部。需要強調一點,儘管這些代碼是順序書寫的,但它們並不會順序執行。閉包只會在搜索任務完成之後執行。App 的執行順序將從 let searchTask … 一句跳到 searchTask.start() 一句,當 HTTP API 向 App 返回數據時,又會跳到 if let response = … 一句開始執行。

    這個閉包的簽名(又叫做閉包表達式語法)是:(result)->Void in。意思是這個閉包有一個參數 result,閉包返回值為空(Void)。這和我們常見的方法是一樣的。

    解析返回結果
    然後是兩個可空綁定 if let:

    • 檢查 result.response 是否為空的,如果它不為空,將它解包并付給 response 常量(同時執行 if 語句)。
    • 檢查 response[“venues”] 是否為空,同時它是否能夠轉換為 [[String: AnyObject]] 類型。

    這種方式能夠確保數據類型是我們期望的。如果转换失败,或者可空绑定失败,if 语句就不會被執行。這就像是一塊石頭上站了兩隻鳥:判斷值不為空,同時嘗試將值轉換為預期的類型。

    你現在知道 venues 是什麼類型麼?它是一個字典的數組,字典的類型則是 String:AnyObject 鍵值對。

    自動釋放內存
    然後,是一件很有趣的事情:我們創建了一個自動釋放池。當然,這是一個很大題目。你了解 iPhone 的內存管理機制嗎?

    基本上,當內存中的對象不再被任何對象引用時,它會被刪除。類似垃圾回收機制,但又不完全相同。當自動釋放池中的對象被釋放時,它被交給自動釋放池處理。當自動釋放池被釋放時,池里的所有內存才被釋放。它就像是批量的內存釋放。

    為什麼要這樣做?自己創建自動釋放池,有助於提高 iPhone 系統的內存管理效率。因為我們需要在自動釋放池中處理數百個 venue 對象,如果不清理內存的話,這會導致內存緊張。對於一般的自動釋放池來說,釋放內存的最早時機是方法結束的時候。因此,這就有可能導致內存耗盡,因為自動回收機制無法及時、迅速地回收內存。通過創建自己的自動釋放池,我們可以干預內存的回收,避免內存空間不足。

    使用 Realm

    然後是“let realm = try! Realm()”一句,這實例化了一個 Realm 對象。在使用 Realm 的數據之前我們需要一個 Realm 對象。 try! 關鍵字是 Swift 的異常處理機制。通過它,我們表明:我們不處理來自於 Realm 的錯誤。在真實項目中,我們不建議這麼做,但在這裡我們這樣做是為了讓代碼更簡潔。

    開始事務
    接下來,調用 realm 對象的 beginWrite 方法。這句代碼開始了一個事務。我們簡單討論一下什麼是效率。看一下下面的例子,你覺得哪個方法更有效率?

    • 創建一個文件指針,打開文件,寫 1 個字節到文件,關閉文件,然後重複這個動作 50 遍。
    • 創建一個文件指針,打開文件,寫 50 個字節到文件,關閉文件。

    答案是第二個方法。Realm 將數據(就像其他數據庫系統一樣)保存到普通的文本文件。要使用文本文件就意味著 OS 需要打開這個文件,為 App 分配讀寫權限,然後將數據一字節一字節地從 App 寫到文件中。

    與一次寫入一個 Realm 對象相反,我們打開文件后一次性寫入了 50 個對象。因為這些對象的數據都是類似的,它們可以一個接一個地成功寫入,這種方法──打開一次文件,寫 50 次,然後關閉文件 ── 是比較快的方法。這就是事務的概念。

    最後一點:如果在事務中有一次寫入失敗,則所有的寫入都會失敗。這就類似于銀行和賬戶:如果你在賬本中記入了 50 筆交易,其中一筆出錯了(賬戶餘額不足),你想取消那筆交易。最終你不得不銷燬整個賬本!事務確保“要麼全部成功,要麼全部失敗”,以此來降低了數據損壞的可能。

    遍歷地標數據

    現在來看看 for-in 循環。在上面的可空綁定語句中,你已經確保 venues 是有效的。用一個 for-in 循環遍歷 venues 數組,在循環中將數組中的元素依次取出放到 venue 變量。

    首先是創建一個 Venue 對象 venueObject。這句語句將拋出一個錯誤,因為到目前為止我們還沒有一個叫做 Venue 的類。我們將這個任務留到稍後解決,現在先不管這個錯誤。

    然後是幾個可空綁定語句。每個可空綁定都用於訪問 venue 變量的一個鍵值對,并將它們轉換為預期的類型。例如,如果 venue 中有一個鍵名為 id 的鍵值對,則將它的值轉換為 String,如果成功,將它賦給 venueObject 的 id 屬性。

    location 的可空綁定看起來要麻煩一些,但其實不然。注意,lat、lng 和 formattedAddress 組成了 location (而不是 venue)。從數據結構來說,它們之間相差了一個層級。

    接下來是 for-in 循環的最後一句:realm.add(venueObject,update:true)。這將 venueObject 對象加入到 Realm中,并寫到數據庫(在事務中)。update 參數表明當同一對象存在的情況下,Realm 會用新數據覆蓋舊對象。後面,我們會為每個 Venue 對象指定一個唯一的 ID,以便 Realm 能夠識別哪個對象是已經存在的。

    錯誤處理
    現在 Realm 將所有的寫入操作放到了事務中,并準備將它們寫到數據庫中去。這個過程中,會出現錯誤。幸運的是,Swift 已經增加了一個可擴展的錯誤處理機制。大致流程如下:

    1. 進行某個危險的操作。
    2. 如果錯誤發生,拋出錯誤。
    3. 由調用這個危險操作的調用者俘獲這個錯誤。
    4. 調用者處理錯誤。

    在大部份語言中這被稱作 try-catch 機制,但 swift 的創造者們把它稱作 do-catch 機制(是的,他們還將 do-while 循環改成了 repeat-while 循環…)。在你的代碼中,它看起來是這樣:

    do {
        try realm.commitWrite()
        print("Committing write...")
    }
    catch (let e)
    {
        print("Y U NO REALM ? \(e)")
    }
    

    危險操作 realm.commitWrite() 方法放在了 do 後面的一對大括號 {} 中。同時在語句前增加了一個 try 關鍵字。往前滾動代碼,找到 try!(有一個感嘆號),這里感嘆號的使用會導致錯誤直接被忽略。

    如果 do{} 語句塊中有錯誤拋出,catch 塊將被執行。catch 塊有一個參數,即 let e,這個 e 中就包含了具體的錯誤。在這個代碼塊中,我們引入了 e 并打印了錯誤信息。當 App 運行并有錯誤拋出時,打印出來的信息會讓我們知道是什麼導致了錯誤。

    你在上面的代碼塊中看到的錯誤處理是非常簡單的。設想類似這樣的固定的錯誤處理系統,你不僅能抓取錯誤,還能使用它們。例如,你向一個文件中寫入數據時,如果磁盤已滿,你可以彈出一個窗口告訴用戶磁盤已滿。如果是老的 Swift 版本,錯誤的處理非常麻煩,稍微搞不好就會讓 App 崩潰。

    Swift 的錯誤處理有一定的強制性。錯誤要麼必須被處理,要麼被忽略,總之不能無緣無故地讓它溜走。處理錯誤使你的代碼更健壯,將使用 do-catch 進行錯誤處理形成一種習慣,而不要使用 try! 來忽略錯誤。

    好,進入這個方法的最後兩句代碼。首先是第一句:

    NSNotificationCenter.defaultCenter().postNotificationName(API.notifications.venuesUpdated, object: nil, userInfo: nil)
    

    這句代碼會發送一個通知,給所有 App 中監聽了該通知的對象。這是 de facto 的通知機制,如果 App 有多個地方需要接收這個通知,這種方法非常有效。設想你剛剛從 Foursquare 收到新的數據。你可能會刷新表格視圖,以顯示新數據,也可能會觸發其它代碼。這時,只消一個通知就可解決所有問題。

    注意發送通知的那個線程,即上面代碼所在的線程。如果你在主線程之外即發送通知的那個線程中進行更新 UI 操作,則 App 會崩潰并拋出一個致命的錯誤。

    注意到 API.notificatoins.venuesUPdated 這個字符串嗎?這是一個硬編碼的字符串,我們也可以用 “venuesUpdated”替代。但使用硬編碼的編譯時常量可以使你的代碼更安全。如果你代碼寫錯了,編譯器會警告你。如果你把字符串“venuesUpdated”寫錯了,則編譯無論如何都不會告訴你!

    在閉包之後,是最後一句代碼:

    searchTask.start()
    

    注意這句代碼在 let searchTask … 之後執行,無論它前面的閉包執行與否。這句代碼什麼意思?我們已經創建了一個請求,設置了它所需的參數,這句代碼的作用就是啟動搜索任務。

    Das Quadrat 庫向 Foursquare 發送了一條消息,并等待返回,然後調用你編寫的閉包對返回的數據進行處理。非常簡單,不是嗎?

    暫時離開這段代碼,因為我們還有一個 Venue 類沒有編寫。

    編寫 Venue 類

    你知道 Realm 最厲害的是什麼嗎?整個代碼結構都非常精幹。要使用 Realm,你只需要一個類文件。你可以用這個類創建一堆的對象,將他們寫到 Realm 文件,然後嘣的一下,你的本地數據庫就實現了。

    此外,Realm 還包含了大量有用的特性,諸如排序、篩選,以及使用 Swift 原生數據類型。它非常快,你不需要用 NSFetchedResultsController(Core Data 中的)加載成千上萬的對象到表格視圖。Realm 有它自己的基本的數據瀏覽器。

    好了,來看看 Venue 類。你需要:

    • 在項目導航窗口中,右鍵點擊 Coffee 文件夾。
    • 選擇 New File … 然後從iOS -> Source 下選擇 Swift File,然後點擊 Next。
    • 文件命名為 Venue.swift ,在 target 列表中選中 Coffee。
    • 點擊 Create。

    這會創建一個空的 Swift 文件。這個文件中將包含 Realm 對象的代碼,即 Venue 類的代碼。

    導入正確的庫。在 import Foundation 一句下加入:

    import RealmSwift
    import MapKit
    

    繼續在下邊加入:

    class Venue: Object
    {
    
    }
    

    這是 Venue 類的簽名。冒號用於表示你將繼承 Object 類。 在面向對象編程中,你可以為類之間創建“父﹣子”關係,即繼承的概念。在上面的代碼中,你繼承了 Object 類,這個類在 Realm 庫中定義。

    也就是說,你將父類的所有的屬性和方法複製到了子類中。注意,繼承和創建擴展不同,後者僅僅是用新的功能修飾已有的類(不用創建任何新的類)。

    接著,將下列代碼拷貝到這個類。將它放到类的一对大括號之間。

    dynamic var id:String = ""
    dynamic var name:String = ""
    
    dynamic var latitude:Float = 0
    dynamic var longitude:Float = 0
    
    dynamic var address:String = ""
    

    這是什麼意思?很簡單:為這個類定義了五個屬性。你可以利用這些屬性,將數據賦給這個類的實例,就像我們使用 CoffeeAPI 代碼時所作的一樣。

    dynamic 屬性確保 O-C 運行時能夠訪問這些屬性。這又是另外一個話題了,但我們可以想像成 Swift 代碼和 O-C 代碼分別運行在各自的“沙盒”中。在 Swift 2.0 以前,所有的 Swift 代碼都運行在 O-C 運行時中,但現在,Swift 擁有自己的運行時。將一個屬性標明為 dynamic 之後,O-C 運行時就可以訪問這個屬性了,這是必須的,因為 Realm 底層依賴於 O-C 運行時。

    每個屬性都有一個類型:String 或者 Float。Realm 支持幾種 Swift 原生類型,比如 NSData,NSDate(精度為秒),Int,Float,String 等等。

    然後,在 address 屬性下加入:

    var coordinate:CLLocation {
        return CLLocation(latitude: Double(latitude), longitude: Double(longitude));
    }
    

    這是一個計算屬性。它不會保存到 Realm 中,因為計算屬性是不會被保存的。所謂的計算屬性,名副其實,是說這個屬性其實是来自于某個表達式計算的結果。它就像一個方法,但是以屬性的形式來調用。上面的這個計算屬性中,我們將緯度和精度轉換成一個 CLLocation 對象。

    經過這樣的轉換后會方便許多,因為我們可以通過 venueObject.coordinate 來訪問正確類型的對象,而不需要再臨時創建一個。

    然後在上面的代碼後面加入:

    override static func primaryKey() -> String?
    {
        return "id";
    }
    

    這是個新方法,我們覆蓋了來自於父類 Object 的同名方法。通過這個方法你可以告訴 Realm 用什麼來做主鍵。主鍵的概念類似于唯一標識。在 Realm 數據庫中,每個對象都必須擁有一個唯一的主鍵,就像鎮子里的每棟房屋都必須有一個唯一的門牌號。

    Realm 通過主鍵來區分不同的對象,並以此來判斷一個對象是否和另一個對象相同。

    這個方法的返回值是 String,因此我們可以返回一個屬性名,并以該屬性來作為主鍵。如果不想使用主鍵,則可以返回一個 nil。

    你可以將 Realm 對象的屬性(比如 id、name)想像成表格中的列。primaryKey 的返回值就是這些列中的某一列,這裡就是 id。

    最後,按下 Command + B,編譯 App,查看是否一切正常。這裡我們不運行 App,因為我們還沒有修改 UI 代碼。我們編譯只是為了測試我們的代碼是否有錯誤。如果你檢查一下 CoffeeAPI.swift 中的代碼,你會發現 venueObject 旁邊的錯誤提示消失了。

    在地圖中顯示地標數據

    現在,讓我們用下載的數據做一些事情。我們將數據以大頭釘的形式顯示到地圖上。

    首先,回到 ViewController.swift 文件。看一下將用戶位置顯示到地圖上的代碼。

    然後,在文件頭部,加入 import 語句:

    import RealmSwift
    

    在類中聲明幾個屬性(在 distanceSpan 下面):

    var lastLocation:CLLocation?
    var venues:Results?
    

    要讓 RealmSwift 庫能夠使用 Realm,我們需要用這兩個屬性存放座標和地標數據。

    接著,找到 locationManager:didUpdateToLocation:fromLocation 方法。然後找到這個方法的右大括號 }。在這下面加入下列代碼。

        func refreshVenues(location: CLLocation?, getDataFromFoursquare:Bool = false)
        {
            if location != nil
            {
                lastLocation = location
            }
    
            if let location = lastLocation
            {
                if getDataFromFoursquare == true
                {
                    CoffeeAPI.sharedInstance.getCoffeeShopsWithLocation(location)
                }
    
                let realm = try! Realm()
    
                venues = realm.objects(Venue)
    
                for venue in venues!
                {
                    let annotation = CoffeeAnnotation(title: venue.name, subtitle: venue.address, coordinate: CLLocationCoordinate2D(latitude: Double(venue.latitude), longitude: Double(venue.longitude)))
    
                    mapView?.addAnnotation(annotation)
                }
            }
        }
    

    哇,這個方法有好多代碼!它們是什麼意思?

    先來看檢查兩個座標的 if 語句。第一個 if 語句檢查 location 是否為空,第二個 if 語句檢查 lastLocation 屬性是否為空(用一個可空綁定)。

    這兩行代碼非常類似,雖然它們干的是不同的事情。先讓我們暫停一下。思考一下下列描述是否正確:

    • App 中的所有座標必須都來自于 locationManager:didUpdateToLocation:fromLocation 方法。只有這個方法才會向 App 傳入 CLLocation 對象,而這個對象的數據來自於 GPS 硬件。
    • refreshVenues 方法使用一個 location 參數,這個參數是可空的。
    • refreshVenues 方法可以在 location 為空的時候調用,也就是說,會在 locationManager:didUpdateToLocation:fromLocation 方法之外的代碼中調用。

    最後一點非常重要。很顯然,因為我們想讓 refreshVenues 方法在 locationManager:didUpdateToLocation:fromLocation 方法之外也能被調用,因此我們要將座標數據保存到某個地方。

    每當 refreshVenues 方法被調用,我們都在 location 參數不為空時將它保存到 lastLocation 參數。然後,我們用可空綁定檢查 lastLocation 參數是否為空。只有不為空 if 語句才會被執行,因此我們能夠保證 if 語句中的代碼 100% 的有一個有效的 GPS 座標可用。

    這讓 refreshVenues 方法真正能夠讀取到真正的座標數據。這是毫無疑問的。如果你還不明白,請再次閱讀上一段內容。代碼非常簡單的,這樣的寫法也讓你的 App 在保證數據安全的同時保持解耦。

    然後是 refreshVenues 方法的下一行。它又是什麼意思?它通過 CoffeeAPI 的共享實例從 Foursquare 請求數據。

    if getDataFromFoursquare == true
    {
        CoffeeAPI.sharedInstance.getCoffeeShopsWithLocation(location)
    }
    

    它只會在 getDataFromFoursquare 參數為 true 時進行請求。讓 CoffeeAPI 請求數據是件簡單的事情。記住,如果我們想在數據抓取完畢的時候獲得消息,我們需要監聽 CoffeeAPI 的通知。這個步驟我們稍後進行。

    然後是下面的代碼:

    let realm = try! Realm()
    venues = realm.objects(Venue)
    

    這個代碼我們已經熟悉了,但這就是重要的地方。首先,獲取了一個 Realm 的引用。然後 Realm 讀取所有的 Venue 對象并保存到 venues 屬性。這個屬性的類型為 Result?,類似于一個 Venue 對象數組(會有輕微的不同)。

    最後,是一個 for-in 循環,遍歷了 venues 中的所有 Venue 對象,然後以大頭釘形式添加到地圖上。這裡很可能會拋出一個錯誤,我們會解決它。

    創建 Annotation 類

    要創建一個 Annotation 類,你需要:

    1. 在 Coffee 文件夾上點擊右鍵,選擇 New File ….
    2. 在 iOS -> Source 下面選擇 Swift File,點擊 Next。
    3. 文件命名為 CoffeeAnnotation.swift 然後點擊 Create。

    編輯文件內容為:

    import MapKit
    
    class CoffeeAnnotation: NSObject, MKAnnotation
    {
        let title:String?
        let subtitle:String?
        let coordinate: CLLocationCoordinate2D
    
        init(title: String?, subtitle:String?, coordinate: CLLocationCoordinate2D)
        {
            self.title = title
            self.subtitle = subtitle
            self.coordinate = coordinate
    
            super.init()
        }    
    }
    

    代碼很簡單:

    • 我們創建了一個名為 CoffeeAnnotation 的類,繼承自 NSObject 并實現了 MKAnnotation 協議。後者很主要:如果你需要讓一個類作為大頭釘顯示,你必須讓它遵循 MKAnnotation 協議。
    • 然後,聲明了幾個屬性。這些屬性都是必須的,這是協議中規定的。
    • 最後是一個構造函數,用方法參數對屬性進行了賦值。

    回到 ViewController.swift,在看一下 CoffeeAnnotation 旁邊的錯誤提示是否消失了。

    下一步,在 ViewController 類中添加如下方法。這個方法的代碼很常見,它確保你加到地圖的大頭釘能夠得到顯示。

    func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView?
    {
        if annotation.isKindOfClass(MKUserLocation)
        {
            return nil
        }
    
        var view = mapView.dequeueReusableAnnotationViewWithIdentifier("annotationIdentifier")
    
        if view == nil
        {
           view = MKPinAnnotationView(annotation: annotation, reuseIdentifier: "annotationIdentifier")
        }
    
        view?.canShowCallout = true
    
        return view
    }
    

    就像表格視圖一樣,地圖視圖會重用對象以使大頭釘能夠順暢地顯示到地圖上。在上面的代碼中,發生了這些事情:

    • 首先,檢查大頭釘不是用戶的標誌。
    • 從緩存中取出一個現成的大頭釘。
    • 如果取出的是一個空對象,則創建一個新的對象。
    • 設置大頭釘的是否顯示標註屬性(一個帶小框的信息)。
    • 最後,返回 view,以便它能顯示。

    注意這個方法是協議中定義的方法。前面我們已經將地圖視圖的 delegate 設置為 self。如果地圖視圖設置了 delegate 屬性,則當地圖準備顯示大頭釘的時候,它會調用 mapView:viewForAnnotation: 方法,這個方法就是上面的這段代碼。

    委託是一種很好的自定義代碼的方法,它避免了重寫整個類。

    處理地標數據通知

    讓我們將所有珠子串起來。在 ViewController.swift 的 viewDidLoad 方法中,添加下列語句,就在 super… 的下方:

    NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("onVenuesUpdated:"), name: API.notifications.venuesUpdated, object: nil)
    

    這一句將告訴通知中心當前類(self)將監聽 API.notification.venuesUpdated 通知。當出現這個通知時,請調用 ViewController 的 onVenuesUpdated: 方法。簡單吧?

    在 ViewController 類中新加一個方法:

    func onVenuesUpdated(notification:NSNotification)
    {
        refreshVenues(nil)
    }
    

    這又是幹什麼意思?

    • 當從 Foursquare 收到位置數據后,調用 refreshVenues 方法。
    • 調用時沒有提供座標數據,也沒有提供 getDataFromFoursquare 參數,這個參數默認為 false,因此不需要從 Foursquare 請求數據。如果不這樣的話,會導致一個無限循環,因為當數據返回后又會創建一個 Foursquare 請求。
    • 這樣,當 Foursquare 數據返回,地圖上就會畫出大頭釘。

    還有一個至關重要的部份。在 locationManager:didUpdateToLocation:fromLocation: 方法的 if 語句內部的最後添加如下代碼:

    refreshVenues(newLocation, getDataFromFoursquare: true)
    

    這個方法現在變成了這樣:

    if let mapView = self.mapView
    {
        let region = MKCoordinateRegionMakeWithDistance(newLocation.coordinate, distanceSpan, distanceSpan)
        mapView.setRegion(region, animated: true)
    
        refreshVenues(newLocation, getDataFromFoursquare: true)
    }
    

    這段代碼什麼意思?很簡單:它以用戶的 GPS 座標來調用 refreshVenues 方法。另外,它告訴 API 從 Foursquare 抓取數據。也就是說,每當用戶的位置發生變化,它就會從 Foursquare 抓取數據。當然我們設置了每移動 50 米才會觸發這個方法。幸虧有通知中心,地圖才會刷新!

    運行 App,檢驗它是否工作正常。怎麼樣?干得不錯吧!

    Foursquare Venue Data In Tokyo

    在表格視圖中顯示地標數據

    現在地圖已經完成了,如果再在表格視圖中顯示這些數據,整個 App 就完成了。這個實現起來是非常簡單的。

    首先,在 ViewController 中增加一個 IBOutlet 屬性,就放在類的頭部,mapView 屬性的下面。

    @IBOutlet var tableView:UITableView?
    

    打開 Main.storyboard,然後選擇 View Controller Scene。打開 Connections 面板,找到 tableView 并拖一條線到故事板編輯器的表格視圖上。這就創建了一個出口連接。

    在 ViewController.swift 的 viewWillAppear 中添加下列代碼,就像對 self.mapView 所做的一樣,使用一個可空綁定:

    if let tableView = self.tableView
    {
        tableView.delegate = self
        tableView.dataSource = self
    }
    

    為 ViewController 增加兩個協議的聲明:

        
    UITableViewDataSource, UITableViewDelegate
    

    接下來,添加兩個方法:

    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
    {
        return venues?.count ?? 0
    }
    
    func numberOfSectionsInTableView(tableView: UITableView) -> Int
    {
        return 1
    }
    

    這兩個方法屬於表格視圖的 delegate 協議。第一個方法用於指定表格視圖中要顯示的 cell 的行數,第二個方法用於指定要在表格視圖中顯示幾個 section。注意,?? 是一個“空合併”操作。意思是說:當 venues 為空的時候,用 0 來作為默認值。

    然後,添加這個方法:

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
    {
        var cell = tableView.dequeueReusableCellWithIdentifier("cellIdentifier");
    
        if cell == nil
        {
            cell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "cellIdentifier")
        }
    
        if let venue = venues?[indexPath.row]
        {
            cell!.textLabel?.text = venue.name
            cell!.detailTextLabel?.text = venue.address
        }
    
        return cell!
    }
    

    這些代碼大部份都是千篇一律的:

    • 試圖從緩存中重用(獲取)一個 cell。
    • 如果重用不成功,則創建一個新 cell,風格為 Subtitle。
    • 如果 venues 中能夠索引到 indexPath.row 的對象,則用這個對象來渲染 cell 的 textLabel 和 detailTextLabel。
    • 返回 cell。

    跟地圖視圖差不多,tableView:cellForRowAtIndexPath: 方法在表格視圖需要渲染 cell 的時候被調用。你可以利用這個方法對表格視圖的 cell 進行定製化。這比子類化 cell 要簡單!

    下一步,是最後一個表視圖相關的方法。在 ViewController 中添加這個方法:

    func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath)
    {
        if let venue = venues?[indexPath.row]
        {
            let region = MKCoordinateRegionMakeWithDistance(CLLocationCoordinate2D(latitude: Double(venue.latitude), longitude: Double(venue.longitude)), distanceSpan, distanceSpan)
                mapView?.setRegion(region, animated: true)
        }
    }
    

    這個委託方法會在用戶點擊 cell 的時候調用。這段代碼很簡單:如果在 venues 中找到下標與 indexPath.row 對應的 Venue 對象,則用它去設置地圖視圖的 region 屬性。也就是說:將地圖的中心設置到所點擊的地方!

    最後只剩下一件事情,根據通知來刷新表格視圖的數據。當通知出現,我們就顯示新的數據。

    在 refreshVenues 方法中添加代碼,就在第二個 if 語句之後。找到 if let location = lastLocation 一行,然後找到它的右大括號 }(就在 for-in 循環之後),加入代碼:

    tableView?.reloadData()
    

    好了,現在來看看 App 是否運行正常。用 Command + R 運行 App,查看運行結果。如果一切順利,表格視圖中將出現地標數據。

    Venue data in the table view

    根據座標篩選地標數據

    現在有一件奇怪的事情。表格視圖顯示了所有的數據!如果你先到日本,然後又來到舊金山,那麼在表格視圖中仍然會顯示日本的咖啡屋。

    你當然不想這樣。因此,讓我們來施展 Realm 大法,獲得正確的數據。

    首先,修改 ViewController 的 venues 屬性,將 Results? 修改為這樣:

    var venues:[Venue]?
    

    這又有何不同?僅僅是類型不同而已。之前是用一個包含了 Venue 對象的 Results 對象,這是屬於 Realm 的類型。後面則變成了 Venue 數組類型。

    最大的不同是延遲加載。Realm 加載數據的效率非常高,它只會加載要用到的數據,也就是在代碼中被訪問的數據。不幸的是,Realm 不支持我們想要的一個特性(對計算屬性排序)。因此,我們只能從 Realm 加載所有數據,然後自己來做過濾。正常情況下我們都是讓 Realm 為我們負責數據的讀取(通過延遲加載)和簡單的過濾。但現在不行了。

    還記得這兩行嗎?

    let realm = try! Realm()
    venues = realm.objects(Venue)
    

    用下面的代碼替換它們:

    let (start, stop) = calculateCoordinatesWithRegion(location)
    
    let predicate = NSPredicate(format: "latitude 
    展开全文
  • 第八章-Swift and Cocoa

    2016-09-18 11:58:15
    Swift cocoa

    Swift可能是个非常棒的新语言,但在开发ios应用时一些核心内容比如Cocoa依然保留在系统库中。Cocoa包含了Foundation和UIKit等框架,这都是在开发ios应用程序时非常重要的框架。

    在这一章中,你将编写一个应用程序去探索Swift与Cocoa框架的交互。你将通过交互去了解cocoa的设计模式如何转换到Swift的世界中。

    即将建立的应用是使用Facebook的ios Sdk去登录Facebook的地图找到用户当前的位置。这个应用会在地图view上显示咖啡馆,用户可以找到一条最近一个咖啡馆的路径。

    av8d,go!

    Getting started - 开始

    在第八章的资源文件夹中,已经准备好了开始练习的项目工程。
    打开project,可看到里面有三个Swift文件:

    AppDelegate.swift:这是应用的代理,正如你可能熟悉的oc中的代理文件。在Swift中,还有更多的文件会拿来使用。注意顶部的@UIApplicationMain。同时注意没有使用在oc开发中用来调用UIApplicationMain的main.m文件。在Swift中,应用的代理类被注释为@UIApplicationMain,告诉Swift通过这个类来创建一个UIKit应用。

    ViewController.swift:目前的视图控制器。除了需要找到附近的咖啡馆需要使用到用户的当前位置,便没有其他操作了。

    JSON.swift: 在Swift中json的解析是相当棘手的,因为编译器想要知道当你解析json时需要处理的类型,可是你只有当解析到它时才知道他的类型。这个帮助类能使你的解析变得容易些。

    在项目中你能看到的另外一样是Facebook的sdk,引进了他的框架。打开来看一下,你会发现全是oc的头文件,并不是Swift!

    在写这个代码的时候,Facebook的sdk只有oc版的。但这没问题,正好用来学习Swift和oc混合开发。在未来的几年里这都是一个非常实用的技术,在软件代码慢慢迁移到Swift的过程中,你可能需要在新的Swift中使用一些已经存在的oc代码。

    运行程序看到如下:
    这里写图片描述

    确保你刚刚允许应用有获取位置的权限!现在,让我来继续应用的开发。

    Obtaining a Facebook application ID - 获取一个Facebook的应用id

    因为你的应用需要访问Facebook,所以为了功能的实现,你需要到Facebook申请一个应用程序的id,连接如下:https://developers.facebook.com
    在屏幕的顶端,点击apps,如果显示“Register as a Developer”则点进入通过,然后回到开发者主页。

    你作为一个开发人员注册,点击 Apps\Create a New App。选择应用程序名字为CafeHunter,选择Food & Drink类别后离开。最后,点击创建app。

    你在屏幕上能看到:
    这里写图片描述

    注意应用程序的id(appID),在后面你会需要用到。

    注:你可以使用你在Facebook上注册的其他应用程序的appID。你只是为了开发了解,并不是为了发布,所以只要有一个appID用来调试开发即可。

    现在你有了Facebook的appID了,是该时候来开始代码编写了!

    Bridging Swift and Objective-C - 桥接Swift和oc

    正如前面提到的,在开始项目中的Facebook的sdk里包含的是oc。但是不要害怕,有一种叫桥接的技术能够让你在Swift中使用oc代码,反之亦然!

    Swift bridging header - Swift桥接头文件

    首先,你需要设置下在你应用中的Facebook的sdk,你需要在Swift中桥接oc文件。你会通过一个标准的桥接头文件来让Swift编译器确定使用什么文件。

    现在开始,点击File\New\File… 然后选择iOS\Source\Header File. 点击Next ,并命名为CafeHunter- ObjCBridging.h ,将其保存在viewController.swift的位置。

    接着,点击在xcode顶部的project导航栏,选择Build Settings 并搜索“bridg” ,你会看到如下搜索结果:
    这里写图片描述
    现在你感兴趣的是oc的桥头文件,设置CafeHunter的target,告诉Swift的编译器在哪能找到桥头文件。
    这里写图片描述

    打开AppDelegate.swift并在顶部application(_:didFinishLaunchingWithOptions:)
    添加代码:

    FBSettings.setDefaultAppID("INSERT_YOUR_FB_APP_ID") 

    将INSERT_YOUR_FB_APP_ID替换为你在Facebook上申请的appID,此代码会调用Facebooksdk去设置此应用。

    编译应用程序,啊-你会注意到他并没有编译!这是因为你还没有在桥头文件添加代码,所以还无法引用Facebook的sdk。

    打开CafeHunter-ObjCBridging.h并在#define和#endif行中添加代码:

    #import <FacebookSDK/FacebookSDK.h>

    再次编译一次,这次能顺利工作了!实在是有些不可思议,Facebook中的oc类直接导入到了Swift中!上面调用的实际是oc中方法setdefaultappid:,在oc的类FBSettings中,FBSettings.h
    中的声明如下:

    +(void)setDefaultAppID:(NSString *)appID; 

    说起来很神奇!事实上,任何导入桥头的oc或c文件,编译器都会将其转入Swift中

    Objective-C compatibility header - oc兼容头文件

    正如你看到的Swift桥接了头文件,所以你可以在Swift代码中使用oc代码,有没有什么办法让你可以在oc中使用Swift的代码呢?

    还记得早前设置build setting时看到的Objective-C Compatibility Header选项吗?就是他,他已经被设置为yes,也就是是默认是开启的。点击左边导航栏下边最后一个build,如下图所示:
    这里写图片描述

    双击Copy CafeHunter-Swift.h 打开此文件,在里面你会看到一些看起来像是oc的东西。因为他就是oc…!这就是众所周知的Objective-C compatibility header,实现Swift的反向桥接。

    在文件的底部,你会看到如下东西:
    这里写图片描述

    这看起来就像是一个在oc中的普通控制器代码,除了顶部有点怪怪的SWIFT_CLASS宏。事实上,这就是一个在oc中普通的控制器引用。这也是为什么你可以在oc中使用Swift类的原因。

    Objective-C compatibility header包括了你项目中任何继承自oc类的Swift类,例如这个例子中的UIViewController。他也包含了不是继承自oc类的Swift类,比如标记了@objc的类。如果你包含的CafeHunter-Swift.h在oc文件中,则你就可以像oc类那样使用一个ViewController了。

    快速的看一下ViewController.swift.注意checkLocationAuthorizationStatus方法在oc的compatibility header并没有出现。这是因为这种方法被标记为私有。因此不在oc接口暴露,即使该方法在运行时存在。

    另一件要注意的事情是,在使用接口前有一个宏。这个接口看起来很奇怪_TtC10CafeHunter14ViewController。

    这是Swift的名字重整。Swift隐式的为你添加了名字命名空间,也就是说你可以在一个库中有个类叫Foo,在另一个库中也有个类叫Foo,他们互相之间没有交集。Swift通过转变每个类,结构,枚举,以及其他的符号名称,包括库名以及其他允许简单的逆转回原来名字的信息。在这个例子中,库就是这个app的本身,因此他的名字是CafeHunter。

    所以实际上ViewController类在运行时叫_TtC10CafeHunter14ViewController。SWIFT_NAME宏告知oc编译器重新命名后的真实的名字。

    你要学的是Swift,不是oc,所以本章不会花太多的时间在oc类中如何使用Swift类。但你要记住怎么实现,因为将来你可能会用到。

    Adding the UI - 添加UI

    你当前的应用程序目前只有一个白色的屏幕,以及一个要求使用位置服务权限的对话框。是时候添加下用户界面了。

    打开 Main.storyboard找到CafeHunter view controller。添加一个MapKit和一个普通的view。将普通的view设置为黑色背景。在identity inspector上设置普通view的类是FBLoginView,然后调整布局如下所示:
    这里写图片描述

    设置自动布局约束,使地图水平,垂直扩展,那个普通view设置为宽200,高50.

    接着打开ViewController.swift 在顶部添加:

    @IBOutlet var mapView: MKMapView! 
    @IBOutlet var loginView: FBLoginView! 

    这些都是普通的属性声明,一个是mapView,一个是特殊的Facebook登录view,这个登录view能帮你处理登录!

    这些属性看上去和oc开发中的Cocoa非常相像。@IBOutlet做的事和oc中修饰属性的IBOutlet一样:可以生成一个对应UI上的变量。

    变量的类型必须是可选的,否则编译器会提示这个变量没有设置初始值。Swift并不能知道Interface Builder在运行时提供变量,因此,要做这步工作,减少生成没提供初始值的错误。

    然而,这也就需要你在用这些outlets时要格外小心。因为这些变量都是隐式解包的可选类型,你可能会不检查nil就直接使用他。如果在viewcontroller加载前使用,outlet是nil,则运行会崩溃!一定要格外小心!

    需要注意的是,在幕后,@IBOutlet修饰符设置对应的属性为弱引用,因此这两个属性实际情况是:

    weak var mapView: MKMapView! 
    weak var loginView: FBLoginView! 

    你可能在oc开发中就已经发现了outlet的属性是weak弱引用,因为ViewController有一个强引用引用Viewcontroller上的view,所以额外的设置其他outlet属性为强引用是没有必要的。

    回到Main.storyboard并将mapView和普通的view与控制器的代码进行关联,此外,设置ViewController作为mapView的代理。

    要使用facebook进行登录,还需要设置下。Facebook sdk会切换到Facebookapp(如果已经安装)或者Safari。登录完成后,sdk需要使用一个特殊的URL来打开你的应用程序,也就是说你的应用程序需要处理特殊的网址。

    单击project导航栏上的project然后选择target中的info,打开URL Types然后将fbxxx填入到URLSchemes中。fb后面跟的是你在Facebook上申请的appID。比如,如果你的应用id是“12345”,你需要在这填“fb12345”.

    这里写图片描述

    最后,打开AppDelegate.swift并添加代码如下

        func application(application: UIApplication, openURL url: NSURL,sourceApplication: String?, annotation: AnyObject) -> Bool
        {
            let wasHandled =
                FBAppCall.handleOpenURL(url, sourceApplication: sourceApplication)
            return wasHandled 
        } 

    这个方法在用户使用Facebook登录后会调用,通过这个方法返回到应用程序中。这个代码只是简单处理应用程序通过Facebook登录并返回应用程序中。

    编译并运行,你会看到应用显示Log in with Facebook,点击sdk将跳转进入登录操作。在你登录过后会返回应用程序,且这个按钮会改为“Log out”。

    恭喜你用oc的Facebook代码登录进了你的Swift应用中!
    这里写图片描述

    Showing the user’s location - 展示用户的位置

    地图view 应该能找到用户的当前位置,现在我们就来实现。添加一个当用户有地理位置信息或者用户移动了一个比较明显的距离时触发的方法。

    打开ViewController.swift 并在变量的顶部添加locationManager变量:

    private var lastLocation: CLLocation? 

    你将用词来表示用户已知的最后一个坐标。因为这个坐标值可能没获取到所以设置为可选类型。

    接着在这个类的顶部继续添加:

    let searchDistance: CLLocationDistance = 1000 

    这声明了一个用于从用户当前位置进行搜索的宽度范围常数,以及当用户离开这个长度距离时会自动刷新cafe馆信息。距离这里用的是米。

    接下来在文件的底部添加扩展声明:

    extension ViewController: MKMapViewDelegate { } 

    此扩展提供了对地图代理协议的实现,这个代理用于告诉你与视图控制器相关的情况,如发现用户的位置等。

    Note:在Swift中,通过使用这样的扩展来声明一个协议是非常平常的事。他将协议方法放在一起,你仍然可以访问其他的方法和属性,现在,将方法添加到刚刚声明的扩展中去。

        func mapView(mapView: MKMapView, didFailToLocateUserWithError error: NSError)
        {
            print(error)
            let alert = UIAlertController(title: "错误",message: "没有坐标信息!", preferredStyle: .Alert)
            alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
            self.presentViewController(alert, animated: true, completion: nil)
        }

    如果用户定位失败,比如,当用户在地下室等gps无法工作的地方,这个方法会告知用户错误。

    接着,在这个扩展中继续添加方法:

      func mapView(mapView: MKMapView,didUpdateUserLocation userLocation: MKUserLocation)
        {
            // 1
            let newLocation = userLocation.location
            // 2
            let distance = self.lastLocation?.distanceFromLocation(newLocation!)
            // 3
            if distance == nil || distance! > searchDistance {
                self.lastLocation = newLocation
                self.centerMapOnLocation(newLocation!)
                self.fetchCafesAroundLocation(newLocation!) 
            }
        }

    当更新了用户的当前位置时这个方法会回调。下面来解析下:

    1.你从代理方法的userLocation参数中获取到新的地址坐标。
    2.你从上一个坐标计算距离。注意lastLocation属性值这里用的是问号。lastLocation是一个可选类型的属性,也就是说他的值可能为nil。如果是nil,则这个表达是返回nil且不会继续后面操作。只有当这个属性中有值是distanceFromLocation才会被调用。基于这个原因,distance的变量也是个可选类型。

    3.如果没有距离信息或者用户已经移动了一定的距离后,你会想要更新下地图。因为distance是一个可选类型,所以你可以用if语句轻松的完成检查。如果不是可选类型,这个检查会复杂的多,因为你没法区别没有distance值和distance的值为0的区别。可选类型,你值得拥有!

    如果你需要更新地图,则你需要设置lastLocation属性并调用用户的位置中心以及周围的cafe馆信息。

    这个方法要用到你还没实现的两个方法。在Viewcontroller.swift中,在Viewcontroller类的定义下先添加第一个方法:

    private func centerMapOnLocation(location: CLLocation) { 
        let coordinateRegion = MKCoordinateRegionMakeWithDistance(location.coordinate,      searchDistance, searchDistance) 
        self.mapView.setRegion(coordinateRegion, animated: true) 
    } 

    这个方法需要传入地图一个坐标作为地图的中心。你的地图搜索区域大小是你定义的常数决定的,所以一旦找到cafe馆信息,会有足够的位置显示所有的cafe馆。

    接着在前一个方法后面添加一个新的方法:

    private func fetchCafesAroundLocation(location: CLLocation) { 
        if !FBSession.activeSession().isOpen { 
            let alert = UIAlertController(title: "Error", 
                message: "Login first!", preferredStyle: .Alert) 
        alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil)) 
        self.presentViewController(alert, animated: true, completion: nil) 
        return 
        } 
        // TODO 
    }

    你可以立即搜索到cafe馆。目前,如果facebook的会话没有打开的话这个方法只会简单的显示一个错误。如果打开了这个会话的话则是一个用户登录如果。facebook的api需要用户的访问token,所以如果你需要抓取数据的话你需要先进行登录。

    最后,找到checklocationauthorizationstatus并改变成如下:

    func checkLocationAuthorizationStatus() {

    if CLLocationManager.authorizationStatus() == .AuthorizedWhenInUse { 
        self.mapView.showsUserLocation = true 
    } else { 
        self.locationManager.requestWhenInUseAuthorization() } 

    }

    为了测试目的,你可以修改你模拟器上的坐标来调整你当前的位置,可以在xcode控制台的文本编辑器窗格底部找到他:
    这里写图片描述

    选择London,England,然后能看到如下:
    这里写图片描述

    是时候抓取一些cafe馆数据了。

    Fetching data - 抓取数据

    地图上现在还没显示任何咖啡馆,现在就来实现。首先,你需要创建数据模型,然后从Facebook获取到咖啡馆信息,最后解析这些数据保存。

    Building the data model - 建立数据模型

    你需要个模型对象来表示每个咖啡馆。在Swift中,你有两种选择:用类或者结构。因为咖啡馆对象只是纯粹的数据,所以,貌似用结构不错。

    点击 File\New\File… 然后选择iOS\Source\Swift。点击next,并命名文件为Cafe并保存。
    打开Cafe.swift然后添加代码:

    struct Cafe {

        let fbid: Stringlet location: CLLocationCoordinate2D 
        let city: Stringlet zip: String 
        init(fbid: String, name: String,location: CLLocationCoordinate2D,street: String, city: String,zip: String) 
    {
  self.fbid = fbid 
        self.name = name 
        self.location = location 
        self.street = street 
        self.city = city 
        self.zip = zip 
        } 
    } 

    这里增加了一个叫Cafe的结构,里面对应着从Facebook api中获取的各种属性。这里没什么特别的东西。

    你想要在地图上标注这些咖啡馆。要实现这一点,你需要让对象符合MKAnnotation协议。通过在底部的扩展文件中实现协议:

    extension Cafe: MKAnnotation { 
        var title: String! { 
            return name 
        } 
        var coordinate: CLLocationCoordinate2D { 
        return location 
        } 
    } 

    编译器报错:Use of undeclared type ‘MKAnnotation’

    这个很容易解决,因为你没有导入MapKit所以编译器无法找到这个协议。滚动到文件顶部,添加引入代码:

    import MapKit 

    但是等等,编译器依然报错!你会看到如下:

    Non-class type ‘Cafe’ cannot conform to class protocol ‘MKAnnotation’

    这个错误提示你cafe必须是类而不能是个结构,为什么呢?
    MKAnnotation对象需要在oc编写的mapView上使用,oc的桥接声明在前一个章节中已经讲过了,因为在Swift中不支持结构的桥接。在Swift中,结构可以有方法,但在oc中结构就是c结构,只是个数据对象,所以无法桥接。

    为了删除这个错误,修改cafe的声明如下:

    class Cafe { 

    现在是类结构了,但是还需要注意另外一个错误:

    Type ‘Cafe’ does not conform to protocol ‘NSObjectProtocol’

    MKAnnotation 继承自NSObjectProtocol,所以修改如下:

    class Cafe: NSObject { 

    现在Cafe是有了一个父类的类,初始化需要多一个步骤:

    super.init()

    这是为了确保当Cafe对象被初始化时,NSObject’s 也会被初始化。

    注:如果你对最后这一步父类方法的调用有些迷糊,请参照第三章“类和结构体”

    你可能已经发现有时Swift必须使用类,尤其是在和oc代码混编时。

    Fetching from Facebook - 从Facebook获取数据

    打开ViewController.swift ,在类的顶部定义下面的属性,就像下面这样:

    private var cafes = [Cafe]() 

    这是将要显示的当前cafe地图列表。

    现在找到fetchCafesAroundLocation和TODO注释。这就是你现在要做的,用下面的代码来替换掉TODO注释:

           // 1
            var urlString = "https://graph.facebook.com/v2.0/search/"
            urlString += "?access_token="
            urlString += "\(FBSession.activeSession().accessTokenData.accessToken)"
            urlString += "&type=place"
            urlString += "&q=cafe"
            urlString += "&center=\(location.coordinate.latitude),"
            urlString += "\(location.coordinate.longitude)"
            urlString += "&distance=\(Int(searchDistance))"
    
            // 2
            let url = NSURL(string: urlString)
            print("Requesting from FB with URL: \(url)")
    
    
            // 3
           let request = NSURLRequest(URL: url!)
            NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue()) {
                (response: NSURLResponse?, data: NSData?, error: NSError?) -> Void in
                // 4
                if error != nil {
                    let alert = UIAlertController(title: "Oops!", message: "An error occured", preferredStyle: .Alert)
                    alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
                    self.presentViewController(alert, animated: true, completion: nil)
                    return
                }
    
                var error: NSError?
                let jsonObject: AnyObject!
                do {
                    jsonObject = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions(rawValue: 0))
                } catch let error1 as NSError {
                    error = error1
                    jsonObject = nil
                } catch {
                    fatalError()
                }
                if let jsonObject = jsonObject as? [String:AnyObject] {
                    if error == nil {
                        print("Data returned from FB:\n\(jsonObject)")
                        // 6
                        if let data = JSONValue.fromObject(jsonObject)?["data"]?.array {
                            // 7
    
                            var cafes: [Cafe] = []
                            for cafeJSON in data {
                                if cafeJSON.object != nil {
                                   // TODO: Create Cafe and add to array
                                }
                            }
                            // 8
                            self.mapView.removeAnnotations(self.cafes)
                            self.cafes = cafes
                            self.mapView.addAnnotations(cafes)
                        }
                    }
                }
            }

    看起来有些凌乱,让我们分步解析:

    1.首先,你构建了一个URL用来以当前坐标向facebook询问附近的cafe馆。注意,这里使用了字符串插入的方式创建了一个较为复杂的字符串。如果使用NSString的stringwithFormat会更复杂些。

    2.然后你将string转换为NSURL。虽然NSURL的初始化要求的是一个NSString,但他仍然可以处理Swift的String对象。因为String和NSString无缝桥接。当你使用Cocoa的api时,Swift可以自动帮你转换处理。

    3.然后你使用NSURLConnection的sendAsynchronousRequest发出请求。因为最后一个参数是block,所以可以使用尾随闭包语法。

    4.这段代码将对从facebook的api中返回的数据进行json序列化。oc开发员对这里error参数应该非常熟悉,这是oc复制过来的普通模式中的一部分:因为方法不能返回多个值,所以你可以在方法中用一个指针指向NSError对象。如果有错误,你可以从error中找到指定的参考。

    在Swift中这种模式并不是必须的,但因为Cocoa API一直还在使用所以保留下来了。Swift允许你使用一个可选类型的NSError来处理这个模式,用相同的方式来设置是否有错。

    5.你期望json序列化返回的是一个json对象。如字典格式的字符串,数组,对象等。你知道如facebook这样大多数的api返回的都应该是json格式。if语句用例将jsonObject变量转换为AnyObject的字符串的字典。如果成功的下载且没有错误,则表示你成功的从api中收到了有效的数据。

    细心的读者应该能注意到NSJSONSerialization返回的是一个NSDictionary。自动的完成了Cocoa类型和Swift类型的桥接,就像看到的NSString和String。NSDictionary和NSArray对应桥接为[NSObject:AnyObject] a和 [AnyObject]

    6.这行使用了在JSON.swift文件中定义的JSONValue helper,也就是本章“Getting Starte”中提到的。在你没查看前你不知道JSON对象里边是什么类型的。你可以手动提取每一部分并检查其类型,但那样会有相当多的Swift if判断嵌套其中。JSONValue helper通过枚举匹配解析整个json的结构。

    然后使用可选链接去提取json数据中的key。如果key存在则将其值转换到一个数组,如果if语句通过则你有一个附近位置的数组。

    7.你创建一个新的数组用来保存你解析出来的Cafe对象的数组信息。稍后你会完善这个内部循环。

    8.最后,你从map中删除存在的cafe病添加一个新的上去。

    这里还有些东西要提下,虽然有些麻烦,但这里的才是关键点:
    不管你是用Swift或者oc来写代码,在Cocoa的error处理方式通常都是通过一个可选类型的NSError变量的应用。当有error时变量会包含有相关的error信息。

    Swift的[NSObject:AnyObject]无缝桥接NSDictionary,反之亦然。

    json的性质使其很难在Swift中进行处理。用大量的类型进行判断你需要非常的小心,而且十分麻烦。用一个如JSONValue的helper能让你简单不少。

    编译并运行程序,然后看控制台,应用程序已经找到了当前的坐标位置,如下所示:
    Requesting from FB with URL: https://graph.facebook.com/v2.0/search/?access_token=CAAEn00…&type =place&q=cafe&center=51.50998,-0.1337&distance=1000
Data returned from FB:
[paging: {
    next = “https://graph.facebook.com/v2.0/search?type=place&center=51.50998 ,- 0.1337&distance=1000&access_token=CAAEn00…&limit=5000&offset=5000& __after_id=enc_Aez8JAnU-42GS9d- ffWv1x1cw9sLQy3jvsm7ipg_zDW0Yb9Rqp96AKIhM1CzBu
    F602DWN7yBabSeyasmeg DQwbJ7”;
}, data: (
    {
category = “Restaurant/cafe”; “category_list” = (
    {
id = 192831537402299;
    name = “Family Style Restaurant”; },
    {
id = 197871390225897;
    name = Cafe; },
    {
id = 133436743388217;
    name = “Arts & Entertainment”; }
    ); id = 63834778746; location = {
    city = London;
country = “United Kingdom”;
latitude = “51.510830565071”; longitude = “-0.13391656332172”; state = “”;
street = “20-24 Shaftesbury Avenve”; zip = “W1D 7EU”;
    };
    name = “Rainforest Cafe, London”; },

    现在你只需要简单的完成json解析提取cafe数据并为每个cafe信息创建一个对象即可。

    Parsing the JSON data - 解析JSON数据

    打开Cafe.swift 并在Cafe类的定义下添加代码:

    class func fromJSON(json: [String:JSONValue]) -> Cafe? { // 1 
        let fbid = json["id"]?.stringlet name = json["name"]?.stringlet latitude = json["location"]?["latitude"]?.double let longitude = json["location"]?["longitude"]?.double 
    // 2 
    if fbid != nil && name != nil
&& latitude != nil && longitude != nil { 
    // 3 
    var street: String
if let maybeStreet = json["location"]?["street"]?.string { 
    street = maybeStreet } else { 
    street = "" } 
    // 4 
    var city: String
if let maybeCity = json["location"]?["city"]?.string { 
    city = maybeCity } else { 
    city = "" } var zip: String
if let maybeZip = json["location"]?["zip"]?.string { 
    zip = maybeZip } else { 
    zip = "" } 
    // 5 
    let location = CLLocationCoordinate2D(latitude: latitude!, 
    longitude: longitude!)
return Cafe(fbid: fbid!, name: name!, location: location, 
    street: street, city: city, zip: zip) 
    } 
    // 6 
    return nil 
    } 

    这个方法的作用是在json数据中找到cafe对象,如果解析成功返回一个cafe对象,没有则返回一个nil。下面来解析是如何工作的:

    1.首先,你需要从json中获取fbid,name,latitude和longitude。如果json不包含“id”这个key,则fbid是nil。如果json[“id”]包含的内容不是string则fbid依然是个nil。

    2.如果成功的解析了Facebookid,name,latitude和longitude,就可以创建一个Cafe对象了。

    3.这里你需要处理第一个可选类型参数,如果没有street,则会用一个空字符串代替。

    4.同样的如果城市和邮政没有,则用空字符串代替。

    5.最后,你创建一个cafe并返回。

    6.如果你因为某个参数的缺失无法创建cafe对象,则用nil返回表示error。

    如果你是个oc开发者的话,你可能会想知道为什么这里不能用一个初始化方法。毕竟,在oc中这样的方法你一般跟都是在初始化中实现。

    你不能在这里使用初始化方法,因为在Swift中,初始化方法不能返回一个nil,必须返回一个完全有值的对象。如果初始化可以返回nil,则每个变量都需要去做可选类型nil的判断。这也就是说,Swift为了保证在运行时的对象正确,不能有可选类型了。

    回到ViewController.swift 并找到fetchCafesAroundLocation,替换掉TODO 如下:

    if let cafe = Cafe.fromJSON(cafeJSON) { 
        cafes.append(cafe) 
    } 

    这个方法用来添加你刚从json解析回来的cafe对象,如果有值,则将其添加到cafes数组中。

    编译并运行,UI如下:
    这里写图片描述

    哇哦,数据都在地图上显示出来了,这里有好多的cafe馆!

    Selectors - 选择器

    该应用程序在用户离开了搜索的区域时会自动刷新搜索cafe。一些耐心稍差的用户可能希望能比自动刷新更快的方式看到最新的结果,所以你需要添加一个刷新功能。这个按钮是值得做的,当用户的网络连接出现异常时,需要用这个按钮进行重试。

    本节将详细的介绍有关和cocoa api交互的另一样东西:Selectors
    oc使用动态分配,在运行时根据方法名去执行。在这种情况下,oc的方法名叫做selector。举个例子,你可以让用户在文本框中输入内容,然后在任何对象上通过方法名字进行调用。很强大,但也有潜在风险。

    Swift没有使用动态分配,而是用编译器来确定一个给定的方法是否存在。但有一些cocoa api依然需要使用selector。例如,控件的target-action模式需要你定义一个selector执行的目标。手势识别也需要做相同的事。

    幸运的是,Swift有个方法用来弥补,如下:

    打开ViewController.swift 并在viewDidLoad:后面添加代码:
    self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .Refresh,
    target: self, action: “refresh:”)

    这就是我说的target-action模式,target在这是self,这个ViewController的实例。然而,action是一个Swift字符串。如果你看了UIBarButtonItem的初始化定义可能会觉得有些奇怪。
    init(image: UIImage!, style: UIBarButtonItemStyle, target: AnyObject!, action: Selector)

    action的参数应该是一个Selector,但是你传入的是一个String!如果你查看下Selector,你会发现他符合StringLiteralConvertible协议。也就是说他可以直接转换成一个字符串。nice!

    你使用的selctor叫refresh:。他将去查找需要一个参数的叫refresh的方法。这和oc中完全一样,因为Swift方法和oc使用相同的模式,相同的命名参数。

    编译并运行,然后点击屏幕顶部的刷新按钮,你会看到控制台输入错误,如下:
    Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘-[CafeHunter.ViewController refresh:]: unrecognized selector sent to instance 0x7c230a90’

    编译器提示你refresh:在ViewController类中没有实现。Swift中支持的selector本质上依然是动态的,所以在这里编译器无法帮你查找。编译器无法知道任何的oc selectors,所以也无法帮你确认是否有实现该方法。毕竟,oc的特点是可以在运行时来确定是否有实现该方法。

    通过实现refresh来解决运行时错误。在ViewController类的定义后面添加方法:
    func refresh(sender: UIBarButtonItem) { if let location = self.lastLocation {
    self.centerMapOnLocation(location)
    self.fetchCafesAroundLocation(location) } else {
    let alert = UIAlertController(title: “Error”,
    message: “No location yet!”,
    preferredStyle: .Alert)
    alert.addAction(UIAlertAction(title: “OK”,
    style: .Default,
    handler: nil))
    self.presentViewController(alert,animated: true, completion: nil)
    }
    }

    通过UIKit,传入方法的参数是UIBarButtonItem实例。target-action模式通常是通过按钮或控件来触发action。以防你想执行基于它的具体行动。

    map和中心的方法获取cafe馆是否有位置,如果没有,则显示一个error提示位置没有发现。

    编译并运行应用程序,然后,移动地图位置离开当前位置点击refresh按钮。该应用应该返回到用户的当前位置,并重新加载cafe馆。
    这里写图片描述

    Protocols and delegates - 协议和代理

    目前,应用程序只能让你在地图上看到cafe馆。如果点击每个cafe馆都能看到与之相关的信息就更好了。毕竟,你手头上有街道,城市和邮编等信息。你可以通过facebook的api来抓取与cafe馆相关的图片。

    本章接下来的部分会创建一个详细cafe馆的view以及你访问其的路线。你可以自己定义协议来实现他的代理,确保协议能理解oc语句。

    Creating the detail view - 创建一个详细的view

    点击File\New\File… ,选择iOS\Source\Cocoa Touch Class 再点击Next ,生成一个父类是UIViewController 的叫CafeViewController的类。确保选择的语言是Swift,没有启用xib,点击Next然后Create.

    打开CafeViewController.swift 并在类的定义顶部添加:

    @IBOutlet var imageView: UIImageView! 
    @IBOutlet var nameLabel: UILabel! 
    @IBOutlet var streetLabel: UILabel! 
    @IBOutlet var cityLabel: UILabel! 
    @IBOutlet var zipLabel: UILabel! 

    这些将成为你将要填充的内容。再一次,将storyboard与引用元素连接在一起。在类的定义顶部添加代码:

    var cafe: Cafe? { 
        didSet { 
            self.setupWithCafe() 
        } 
    }

    这定义了一个可选类型的cafe。会将cafe馆在当前的ViewController中显示出来。

    这个声明的第二部分设置了一个变量更改时的监听变量有wilSet和didiSet闭包来定义。前者是即将设置一个新的变量值,后者是改变了变量值后的值。

    在这种情况下,当cafe设置了一个新的值后didSet就会被调用。在里面添加方法以便控制器可以在里面设置当前的cafe值。

    在你实现setup方法之前,你需要在Cafe对象中添加一些东西。你想为每个cafe都在界面上显示一个照片,照片url来自facebook抓取的值。

    打开Cafe.swift并在属性定义后面添加代码:

    var pictureURL: NSURL { 
        return NSURL(string: "http://graph.facebook.com/place/picture?id=\(self.fbid)" + 
    "&type=large") !
    }

    这里定义了一个NSURL类型的计算属性。URL用facebookID指向大图。要注意的是因为fbid属性不是可选类型,所以这里一定要有facebookid的值。Swift的语言虽然比较严,但相对的好处便是你无需无时无刻的都担心无处不在的nil值。

    现在来实现控制器的实现方法。打开CafeViewController.swift并在类的定义下面添加代码:

    private func setupWithCafe() { // 1
            if !self.isViewLoaded() {
               return
            }
            // 2
            if let cafe = self.cafe { // 3
                self.title = cafe.name
                self.nameLabel.text = cafe.name 
    
                self.streetLabel.text = cafe.street
                self.cityLabel.text = cafe.city
                self.zipLabel.text = cafe.zip
                // 4
                let request = NSURLRequest(URL: cafe.pictureURL)
                NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue()) {
                    (response: NSURLResponse?, data: NSData?, error: NSError?) -> Void in
                    let image = UIImage(data: data!)
                    self.imageView.image = image
                }
            }
        }

    这个方法做了如下事:
    1.这个方法会使用到IBoutlet属性。回想下他们都是隐式解包的可选类型。他们只有在只访问视图加载后才会有值,所以如果这个方法在这之前被调用的话,如nameLabel,streetLabel等变量还没有建立。你可以直接对每个outlet属性都进行检查,但这个工作量非常大。在这种情况加,直接检查视图是否已经被加载就相对简单很多。

    2.如果没有cafe,那么在界面上没有任何可设置的。只有有cafe信息是才会继续操作。

    3.接下来的几行设置了在界面上的各种标签以及标题。

    4.最后,你使用NSURLConnection加载cafe的图片。

    想象下如果在视图加载完成前设置cafe会发生什么。因为IBOutlet的变量还没有加载所以界面无法正确加载。为了解决这个为题,找到ViewDidLoad并修改成如下:

    override func viewDidLoad() { 
        super.viewDidLoad()
        self.setupWithCafe() 
    } 

    这将在视图加载的时候执行设置。perfect!

    最后,是时候在xb中设计UI了。打开Main.storyboard 并再拖一个控制器在场景scene中。设置storyboardID为CafeView并设置custom class为CafeViewController。然后添加imageView,四个label以及一个button,如下:
    这里写图片描述

    添加自动布局约束都水平居中,imageView的大小是200*200.四个标签由上自下:nameLabel,streetLabe,cityLabel和zipLabel。

    这就是cafe的UI细节了,现在需要做的是讲这些UI连接到代码中。

    Wiring up the detail view - 编写视图的细节

    当前,你当前在地图上显示了大头针pin。为了让用户能够点击大头针,所以你需要在pin的回调中添加一个按钮。当用户点击这个按钮时,将会为cafe展示详细的cafe信息。

    打开ViewController.swift 。找到MKMapViewDelegate 扩展,并在顶部添加代码如下::

    func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {
        if let annotation = annotation as? Cafe {
          let identifier = "pin"
          var view: MKPinAnnotationView
          if let dequeuedView = mapView.dequeueReusableAnnotationViewWithIdentifier(identifier) as? MKPinAnnotationView {
            dequeuedView.annotation = annotation
            view = dequeuedView
          } else {
            view = MKPinAnnotationView(annotation: annotation, reuseIdentifier: identifier)
            view.canShowCallout = true
            view.calloutOffset = CGPoint(x: -5, y: 5)
            view.rightCalloutAccessoryView = UIButton(type: UIButtonType.DetailDisclosure) as UIView
          }
          return view
        }
        return nil
      }

    当地图界面需要显示一个注释标注annotation时这个地图代理会被调用。你需要做的是返回一个提供annotation的初始化的view。

    分解下这里的代码:
    1.你只在这个控制器中处理cafe的注释。其他的,如用户当前位置(蓝色的点)你希望map view自己进行处理。因此你用条件筛选,寻找cafe对象进行注释。

    2.map view保留了重用队列(比如UITableview),所以你不需要每个annotation都创建一个新的。你可以直接使用已经生成好的annotation。如果重用队列中有一个annotation,则你可以直接使用。另外一个确保视图类型的是MKPinAnnotationView。

    3.如果有一个view 队列,那你只需要对那里面的view进行设置。

    4.如果没有可复用的view,你需要创建一个新的MKPinAnnotationView并设置一个按钮作为标注附件。

    5.最后,你返回annotation view。

    编译并运行,点击一个Cafe的附件按钮,界面如下:
    这里写图片描述
    在pin的附件信息上出现了一个按钮,所以当用户点击这个按钮是,你需要处理下。在签名的方法后面添加新的方法:

    func mapView(mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
        if let viewController = self.storyboard!.instantiateViewControllerWithIdentifier("CafeView") as? CafeViewController {
          if let cafe = view.annotation as? Cafe {
        viewController.cafe = cafe
            viewController.delegate = self
            self.presentViewController(viewController, animated: true, completion: nil)
          }
        }
      }

    当用户点击了这个标注按钮是这个方法会调用。分步解析:
    1.通过你早前用storyboardID设置的控制器实例化一个新的CafeViewController。如果失败返回nil,所以你用条件判断来进行解包。

    2.然后,你需要检查从点击的view上返回的annotation是不是Cafe对象。虽然你很清楚,但是编译器不明白因为annotation的属性类型是MKAnnotation。

    3.最后你设置控制器并将其显示出来。

    注意你声明的这个控制器实例作为CafeViewController的代理。你还没有定义这个代理,但是当用户完成了CafeViewController时你需要用他来告诉控制器,即当用户点击了后退按钮。

    打开CafeViewController.swift 并在文件的顶部添加类的定义:

    @objc protocol CafeViewControllerDelegate { 
        optional func cafeViewControllerDidFinish( viewController: CafeViewController) 
    } 

    这定义了一个协议,用于告诉控制器当用户使用完cafe detail view时应该被移除。optional告诉编译器这个方法可能没有被定义,是否执行此方法有执行类来决定。

    如果你想在协议中添加可选类型的方法,则必须要在前面用@objc来声明协议。将协议标记为@objc可以让Swift在运行时进行检查,检查符合协议的地方,检查是否有实现的协议方法等。

    note:你可以限制你的协议只能由类来实现:
    protocol MyClassOnlyProtocol: class { … }

    这意味着只有类可以采用这个协议。没有这个声明的话,结构也可以实现这个协议。添加@objc修饰符当你声明CafeViewControllerDelegate是类协议是可以在运行时检查对象一定是个类。

    现在在CafeViewController类中,IB属性前面添加属性:

    weak var delegate: CafeViewControllerDelegate? 

    这里你声明了一个CafeViewControllerDelegate可选类型的属性叫delegate。你也将其设置为weak,这是代理的标准做法防止循环引用。如果一个对象有一个另外一个对象的强引用,而另外一个对象是代理的话,强引用的代理就会造成两个对象间的循环引用。

    这里使用weak意味着协议只能类实现,因为只有类可以用弱引用来调用他们。记住结构是值类型,声明为弱引用对他没有意义。

    接下来,在CafeViewController类的定义底部添加:

    @IBAction private func back(sender: AnyObject) { 
    self.delegate?.cafeViewControllerDidFinish?(self) 
    } 

    这里的@IBAction意味着你可以从xb文件中拖动一个控件如button的action进行关联。

    这个方法使用了可选链接调用代理方法去告知他已经结束。如果delegate属性是nil,则他什么也不会执行。同样,如果delegate存在但cafeViewControllerDidFinish没有实现,则表达式也不会进行任何操作。可选链接用来处理这类的事得心应手。在Objective-C中,将需要一个单独的语句来见着delegate是否实现了方法。

    现在打开Main.storyboare 并将back按钮关联到此back方法。现在还有最后一件事需要做!回到ViewController.swift并添加代码:

    extension ViewController: CafeViewControllerDelegate {
    
        func cafeViewControllerDidFinish(viewController: CafeViewController) {
            self.dismissViewControllerAnimated(true, completion: nil)
        }
    
    }

    这个扩展实现了CafeViewControllerDelegate的可选方法。当用户完成了操作后就移除掉控制器。

    编译并运行程序,选择一个cafe的pin并点击上面的按钮。你可以看到如下内容:
    这里写图片描述
    你可以通过back按钮返回到地图上。到这里你的工作就完成了。

    Where to go from here? - 接着干什么

    在这一章中,你已经通过应用程序练习了与Cocoa框架间的相互操作。

    首先,你通过整合facebook的sdk学会了与oc之间的互相操作。理解这种桥接是至关重要的,因为现在还有很多代码都还在使用oc,包括Cocoa本身!
    然后,你建立了一个引用使用各种标准的Cocoa功能。你也看到了在Swift中如何使用selectors。最后,你实现了一个代理delegate,和Cocoa一样的模式。

    用你新了解的知识,使用Swift和Cocoa的混编来开发应用程序吧!

    展开全文
  • 更新,,,今天,学习了view中的MapKit,实现了基本的地图工具调用。 Functions 功能 pickerView 实现滚动选择地点 每一个pickerView中的item有一个对应的经纬坐标 选到一个地点,上方的地图即切换到相应的位置 ...
  • 人們常說,程序員能將咖啡變成電腦... Swift、Xcode 和 Interface Builder (自動佈局、約束和故事板) Realm,一個輕量級的 Core Data 封裝,用於本地數據庫。 Foursquare,用 “Das Quadrat” 庫來訪問 Foursquare 的
  • 1.xib 上的 3D效果 按钮 2. import UIKit ...import MapKit class ViewController: UIViewController { @IBOutlet weak var mapView: MKMapView! override func touchesBegan(...
  • Swift 语言指南

    2015-07-03 10:09:06
    这份指南汇集了 Swift 语言主流学习资源,并以开发者的视角整理编排。 GitHub:ipader/SwiftGuide | 网站:http://dev.swiftguide.cn | 欢迎开发者一起维护,或反馈/投稿 想了解关于该指南及 Swift 更多信息...
  • IOS 9的更新和Swift 2

    2015-06-17 23:10:23
    本文意在帮助英语不好的同学(比如我)更快地了解IOS 9都更新了些什么,文中一些链接都是官网的API或Demo。如果发现本文有任何错误或者不通顺的地方,请留言告诉我,谢谢!Start Developing for iOS 9iOS 9 SDK包含...
  • 前言:关于地理位置及定位系统,在iOS开发中也比较常见,比如美团外面的餐饮店铺的搜索,它首先需要用户当前手机的位置,然后在这个位置附近搜索相关的餐饮店铺的位置,并提供相关的餐饮信息,再比如最常见的就是...
  • 老规矩先上效果图(北京的朝阳区): 此图是从... 先描述一下情况: 这几天正好做实验室的一个小项目app(也就是仅供学习那种。。。...
  • 本教程使用Swift 3.1, Xcode 8.0 代码:https://github.com/jamesdouble/JDSwiftHeatMap 现在Iphone使用者常使用的地图插件,不外乎就是高德与百度,国外则是Google,看来看去就是没啥人在用本地端自带的MKMapView,...
  • IOS开发百度地图API-用点生成路线,导航,气泡响应 (2013-03-21 18:57:11) 转载▼ 标签: ios百度地图开发 ios百度地图sdk 百度地图api开发 百度地图ios版开发 ios开发百度地图api ...IO
  • 原文:Augmented Reality ...更新说明:本教程由 Jean-Pierre Distler 升级至 Swift3 和 iOS 10。增强现实是一种很酷的流行技术,你可以通过特定设备(比如iPhone 摄像头或者微软的 Hololens)来观察世界,这个设备会
  • 相关链接: IOS7开发~Xcode5制作framework 使用静态链接库(Xcode4.6.2) ...在实际的编程过程中,通常会把一些公用函数制成函数库,供其它程序使用,一则提搞了代码的复用;...所以在实际的项目开发中
1 2 3 4 5 ... 10
收藏数 190
精华内容 76
热门标签