第八章-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的混編來開發應用程序吧!

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