Swift是一门新推出的语言,但是核心框架还是Cocoa,这与OC是一致的,Cocoa的Foundation和UIKit框架对于开发应用仍是最重要的。这一章将创建一个应用,主要介绍Swift一Cocoa直接的交互,同时了解Cocoa的设计模式如何在Swift中体现出来。
Getting started
作者提供了一个starter project,添加了一个viewcontroller和之前介绍的JSON.swift(帮助解析JSON的工具类),另外还有facebook的SDK,所以接下来会介绍如何在Swift中混用OC代码。
Bridging Swift and Objectivec-C
通过bridging 技术,可以让我们在Swift和OC间相互调用。
Swift bridging header
新建一个OC的.h文件,如:CafeHunter-ObjCBridging.h,然后在Build Settings 中找到Objective-C Bridging Header ,填写该头文件的路径,如:CafeHunter/CafeHunter-ObjCBridging.h,然后可以在该头文件中添加所需的OC头文件,如下,这样就可以在Swift中使用OC的类和方法了。
1
#import <FacebookSDK/FacebookSDK.h>
1
FBSettings . setDefaultAppID ( "INSERT_YOUR_FB_APP_ID" )
Objective-C compatibility header
那么如何在OC中使用Swift代码呢?还记得刚才在Build Settings 中找到的Objective-C Bridging Header 上面的Install Objective-C Compatibility Header 吗?该项就是控制Swift向OC转换的开关,默认是打开的。
在report navigation的Build,中可以找到 Copy CafeHunter-Swift.h 的信息,可以双击打开该文件,你会发现你用Swift中创建的继承于OC的类和方法都在这里有对应的OC代码,而且实际上还有用@objec标记的代码,也有对应的转化。需要注意的两点是:
Swift中的私有方法是没有转化的,因为理论上外部不可能使用私有方法,但私有方法仍会注册在runtime中。
转化的代码每个类之前会有一行类似SWIFT_CLASS(“_TtC10CafeHunter11AppDelegate”) 的代码,实际上是Swift的压缩命名,为每个类添加了命名空间,也是该类在runtime中实际的名字,即使不同库中有相同名类,编译器也会通过该类名,准确找到对应的类。
Adding the UI
添加一个FaceBook的登录View和MKMapView到Storybiard,然后关联到代码,如下。
1
2
@ IBOutlet weak var mapView: MKMapView !
@ IBOutlet weak var loginView: FBLoginView !
有几点说明:
outlet的类型是optional的,而且是隐式拆解的,这是因为如果不这么设置,编译器会发现这些变量没有在初始化中赋值,从而报错,所以这是为了避免编译器报错的手段,但我们知道它会在IB中初始化,所以采用了隐式拆解。使用outlet时不用再去拆解,但同时需要注意在view加载之前调用它们会导致崩溃,这点需要谨记。
outlet被加了weak关键字,这和OC中是一致的,是因为viewController的view对outlet是有强引用的,所以不必再添加额外的强引用。
Showing the user’s location
在Swift中长把协议的实现放在单独的extension中,这样可以将代码分组,但依然可以访问原类的变量和方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
extension ViewController: MKMapViewDelegate {
func mapView ( mapView: MKMapView , didFailToLocateUserWithError error: NSError ) {
print ( error )
let alert = UIAlertController ( title: "Error" , message: "Failed to obtain location!" , preferredStyle: . Alert )
alert . addAction ( UIAlertAction ( title: "OK" , style: . Default , handler: nil ))
self . presentViewController ( alert , animated: true , completion: nil )
}
func mapView ( mapView: MKMapView , didUpdateUserLocation userLocation: MKUserLocation ) {
let newLocation = userLocation . location !
let distance = self . lastLoction ? . distanceFromLocation ( newLocation )
if distance == nil || distance > searchDistance {
self . lastLoction = newLocation
self . centerMapOnLocation ( newLocation )
self . fetchCafesAroundLocation ( newLocation )
}
}
}
本节其他部分都是一些业务逻辑方面的内容,数不赘述。
Fetching data
前面的开发进行到了最后一步,就是将用户位置附近的咖啡馆找出来,并在地图上展示,这依赖于一个FaceBook的接口访问,同时本地需要定义咖啡馆的模型。
Building the data model
先定义Cafe的model,首先你会想到Cafe在这里是一个纯数据类型,你可能会使用struct,因为Swift中struct也是model的选择之一。
但同时,你希望Cafe可以直接被显示到地图上,那么它就必须遵循MKAnnotation协议,这时,编译器便会报错,因为MKAnnotation是OC的协议,Cafe遵循该协议,必须可以被转化为OC代码,但struct在OC中只是一个C的数据类型,无法转化,所以这里必须声明Cafe为class,且必须继承自NSObject,因为MKAnnotation也遵循NSObject协议。最后,别忘了在init()结尾,添加super.init() ,这样才能调到NSObject的初始化方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import Foundation
import MapKit
class Cafe: NSObject {
let fbid: String
let name: String
let location: CLLocationCoordinate2D
let street: String
let city: String
let 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
super . init ()
}
}
extension Cafe: MKAnnotation {
var title: String ? {
return name
}
var coordinate: CLLocationCoordinate2D {
return location
}
}
Fetching from Facebook
下面将从Facebook的接口获取数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
var urlString = "https://graph.facebook.com/v2.0/search/"
urlString += "?access_token="
urlString += "\(FBSession.activeSession().accessTokenData.accessToken)"
urlString += "&type=place"
urlString += "&q=cafe"
urlString += "¢er=\(location.coordinate.latitude),"
urlString += "\(location.coordinate.longitude)"
urlString += "&distance=\(Int(searchDistance))"
let url = NSURL ( string: urlString ) !
print ( "Request URL: \(url)" )
let request = NSURLRequest ( URL: url )
NSURLConnection . sendAsynchronousRequest ( request , queue: NSOperationQueue . mainQueue ()) { ( response: NSURLResponse ? , data: NSData ? , error: NSError ? ) -> Void in
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
}
let jsonObject: AnyObject ! = try ? NSJSONSerialization . JSONObjectWithData ( data ! , options: NSJSONReadingOptions ( rawValue: 0 ))
if let jsonObject = jsonObject as ? [ String: AnyObject ] {
if let data = JSONValue . fromObject ( jsonObject ) ? [ "data" ] ? . array {
var cafes: [ Cafe ] = []
for cafeJSON in data {
if let cafeJSON = cafeJSON . object {
if let cafe = Cafe . fromJSON ( cafeJSON ) {
cafes . append ( cafe )
}
}
}
self . mapView . removeAnnotations ( self . cafes )
self . cafes = cafes
self . mapView . addAnnotations ( cafes )
}
}
}
首先拼接要访问的URL,采用了String拼接,而不是stringWithFormat:,这样代码比较清晰
然后将string转化为NSURL,虽然NSURL的参数需要NSString,但是使用String也没问题,这是因为Swift对它们进行了无缝的桥接。
使用了NSURLConnection的异步请求,使用closure处理回调。
JSON的反序列化,目前Swift2.0采用了try/catch这样的写法,而弃用了之前OC的传入NSError**参数的做法,代码更简洁。
NSJSONSerialization的JSONObjectWithData()方法实际返回的是NSDictionary,但是我们使用时是转化成[NSObject:AnyObject],这也是Swift的隐式转换,还有NSArray和[AnyObject]。
具体的JSON对象的解析,使用了JSON.swift库,关于这个库的原理在之前讲过,主要是利用enum的新特性,为每一种JSON元素类型提供了type。
Parsing the JSON data
让我们在Cafe类中添加一个直接从JSON初始化的方法,在取JSON每一个元素时,一定要注意使用optional类型,这样可以保证不会因为某个值不存在而崩溃。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class func fromJSON ( json: [ String: JSONValue ]) -> Cafe ? {
let fbid = json [ "id" ] ? . string
let name = json [ "name" ] ? . string
let latitude = json [ "location" ] ? [ "latitude" ] ? . double
let longitude = json [ "location" ] ? [ "longitude" ] ? . double
if fbid != nil && name != nil && latitude != nil && longitude != nil {
var street: String
if let maybeStreet = json [ "location" ] ? [ "street" ] ? . string {
street = maybeStreet
} else {
street = ""
}
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 = ""
}
let location = CLLocationCoordinate2D ( latitude: latitude ! , longitude: longitude ! )
return Cafe ( fbid: fbid ! , name: name ! , location: location , street: street , city: city , zip: zip )
}
return nil
}
从OC的角度考虑,我们可能会问,为什么不写一个secondary初始化方法?这是由于Swift决定的,某个类初始化方法是不能返回nil值的,如果可以,那么所有的Swift类型都是optional类型了,那么区分optional就没有意义,所以我们一定要注意,像构建这类可能返回nil的初始化方法,最好采用该类型的工厂方法来实现。
Selectors
这一小节想给app加一个刷新按钮,引出OC中的target/selector模式在Swift中如何使用的问题。
OC中的方法调用是动态分发的,即方法的调用者(target)和方法名(SEL)都是可以在runtime动态分发的,而且你可以在runtime中修改这些值,而编译器不会在意该方法有没有实现。而在Swift中,所有的方法调用在编译期间就会决定,不再采用动态分发。那么如何解决Swift中调用原OC中需要target/SEL的方法,就需要得到解决。
Swift给出的方案是使用一个结构体Selector,Selector遵循了StringLiteralConvertible协议,其内部使用时可以直接从String转换,而使用者只需传入一个String即可。
1
self . navigationItem . leftBarButtonItem = UIBarButtonItem ( barButtonSystemItem: . Refresh , target: self , action: "refresh:" )
1
public convenience init ( barButtonSystemItem systemItem: UIBarButtonSystemItem , target: AnyObject ? , action: Selector )
你可以尝试不去实现refresh:方法,而编译器也不会报错,而在执行时才会报错,这就是因为它的真正实现还是使用了OC的动态分发,只是做了Swift的桥接。
Protocols and delegates
接下来会为每个cafe创建一个detail view,并定义一个protocol,使用委托模式,并确保可以桥接到OC。
Creating the detail view
创建对应的viewController,并在storyboard上定义好xib。
这里声明了一个Cafe变量作为数据源,如下,与以往不同的是,添加了didSet()方法,该方法会在cafe被赋值之后会调用,类似OC中的KVO,你可以为实现变量的didSet和willSet,分别在赋值前和复制后调用。
1
2
3
4
5
var cafe: Cafe ? {
didSet {
self . setupWithCafe ()
}
}
在Cafe类添加一个pictureURL变量,是一个computed property,提供了返回值。
1
2
3
var pictureURL: NSURL {
return NSURL ( string: "http://graph.facebook.com/place/picture?id=\(self.fbid)&type=large" ) !
}
接下来是setupWithCafe()方法,由于该方法放在了cafe的didSet中调用了,而且存在很多outlet存在,需要判断这些outlet是否加载完成,所以先进行了self.isViewLoaded()判断。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private func setupWithCafe () {
if ! self . isViewLoaded () {
return
}
if let cafe = self . cafe {
self . title = cafe . name
self . nameLabel . text = cafe . name
self . streetLabel . text = cafe . street
self . cityLabel . text = cafe . city
self . zipLabel . text = cafe . zip
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
}
}
}
Wiring up the detail view
在ViewController中,通过实现mapView(mapView: MKMapView!, viewForAnnotation annotation: MKAnnotation!) –> MKAnnotationView!和mapView(mapView: MKMapView!, annotationView view: MKAnnotationView!, calloutAccessoryControlTapped control: UIControl!)两个MKMapViewDelegate的方法,完成了通过点击大头针,弹出详情按钮,再进入详情页的功能。
这里还想将ViewController作为CafeViewController的委托,在CafeViewController点击返回时调用。
1
2
3
@ objc protocol CafeViewControllerDelegate {
optional func cafeViewControllerDidFinish ( viewController: CafeViewController )
}
这里在声明protocol前加了@objc 关键字是因为该protocol有optional方法,因为这样可以使Swift添加多个runtime检测,来检测protocol的一致性和是否实现了optional类型的方法。同时这也限制了,protocol的实现者必须是class类型,因为runtime对@obj的检测需要对象为class。而你也可以通过在protocol后加限制来实现。
1
2
3
@ objc protocol CafeViewControllerDelegate: class {
optional func cafeViewControllerDidFinish ( viewController: CafeViewController )
}
然后在CafeViewController添加delegate变量,与OC一样,使用weak属性,需要注意的是声明为optional类型,因为不是一定有委托对象的。
1
weak var delegate: CafeViewControllerDelegate ?
然后是delegate的调用,这里使用了optional chain,delegate的存在与cafeViewControllerDidFinish()方法的实现与否都是不确定的。
1
2
3
@ IBAction private func back ( sender: AnyObject ) {
self . delegate ? . cafeViewControllerDidFinish ? ( self )
}
最后,是在ViewController中的delegate实现。
1
2
3
4
5
extension ViewController: CafeViewControllerDelegate {
func cafeViewControllerDidFinish ( viewController: CafeViewController ) {
self . dismissViewControllerAnimated ( true , completion: nil )
}
}