第八章-Swift and 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的混编来开发应用程序吧!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章