Swift 程式語言

使用多點連線 (MPC )框架與 Swift 打造聊天 App

使用多點連線 (MPC )框架與 Swift 打造聊天 App
使用多點連線 (MPC )框架與 Swift 打造聊天 App
In: Swift 程式語言

你可能會好奇為何我要帶來這個有點舊的主題,而不是探討 iOS 8 所導入的新功能。因為我有下列 3 個理由:

  1. 許多讀者寫信給我,詢問如何透過之前文章提到過的多點連線來實現各式各樣的任務。在回覆這些信件的時候,我發現自己老早就注意到有這種需求的存在;人們希望可以更了解多點連線,但是卻始終找不到資料。
  2. 在前面幾篇文章當中,我的實作是基於使用 iOS SDK 中既有的預設視圖控制器來邀請其他同伴並且建立連線。我發現人們傾向親手實作這項功能,這也正是我撰寫本文的目的。
  3. 我認為使用 Swift 來實作 MPC 功能非常具有實用性與教育意義。

Swift 的多點連線框架

多點連線框架的存在,已經為廣大的開發者證明了誕生新點子的可能性。透過如此簡單的方式來連結裝置,本身具有非常大的吸引力,而這正是程式設計師想要將多點連線框架整合到他們自己的 App 當中的原因。但是如果你從未使用過 MPC 框架,我必須先提出警告: MPC 有時候並不像你所想的那麼可靠,這是我過去在做專案時發現的,也曾經有其他開發者跟我反映過。 MPC 同時使用藍牙( Bluetooth )和 WiFi 來連結附近( Nearby )的裝置,儘管聽起來很不錯而且大有可為,但是偶爾連線會失敗或者太慢,導致通訊發生錯誤。在傳輸極為重要的資料時,這點顯得非常關鍵。我會建議使用備援通訊方案(例如 Web 服務),擁有替代方案,可以讓你的 App 在 MPC 失效的情況下依然能夠運作。儘管如此,我依然深信 MPC 對於所有 iOS 開發者而言都是一項很棒的工具,值得我們寫篇文章來好好著墨一番。

我並不打算詳述 MPC 的細節。假使你只是想嘗試看看,不妨閱讀這篇文章。相反地,本文將對多點連線框架進行全面性的簡短概述。

MPC 包含了 4 個主要類別,其概念分述如下:

  1. 同伴( Peer )MCPeerID 類別):同伴實際上就是一台裝置,只是換個程式設計上的講法罷了。同伴往往是第一件需要被設定的事,因為此類別的物件會被當成下一個類別實體的初始化參數。此外,同伴包含了名為 displayName 的重要參數。這是附近同伴彼此互相顯示的裝置名稱。
  2. 工作階段( Session )MCSession 類別):這是 2 個同伴之間所建立的連線(在第一台裝置邀請了第二台裝置,而第二台裝置也接受了邀請之後)。工作階段是 2 台裝置之間的事情。第 3 台裝置無法連結到既有的工作階段來取代正在使用中的裝置。
  3. 瀏覽者( Browser )MCNearbyServiceBrowser 類別):此類別的函式是用來尋找附近的其他裝置,並邀請這些裝置加入工作階段。先決條件是,必須由那些想要配對的裝置自行發佈( Advertise )。在本文中,我們將使用此類別來手動邀請其他裝置。
  4. 發佈者( Advertiser )MCNearbyServiceAdvertiser 類別):此類別負責從裝置發佈公告(亦即讓該裝置可以或不可以被其他裝置看見),此外也用來接受或拒絕來自其他同伴的連線邀請。

多點連線框架的邏輯非常簡單:裝置(同伴)使用其瀏覽者來尋找周遭的其他裝置。如前所述,裝置必須自行發佈公告,讓本身變成可被發現。一旦發現了同伴,即可送出邀請以便建立工作階段。邀請會在找到附近的同伴時立即自動送出,或者由使用者自行決定是否送出。事實上,這完全視想要打造的 App 的性質而定,沒有標準答案。你可以自己決定何時送出邀請。任何時候,一旦接受了邀請,工作階段就會建立,隨後 2 台裝置便可以傳送與接收資料、資源或串流 。

本文的目標是展示如何透過程式來瀏覽、邀請和連結其他同伴。請留意稍後即將看到的文章內容,那只是實作方式之一。毫無疑問地,你當然能夠隨心所欲地使用 MPC 所提供的任何工具,並且打造成符合本身需求的任何 App 。本文的範例所講述的是如何從無到有完全透過程式而不假手任何預先建構好的視圖控制器來實作 MPC 框架。希望在閱讀完本文之後,大部分讀者都可以找到自己所尋找的解答。

最後,假使你從未使用過 MPC 框架,我建議你可以快速瀏覽一下 Apple 的官方文件以及我之前寫過的文章,它們有助於你在往下閱讀本文之前先擁有一些概念。現在就讓我們開始深入探索多點連線的世界吧。

關於範例 App

首先讓我們討論一下本文的範例 App ,我們即將打造的是聊天 App。好啦,我知道這樣的情境可能太過平凡,而且我們在之前關於多點連線的文章中就已經建立過聊天 App 了,但是請容我聲明一下:無論我多麼努力想要找尋更好的範例,每當我想要討論程式設計的時候,聊天 App 一直都是我名單上的首選。我相信你在看完本文之後也會同意這點,所以就讓我們開始正題吧。

如同先前那幾篇文章,我們並不打算從無到有建構整個範例 App 。相反地,我將提供一個 Starter 專案,請從這裡下載,我們將在後續篇幅中持續使用此專案。此 App 將取名為 MPCRevisited ,我認為這是最合適的名稱。在下載後請先自行稍微瀏覽此專案,這樣你會對它比較熟悉,後續在操作時也會比較快速。

如前所述,我們要打造的是聊天 App 。此 App 將會區分成 2 個視圖控制器,其中第 2 個將會強制顯示出來讓使用者看見。如果你檢視本專案的 Interface Builder ,那麼將會看見在第 1 個視圖控制器的場景( Scene )中有個表格視圖。在此表格視圖中,我們將會顯示所有透過多點連線框架所發現的附近裝置,這份同伴清單將會動態更新,因為現有的同伴可能會離開,而附近也有可能來了新的同伴。在第 1 個視圖控制器中,還會看到擁有單顆按鈕的工具列。稍後,我們將使用此按鈕來開啟和關閉裝置的探索功能,以便理解 MPC 發佈功能的運作方式。

在點擊了清單上的同伴之後,我們會詢問另一方的使用者是否願意聊天。假使他拒絕的話,則不會再有任何新的動作。但是如果接受的話,那麼名為 ChatViewController 的第 2 個視圖控制器將會顯示在螢幕上,讓 2 台裝置可以互相交換文字訊息。在第 2 個視圖控制器的場景中,有一個用來撰寫訊息的文字欄位,以及一個用來列出所有訊息的表格視圖。除此之外,上方還有一個工具列,包含一顆用來結束聊天的按鈕。我相信無須在此多加贅述,因為所有細節在後續的實作當中都將完整看見。

在提供幾張關於最終 App 的螢幕截圖之前,容我再講最後一件事。儘管我們可以透過多點連線來連接最多 7 台裝置,但是在我們的範例 App 中只會連接 1 台,也就是說我們每次只會跟 1 個同伴聊天。請容我說得更清楚一些,使用工作階段將多個同伴連接在一起,這件事完全截然不同於發現並列出附近的所有同伴,而我現在講的正是第 1 種情況。無論如何,從下一個小節開始,你將會逐步看見每個細節,現在只是讓你感受一下範例 App 的成果而已:

t27_2_ask_chat_alertt27_3_chatting

自訂的類別

知道了本專案的用途之後,讓我們捲起袖子親手實作吧,就從建立自訂的類別開始。此類別將用來處理多點連線,我們將會實作能夠讓 MPC 框架正確運作的所有必要函式。此外,在本小節結束時,你將會看到我們建立了新的協定( Protocol ),以便使用「委派設計模式」( Delegation Pattern ),並且經由此方式將訊息傳回給呼叫端類別。

首先,在 Xcode 中,按下鍵盤上的 Cmd-N 按鍵組合。在出現的指引視窗中,選取建立新的 Cocoa Touch Class ,如下圖所示:

t27_4_class_template_1

接著,讓新的類別繼承自 NSObject 類別,並命名為 MPCManager

t27_5_class_template_2

跟隨指引視窗的提示來完成後續的步驟,確認稍後要使用的檔案是 MPCManager.swift

因為我們要在此類別程式碼的第一行匯入多點連線框架。所以請來到此檔案的開頭,並且加入下列這行:

import MultipeerConnectivity

現在,宣告一些將會使用到的 MPC 框架類別的物件。在此類別開頭處加入下列這幾行:

var session: MCSession!

var peer: MCPeerID!

var browser: MCNearbyServiceBrowser!

var advertiser: MCNearbyServiceAdvertiser!

除了上述這些之外,我們還需要宣告 2 個稍後將會使用到的變數:

var foundPeers = [MCPeerID]()

var invitationHandler: ((Bool, MCSession!)->Void)!

foundPeers 陣列中,我們將會存放所有由裝置的瀏覽者所發現的同伴。請留意,目前還不會與這些同伴建立連線,我們只是先了解裝置找到了哪些同伴而已。此外,此陣列在宣告時也一併進行了初始化,所以不需要設定為 nil 。我們希望此陣列隨時準備好可以在發現新的同伴時立刻填入新的物件。

invitationHandler 實際上是一個完成處理常式( Completion Handler )的宣告,但是我們現在還沒有要討論這個部分。稍後在使用到時我們才會有更多的著墨。

下一步是確定我們的自訂類別有遵循特定的 MPC 協定。這些協定的委派函式讓我們能夠處理多點連線以及瀏覽者、發佈者和工作階段。那麼現在就來修改此類別的表頭那行,以便加入這些協定吧,示範如下:

class MPCManager: NSObject, MCSessionDelegate, MCNearbyServiceBrowserDelegate, MCNearbyServiceAdvertiserDelegate

我想應該不必詳述個別協定的用途吧。

現在,讓我們來建立初始化常式,並且初始化所有的 MPC 物件。讓我們逐一檢視,從 peer 物件開始。此物件代表的是要讓附近所有裝置看見的裝置本身,根據其初始化條件,需要帶入顯示名稱。其他同伴將會看見此顯示名稱,它可以是任何你想要的字串。為了方便起見,我們在此使用裝置的名稱來充當同伴的顯示名稱,但是我並不建議在真實的 App 當中這麼做。或許你應該讓使用者輸入他們希望的名稱,或者透過其他方法來指定同伴的名稱。初始化程式碼如下所示:

override init() {
    super.init()

    peer = MCPeerID(displayName: UIDevice.currentDevice().name)
}

我們透過 UIDevice 類別取得了裝置的名稱。

在初始化完同伴物件之後,緊接著將繼續處理其他的物件。請留意,同伴必須最先被初始化,因為所有其他物件都將以此作為初始化的參數。讓我們來看 session 物件:

override init() {
    ...

    session = MCSession(peer: peer)
    session.delegate = self
}

如你所見,工作階段物件在初始化時只有一個參數,也就是先前指定的同伴。除了初始化之外,我們也將類別本身變成 session 物件的委派。

接著輪到 browser 物件:

override init() {
    ...

    browser = MCNearbyServiceBrowser(peer: peer, serviceType: "appcoda-mpc")
    browser.delegate = self
}

此物件的初始化常式需要 2 個參數:第 1 個是同伴。而第 2 個則是在初始化之後便無法變更的數值,與瀏覽者所要瀏覽的服務類型有關。簡單地說,此數值用來唯一識別每個 App ,好讓 MPC 知道要搜尋的對象,而發佈者也必須設定為相同的服務類型數值(稍後即將看到)。在設定此數值時,務必遵守 2 條規則:(a)長度不可以超過 15 個字元;(b)只能夠包含小寫 ASCII 字元、數字和減號。假使沒有遵循這些規則,在執行階段將會丟擲異常並導致 App 當機。

下列是關於 advertiser 的部分:

override init() {
    ...

    advertiser = MCNearbyServiceAdvertiser(peer: peer, discoveryInfo: nil, serviceType: "appcoda-mpc")
    advertiser.delegate = self
}

請留意,此處我們設定了與之前相同的服務類型數值。此處還有額外的參數 discoveryInfo ,這是可以包含任何你想要在探索發現期間傳遞給其他同伴的字典。請留意,此字典的索引鍵和數值都必須是字串。為了方便起見,我們將此參數設定為 nil 。

init 函式已經準備好了,程式碼如下所示:

override init() {
    super.init()

    peer = MCPeerID(displayName: UIDevice.currentDevice().name)

    session = MCSession(peer: peer)
    session.delegate = self

    browser = MCNearbyServiceBrowser(peer: peer, serviceType: "appcoda-mpc")
    browser.delegate = self

    advertiser = MCNearbyServiceAdvertiser(peer: peer, discoveryInfo: nil, serviceType: "appcoda-mpc")
    advertiser.delegate = self
}

在本小節結束之前,讓我們趕緊來建立新的協定,以便實現委派設計模式。請留意,我們將會預先宣告好在開發本 App 期間需要用到的所有委派函式。因為我們不想數度因為處理協定的事情來打擾各位讀者,而且我們希望未來有需要時可以立即使用那些函式。

來到類別定義,加入下列的程式碼區塊:

protocol MPCManagerDelegate {
    func foundPeer()

    func lostPeer()

    func invitationWasReceived(fromPeer: String)

    func connectedWithPeer(peerID: MCPeerID)
}

我並不打算討論上述個別函式的功能。我們會在後續小節中探討它們的實作細節。

最後,在 MPCManager 類別中宣告一個委派物件:

var delegate: MPCManagerDelegate?

到這裡為止,我們已經成功抵達此專案的第 1 個重要查核點,現在可以稍微喘口氣了。在本小節結束之前,請容我這麼說,任何你在 Xcode 當中看到的錯誤都是正常的,在我們實作完 MPC 的所有委派函式之後,這些錯誤都將消失無蹤。

瀏覽附近的同伴

MCNearbyServiceBrowserDelegate 協定包含了 3 個函式,可以讓我們處理找到的和遺失的同伴,以及處理在瀏覽期間所遭遇的任何錯誤。我們將實作這些函式,以便繼續開發範例 App ,不過它們都非常簡單,因為要做的事情並不多。

讓我們從第 1 個函式開始,此函式會在發現附近的同伴時(換言之,找到其他裝置時)被 MPC 呼叫到。我先提供實作,稍後再來進行討論:

func browser(browser: MCNearbyServiceBrowser!, foundPeer peerID: MCPeerID!, withDiscoveryInfo info: [NSObject : AnyObject]!) {
    foundPeers.append(peerID)

    delegate?.foundPeer()
}

我們要做的第一件事,同時也是最重要的事,就是將找到的同伴加入到 foundPeers 陣列中(我們在稍早宣告過此陣列,還記得嗎?)。稍後,我們將在 ViewController 類別中利用此陣列來當作表格視圖的資料來源,而此表格視圖將用來列出找到的所有同伴。為了實現此任務,我們將會呼叫 MPCManagerDelegate 協定的 foundPeer 委派函式。此委派函式將會在 ViewController 類別中實作(下一個小節將會說明),我們將在那裡重新載入表格視圖的資料,好讓新找到的同伴能夠顯示給使用者看見。

現在我們已經處理完找到同伴的情況了,我們還必須處理完全相反的情況;亦即考慮無法再存取(無法被找到)的同伴。基於這個理由,我們實作了下一個委派函式:

func browser(browser: MCNearbyServiceBrowser!, lostPeer peerID: MCPeerID!) {
    for (index, aPeer) in enumerate(foundPeers){
        if aPeer == peerID {
            foundPeers.removeAtIndex(index)
            break
        }
    }

    delegate?.lostPeer()
}

無須贅言,我想程式碼本身就是最好的說明。首先我們在 foundPeers 陣列中找到消失的同伴,接著將之移除。在完成此動作而且同伴已經不存在於我們的「清單」中之後,我們必須通知 ViewController 類別,以便更新表格視圖中所顯示的同伴。為此,我們呼叫了 lostPeer 委派函式,稍後在實作時我們將會重新載入表格視圖的資料。

最後,我們應該還要再實作一個委派函式,用來管理任何可能遭遇的錯誤以及處理無法執行的瀏覽動作。顯然我們並不打算在本文中處理任何嚴重的錯誤,我們只會顯示錯誤訊息而已。程式碼如下:

func browser(browser: MCNearbyServiceBrowser!, didNotStartBrowsingForPeers error: NSError!) {
    println(error.localizedDescription)
}

在本小節結束之前,摘要重點如下:相較於使用上述的委派函式來通知 ViewController 類別關於同伴所發生的變化,我們可以使用索引鍵-數值編碼( Key-Value Coding, KVC )和索引鍵-數值觀察( Key-Value Observing, KVO )機制來追蹤 foundPeers 陣列的變化。但是我們並沒有這麼做,原因在於有了 MPCManagerDelegate 協定就不需要撰寫額外的程式碼了。假使除了上述的 2 個函式之外並不需要再實作更多的函式,那麼我們可以使用 KVC 和 KVO 來取代委派設計模式。所以我們並沒有使用 KVC 和 KVO ,因為在本例中,委派函式的實作內容相對而言比較快速和簡單。

在完成上述的實作之後,範例 App 便具備了瀏覽其他同伴的能力。現在,讓我們繼續加入必要的程式碼,以便在發現同伴時予以顯示出來。

顯示找到的同伴

現在我們的範例 App 可以找到附近的同伴,並將之加入(或移除)到 foundPeers 陣列中,讓我們回到 ViewController 類別,並且在現有的表格視圖中顯示這些同伴吧。不過在此之前,我們將會先造訪 AppDelegate 類別,並且在那裡宣告 MPCManager 物件。這麼做的話,在其他的類別當中,我們將能夠透過應用程式委派實體來存取此物件。

在 Project Navigator 中點擊 AppDelegate.swift 檔案以便開啟。在類別的開頭,加入下列這行:

var mpcManager: MPCManager!

接著來到 application(application:didFinishLaunchingWithOptions:) 函式,進行初始化作業:

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    // Override point for customization after application launch.

    mpcManager = MPCManager()

    return true
}

現在,我們可以開啟 ViewController.swift 檔案,並且針對其內容進行修改。假使你稍微看過程式碼的話,將會發現已經具備了最低限度的實作內容。不過顯然我們將會再新增一些程式碼,也會修改與表格視圖有關的函式,好讓所有功能都能夠正確運作。就從在類別開頭位置同時宣告和實體化應用程式委派物件來作為起點吧:

let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate

在宣告時針對物件進行實體化或者初始化,是 Swift 一項很棒的功能,因為需要撰寫的程式碼比較少,而且開發流程也變得比較快速。下一步是將 ViewController 設定為 MPCManager 類別的委派,因此在 viewDidLoad 函式中加入下列這行程式碼:

override func viewDidLoad() {
    ...

    appDelegate.mpcManager.delegate = self

}

針對這行, Xcode 現在會出現錯誤,理由很簡單:我們尚未實作 MPCManagerDelegate 協定。所以請來到類別的表頭列,修改成如下所示:

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, MPCManagerDelegate

接著開始實作在前面小節中看過的 2 個委派函式( foundPeerlostPeer )。它們都是用來更新表格視圖,所以實作上非常單純:

func foundPeer() {
    tblPeers.reloadData()
}


func lostPeer() {
    tblPeers.reloadData()
}

對於我們想要實作的 App 而言,上述步驟非常重要,不過現在還看不出效果,除非我們「告訴」表格視圖它的資料來源,以及(當然了)除非我們做了適當的修改來讓表格視圖顯示附近同伴的名稱。那麼,就讓我們從資料列總數開始吧,這個部分只會有一列。資料列的數目顯然會跟找到的同伴數目一致:

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return appDelegate.mpcManager.foundPeers.count
}

現在,該是顯示個別同伴名稱的時刻了:

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    var cell = tableView.dequeueReusableCellWithIdentifier("idCellPeer") as UITableViewCell

    cell.textLabel?.text = appDelegate.mpcManager.foundPeers[indexPath.row].displayName

    return cell
}

請留意,在上述的程式碼中,我們存取了 foundPeers 陣列中每個同伴的顯示名稱。

最後,設定每列的高度:

func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    return 60.0
}

在做完這些工作之後,表格視圖便可以很完美地運作。

即便我們在此完成了很重要的工作,不過還有一件最後也最關鍵的任務要做:在預設的情況下,瀏覽功能是關閉的,裝置將不會瀏覽其他同伴,除非我們指示它這麼做。任何時候都可以啟動瀏覽者,完全視 App 的需求而定。在本例中,我們將在 App 啟動之後立刻啟用瀏覽者,所以來到 viewDidLoad 函式,並加入下列這行:

override func viewDidLoad() {
    ...

    appDelegate.mpcManager.browser.startBrowsingForPeers()
}

若要完成相反的工作,亦即停止瀏覽者,可以呼叫瀏覽者的 stopBrowsingForPeers() 函式。

所有與瀏覽者有關的部分都已經準備就緒,現在可以將重心移往裝置的發佈功能了。

處理發佈

除了瀏覽之外,多點連線框架還可以讓裝置向附近同伴發佈本身的資訊。缺少了發佈,瀏覽就一點用處也沒有了。事實上,它們是彼此互補,而且同等重要的。在談論發佈時,就像是在說其他同伴是否看得見某台裝置。假使某個 App 啟用了發佈功能,那麼附近的其他同伴便看得見該裝置。否則的話,其他同伴便無法找到該裝置。稍後在本小節中將會看到這方面的細節,我們將會讓範例 App 具備啟用和停用發佈的功能。

假使你研究過 Starter 專案並且有了基本的認識,那麼你一定注意到在 ViewController 場景中有一個工具列,上面還有一顆按鈕。我們將為此按鈕實作 startStopAdvertising(sender:) 動作函式,如同你所想的,我們將用來切換(啟用或停用)多點連線框架的發佈功能。為了方便起見,在此按鈕被點擊時,將只會顯示一個動作表單( Action Sheet )。在此動作表單中,只會有 2 顆按鈕:一顆用來切換 2 種不同的發佈狀態,另一顆則用來取消並關閉動作表單對話方塊。為了讓這些動作看起來更有趣一點,我們會讓第 1 顆按鈕的標題隨著其目前狀態而改變。

在檢視前述動作的程式碼之前,首先宣告一個新的 Bool 屬性,用來檢查裝置的發佈開關狀態。請確定目前開啟的依然是 ViewController.swift 檔案,然後來到類別的開頭,並且加入下列這行:

var isAdvertising: Bool!

此變數需要被賦予數值,我們會在 viewDidLoad 函式中做這件事。將其數值設定為 true ,意味著裝置本身目前正處於發佈狀態。但是為了讓這件事成為事實,我們必須啟用發佈功能,所以來到 viewDidLoad 函式,完成我們剛才所說的設定:

override func viewDidLoad() {
    ...

    appDelegate.mpcManager.advertiser.startAdvertisingPeer()

    isAdvertising = true    
}

現在,我們可以來到動作函式,並且進行實作。我先提供個別的程式碼片段,然後再來討論相關細節:

@IBAction func startStopAdvertising(sender: AnyObject) {
        let actionSheet = UIAlertController(title: "", message: "Change Visibility", preferredStyle: UIAlertControllerStyle.ActionSheet)

        var actionTitle: String
        if isAdvertising == true {
            actionTitle = "Make me invisible to others"
        }
        else{
            actionTitle = "Make me visible to others"
        }

        let visibilityAction: UIAlertAction = UIAlertAction(title: actionTitle, style: UIAlertActionStyle.Default) { (alertAction) -> Void in
            if self.isAdvertising == true {
                self.appDelegate.mpcManager.advertiser.stopAdvertisingPeer()
            }
            else{
                self.appDelegate.mpcManager.advertiser.startAdvertisingPeer()
            }

            self.isAdvertising = !self.isAdvertising
        }

        let cancelAction = UIAlertAction(title: "Cancel", style: UIAlertActionStyle.Cancel) { (alertAction) -> Void in

        }

        actionSheet.addAction(visibilityAction)
        actionSheet.addAction(cancelAction)

        self.presentViewController(actionSheet, animated: true, completion: nil)
    }

讓我們來檢視一下上述的實作做了些什麼事:

  • 首先,透過指定訊息和適當的樣式來初始化動作表單控制器。
  • 接著,根據 isAdvertising 變數目前的數值,透過指派適當的數值給 actionTitle 區域變數來指定動作表單第 1 顆按鈕的標題。
  • 透過擁有正確的標題,我們建立了會在使用者點擊第 1 顆按鈕時被觸發的新「通知動作」( Alert Action )。
  • 此處最重要的部分在於:根據 isAdvertising 變數的數值,我們可以停止或啟動裝置的發佈功能。當然了,也別忘了為 isAdvertising 變數設定完全相反的數值。
  • 我們為取消按鈕建立了(空白的)動作。
  • 這 2 個動作都被新增到動作表單控制器中。
  • 最後,動作表單控制器以動畫方式呈現到視圖上。

稍後在測試範例 App 時,你將看到上述動作函式的實際運作情況。透過上述這些程式碼,現在已經可以改變裝置是否可被探索發現的狀態了。

邀請同伴

多點連線的用途是讓 2 台(或更多)裝置建立連線並且交換資料。這意味著到目前為止我們已經完成了一半的工作,因為我們只有實作了瀏覽和發佈的功能。下一步則是邀請同伴來加入工作階段,以便讓 2 台裝置能夠進行通訊。

此處最重要的是要了解在手動實作 MPC 時(例如本文的範例 App ),完全依靠你自己來決定何時才是邀請找到的同伴來加入工作階段的最佳時刻。並沒有所謂的預設時間,最合適的時機完全視 App 的用途而定。舉例而言,在本範例 App 中,我們會在從 ViewController 類別的表格視圖上點擊了附近同伴的名稱之後,向它送出邀請。而其他人的 App 則可能是需要在瀏覽者發現同伴之後立刻就送出邀請。請容我再次強調,你可以自行根據 App 需求來決定何時應該送出邀請。此外同等重要的是,並不需要使用者的同意,即可送出邀請並且建立連線;你可以在場景背後完成任何作業。但是我會建議每次都事先告知使用者,說明 App 即將與其他裝置連線、傳送或接收資料,這樣會比較好。

將上述這些事項牢記在心,然後繼續回到範例 App 的實作工作上。本小節的目標如下:

  1. 在表格視圖中點擊了同伴的名稱之後,向它送出邀請。
  2. 接收邀請。
  3. 詢問使用者是否接受對方的聊天邀請。
  4. 接受或拒絕邀請。

同樣開啟 ViewController.swift 檔案,就從回應表格視圖的點擊開始吧。接下來,我們將實作 tableView(tableView:didSelectRowAtIndexPath:) 委派函式,並在其中實現一項重要任務:向選定的同伴送出邀請。你將看到,這麼做只需要一行程式碼。讓我們來檢視此函式,並且加以討論吧:

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    let selectedPeer = appDelegate.mpcManager.foundPeers[indexPath.row] as MCPeerID

    appDelegate.mpcManager.browser.invitePeer(selectedPeer, toSession: appDelegate.mpcManager.session, withContext: nil, timeout: 20)
}

老實說,我們可以不必寫第一行(將選定的同伴指派給區域變數),只需要單一行程式碼就可以做完所有的工作。無論如何,第一行可以讓程式碼看起來比較簡潔,所以還是維持這樣的 2 行寫法吧。

使用瀏覽者物件的 invitePeer(peerID:toSession:withContext:timeout:) ,以便透過多點連線框架將邀請送給選定的同伴。所需的參數如下:

  1. peerID:我們想要傳送邀請的同伴。
  2. toSession:在 MPCManager 類別中初始化的工作階段物件。
  3. withContext:假使希望傳送一些額外的資料給受邀的同伴,則可以使用此參數。格式為 NSData 物件。
  4. timeout:指定邀請者要花多少秒鐘來等待受邀端同伴的回覆。預設值是 30 秒,在此我們則設定為 20 秒。請檢視你的 App 需求,它會「告訴」你在實際應用中應該要等待多久的時間。

現在,跳到類別的開頭,以便匯入 MPC 框架,讓上述程式碼所造成的新錯誤能夠消失無蹤:

import MultipeerConnectivity

上述的匯入動作只是第一步。接著,我們必須實作一些 MCNearbyServiceAdvertiserDelegate 協定的委派函式,好讓 App 能夠處理收到的邀請。開啟 MPCManager.swift 檔案開始幹活吧。

首先看到的函式,包含了一個「邀請處理常式」( Invitation Handler ),用來回覆送出此邀請的同伴。此常式有 2 個參數,第 1 個是指出是否接受邀請的布林數值,而第 2 個參數則是工作階段物件(假使接受邀請的話)。在本文的範例 App 中,我們不打算直接回覆邀請者,因為我們要先詢問使用者的意願,看看他是否想要聊天。因此我們必須暫時先將邀請處理常式存成屬性,在獲得使用者的回覆之後,我們才會呼叫此常式,針對邀請做出回應。如果你還記得的話,我們先前的宣告如下:

var invitationHandler: ((Bool, MCSession!)->Void)!

我們將利用此變數來儲存邀請處理常式。這樣的話,我們便可以接著檢視第 1 個委派函式的實作:

func advertiser(advertiser: MCNearbyServiceAdvertiser!, didReceiveInvitationFromPeer peerID: MCPeerID!, withContext context: NSData!, invitationHandler: ((Bool, MCSession!) -> Void)!) {
    self.invitationHandler = invitationHandler

    delegate?.invitationWasReceived(peerID.displayName)
}

將邀請處理常式存成 invitationHandler 屬性是非常簡便的作法。請留意, 2 個處理常式都擁有同樣的名稱,所以在此務必使用 self 指示詞。

除此之外,我們也呼叫了 MPCManagerDelegate 協定的另一個函式。透過這樣的方式,我們可以通知 ViewController 類別收到了一個邀請,以便顯示通知控制器來詢問使用者是否想要聊天。稍後將會看到實作內容,但是現在請先留意一點,就是我們傳遞的東西只有同伴的顯示名稱。這是我們目前最需要關心的事情。

MCNearbyServiceAdvertiserDelegate 協定的第 2 個委派函式是用來處理無法啟用發佈者的情況。我們在此處的作法只是將錯誤訊息列印到主控台而已:

func advertiser(advertiser: MCNearbyServiceAdvertiser!, didNotStartAdvertisingPeer error: NSError!) {
    println(error.localizedDescription)
}

現在,讓我們回到 ViewController.swift 檔案,在其中實作 invitationWasReceived(fromPeer:) 委派函式。我們將只會顯示通知控制器讓使用者看見。其訊息將會告知使用者獲邀與誰聊天(我們將會顯示同伴的顯示名稱),並且提供 2 個選項:接受和拒絕。無論使用者選擇哪個方案,我們都會使用 MPCManager 類別的 invitationHandler 屬性來回覆邀請者。現在讓我們來檢視實作:

func invitationWasReceived(fromPeer: String) {
    let alert = UIAlertController(title: "", message: "\(fromPeer) wants to chat with you.", preferredStyle: UIAlertControllerStyle.Alert)

    let acceptAction: UIAlertAction = UIAlertAction(title: "Accept", style: UIAlertActionStyle.Default) { (alertAction) -> Void in
        self.appDelegate.mpcManager.invitationHandler(true, self.appDelegate.mpcManager.session)
    }

    let declineAction = UIAlertAction(title: "Cancel", style: UIAlertActionStyle.Cancel) { (alertAction) -> Void in
        self.appDelegate.mpcManager.invitationHandler(false, nil)
    }

    alert.addAction(acceptAction)
    alert.addAction(declineAction)

    NSOperationQueue.mainQueue().addOperationWithBlock { () -> Void in
        self.presentViewController(alert, animated: true, completion: nil)
    }
}

上述程式碼是典型的通知控制器實作,所以沒有特別複雜的邏輯在裡面。在 acceptAction 動作的情況中,我們呼叫了 mpcManager 物件的 invitationHandler 屬性,設定成 true 作為對邀請的回應,並提供工作階段物件。另一方面,如果使用者不想聊天,則將邀請處理常式設定成 false ,並以 nil 作為第 2 個參數,這次就不需要傳送工作階段物件了。

剛才又完成了另一個重大躍進,讓我們能夠繼續實作下去並且看到工作階段的狀態,以及在連線建立之後看見(在範例 App 中)發生了什麼事。

連接工作階段

MPCManager 類別中以 session 物件所表示的工作階段,是多點連線的最終目標。一旦 2 台同伴裝置連接形成工作階段,它們便能夠交換資料或資源,甚至是串流。工作階段總共有 3 種狀態:最想要的狀態是「已連線」( Connected )。第 2 種狀態是「連線中」( Connecting ),這是在嘗試連線時工作階段所進入的暫時性中間狀態。最後一種狀態是「未連線」( Not Connected ),原因可能是使用者拒絕連線(不接收邀請),或者單純只是連線失敗。

多點連線框架允許我們控制每種狀態,而且可以透過 MCSessionDelegate 協定的某個委派函式來進行。一般而言,此函式並不特別複雜,只需要檢驗個別狀況,然後採取適當的動作即可。在下列的程式碼片段中,你會看到我們只在 Connected 狀態中呼叫 MPCManagerDelegate 協定的另一個委派函式。至於剩下的 2 種情況,我們只會在主控台中顯示訊息,讓我們在測試階段可以隨時得知工作階段的明確狀態。在複製下列這段程式碼之前,請確定開啟的是 MPCManager.swift 檔案。

func session(session: MCSession!, peer peerID: MCPeerID!, didChangeState state: MCSessionState) {
    switch state{
    case MCSessionState.Connected:
        println("Connected to session: \(session)")        
        delegate?.connectedWithPeer(peerID)

    case MCSessionState.Connecting:
        println("Connecting to session: \(session)")        

    default:
        println("Did not connect to session: \(session)")
    }
}

使用 connectedWithPeer(peerID:) 委派函式,我們可以通知 ViewController 類別,指出裝置已經與附近的同伴(由 peerID 參數指定)連接形成工作階段。

再度回到 ViewController.swift 檔案,開始實作 connectedWithPeer(peerID:) 函式。在範例 App 中,我們希望在同伴們連接形成工作階段之後,立刻就開始聊天,所以我們只需要導覽至聊天視圖控制器( Chat View Controller )場景。夠簡單了吧,示範如下:

func connectedWithPeer(peerID: MCPeerID) {
    NSOperationQueue.mainQueue().addOperationWithBlock { () -> Void in
        self.performSegueWithIdentifier("idSegueChat", sender: self)
    }
}

請留意,上述程式碼會讓 2 台裝置(邀請者和受邀者)執行 Segue ,因為兩者的工作階段最終將會來到已連線狀態。

從此刻開始,我們的目標將轉往處理 ChatViewController 類別。在繼續之前,請容我先強調一點:我們並不打算處理使用者拒絕聊天邀請的情況。我認為在那種情況下並沒有任何重要的事情必須特別提出來討論,因此我將那個部分留給讀者們自行實作(如果有需要的話)。

用來傳送資料的簡便函式

在本小節中,我們將在 MPCManager 類別中建立自訂的函式,用來傳送資料給同伴。事實上,在此函式中,我們將會呼叫 MCSession 類別當中負責用來傳送資料的特殊函式,但是首先我們將會準備和設定此函式所需要的參數。我們建立此自訂函式的理由,只是為了往後不必重複進行所有的設定工作。

先來看實作的內容(請確定開啟的是 MPCManager.swift 檔案):

func sendData(dictionaryWithData dictionary: Dictionary, toPeer targetPeer: MCPeerID) -> Bool {
    let dataToSend = NSKeyedArchiver.archivedDataWithRootObject(dictionary)
    let peersArray = NSArray(object: targetPeer)
    var error: NSError?

    if !session.sendData(dataToSend, toPeers: peersArray, withMode: MCSessionSendDataMode.Reliable, error: &error) {
        println(error?.localizedDescription)
        return false
    }

    return true
}

上述呼叫的是 MPCSessionsendData(data:toPeers:withMode:error:) 函式,所需參數如下:

  • data:實際要傳送的資料,以 NSData 物件形式表示。
  • toPeers:陣列( NSArray ),包含了應該要接收資料的同伴。
  • withMode:資料傳輸模式。有 2 種模式:可靠的( Reliable )與不可靠的( Unreliable )。如果資料不是很重要(沒有收到也無妨)的話,則可以考慮使用第 2 種模式。
  • errorNSError 物件,將包含任何遭遇的錯誤。

現在讓我們來討論一下上述實作的功用。如你所見,此函式需要 2 個參數:(a)字典物件,在本例中包含的是訊息;(b)目標同伴。首先,字典會經由 NSKeyedArchiver 類別的歸檔功能,被轉換成 NSData 物件。接著,名為 peersArray 的新陣列被以目標同伴物件進行初始化。在宣告完錯誤變數之後,我們呼叫了 session 物件的資料傳輸函式,並且帶入上述的所有變數作為其參數。

假使在資料傳輸期間遭遇了任何錯誤,將會顯示其描述,並從函式返回 false 。否則的話,便返回 true ,代表一切順利。

上述函式將用於下一個小節當中。對我們而言,此函式就像一個工具,而這正是我們使用此函式的方式。

在同伴之間傳遞資料

同伴們在聊天工作階段期間傳送與接收的所有訊息,都會顯示在「聊天視圖控制器」場景既有的表格視圖中。最後顯示的訊息永遠都是最近收到的訊息,每次當傳送或收到訊息時,都會迫使重新載入表格視圖。

如同你所猜想的,我們將使用陣列來存放所有的訊息。而且顯然此陣列將會充當表格視圖的資料來源。有一點很重要必須說明的,就是此陣列的每個物件都是 Dictionary ,其索引鍵和數值都是字串。為何使用字典?因為傳送或接收的每則訊息都擁有一對資料:訊息的傳送者(作者)以及訊息本身。當我們的裝置是訊息的傳送者時(當我們是作者時),那麼在我們的裝置中,訊息的傳送者( Sender )將會被設定為「 self 」,而我們的同伴顯示名稱將會被傳送給另一方的裝置。

讓我們繼續回到程式的部分,因為還有好幾件事情必須做。第一步是宣告和初始化訊息陣列(表格視圖的資料來源)。此外,我們還會宣告並實體化應用程式委派物件,好讓我們能夠存取 AppDelegate 類別的 mpcManager 屬性。那麼就開啟 ChatViewController.swift 檔案看看要怎麼做吧。在類別的開頭加入下列這 2 行:

var messagesArray: [Dictionary] = []

let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate

messagesArray 被初始化為空白的陣列。此外,我們並不會將 ChatViewController 類別設定為 mpcManager 物件的委派。相反地,我們使用另外一種作法從 MPCManager 類別取得訊息。

在聊天時,所有的動作都是在新的訊息寫完並且按下 Send 按鈕之後被觸發的。一旦產生此行為,我們就必須進行下列這些事項:

  1. 隱藏鍵盤。
  2. 建立訊息字典,並呼叫前一個小節所實作的自訂函式來將此字典傳送給其他同伴。
  3. 建立另一個字典,內容是傳送者及其訊息,並將之儲存在 messagesArray 陣列中。
  4. 更新表格視圖。
  5. 在訊息傳送出去之後,清除文字欄位。

上述這些任務將在 UITextFieldDelegate 協定的 textFieldShouldReturn(textField:) 委派函式中完成。如果你檢視 viewDidLoad 函式的話,將會發現 ChatViewController 類別已經被設定為文字欄位的委派了。

實作內容如下:

func textFieldShouldReturn(textField: UITextField) -> Bool {
    textField.resignFirstResponder()

    let messageDictionary: [String: String] = ["message": textField.text]

    if appDelegate.mpcManager.sendData(dictionaryWithData: messageDictionary, toPeer: appDelegate.mpcManager.session.connectedPeers[0] as MCPeerID){

        var dictionary: [String: String] = ["sender": "self", "message": textField.text]
        messagesArray.append(dictionary)

        self.updateTableview()
    }
    else{
        println("Could not send data")
    }

    textField.text = ""

    return true
}

請留意幾件事情:如你所見,我們呼叫了 sendData(dictionaryWithData:toPeer:) 自訂函式,並且帶入剛才前面所建立的 messageDictionary 。此外,很有趣的一點是,我們使用 appDelegate.mpcManager.session.connectedPeers[0] 物件來指定目標同伴。說得更清楚一些,就是 MCSession 類別包含了名為 connectedPeers 的陣列屬性,所有與我們的裝置相連接的同伴都會被加入其中。在我們的實作當中,我們知道只會有一個同伴被加入工作階段,所以利用此陣列的第 1 個索引來直接存取該同伴是很安全的。

假使資料成功送出了,我們接著便需要準備一個新的包含了傳送者及訊息的字典。由於這是我們的訊息,因此傳送者會被設定為「 self 」。接著,我們使用 messagesArrayappend 函式將字典加入到陣列中。最後,我們呼叫 updateTableview 來更新表格視圖。此為自訂函式,我們稍後即將進行實作。

如果遭遇任何錯誤,我們只會將訊息顯示到主控台上。無論發生了什麼事,在上述函式結束時,我們都會清除文字欄位。

我們接著即將撰寫的 updateTableview 函式具有雙重用途:第一,用來重新載入表格視圖的資料,以便顯示所有的新訊息。第二,自動捲動至表格視圖的結尾處,以便總是看見最新的訊息。程式碼如下所示:

func updateTableview(){
    self.tblChat.reloadData()

    if self.tblChat.contentSize.height > self.tblChat.frame.size.height {
        tblChat.scrollToRowAtIndexPath(NSIndexPath(forRow: messagesArray.count - 1, inSection: 0), atScrollPosition: UITableViewScrollPosition.Bottom, animated: true)
    }
}

假使表格視圖內容大小的高度變得超出了表格視圖外框的高度,我們就必須捲動畫面。作法就是使用上述的函式。

現在,在類別的開頭匯入多點連線框架,以修正 Xcode 在 textFieldShouldReturn(textField:) 函式中所引發的錯誤:

import MultipeerConnectivity

在本小節結束之前,我們還有最後一項任務要做。就是必須修改與表格視圖有關的函式。首先,資料列的數目必須與 messagesArray 陣列中現有的物件總數相符:

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return messagesArray.count
}

tableView(tableView:cellForRowAtIndexPath:) 函式中,我們將會檢查訊息的傳送者是誰。假使傳送者的數值是「 self 」,我們便將副標題標籤設定為紫色,並且顯示訊息為「 I said: 」。如果是相反的情況,則設定為橘色,並且顯示訊息為「 X said: 」,其中 X 是其他同伴的顯示名稱。程式碼如下所示:

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    var cell = tableView.dequeueReusableCellWithIdentifier("idCell") as UITableViewCell

    let currentMessage = messagesArray[indexPath.row] as Dictionary

    if let sender = currentMessage["sender"] {
        var senderLabelText: String
        var senderColor: UIColor

        if sender == "self"{
            senderLabelText = "I said:"
            senderColor = UIColor.purpleColor()
        }
        else{
            senderLabelText = sender + " said:"
            senderColor = UIColor.orangeColor()
        }

        cell.detailTextLabel?.text = senderLabelText
        cell.detailTextLabel?.textColor = senderColor
    }

    if let message = currentMessage["message"] {
        cell.textLabel?.text = message
    }

    return cell
}

上述函式並不特別複雜,所以我們就不進行額外的討論了。

現在要討論的表格視圖資料列高度是一件非常有趣的事。顯然我們無法很明確地知道高度應該設為多少,因為訊息的長度是可變動的,所以每一列的高度應該動態設定。基於這個理由,我們使用 iOS 8 的新功能,叫做「自我調整儲存格」( Self Sizing Cells )。詳情請參考這篇由 Simon 所撰寫的文章。這項功能讓事情變得簡單許多:我們將儲存格的文字標籤的行數設定為 0 ,然後在 viewDidLoad 函式中設定下列 2 個屬性:

tblChat.estimatedRowHeight = 60.0
tblChat.rowHeight = UITableViewAutomaticDimension

iOS 會接手完成剩餘的工作。你會發現我們在 viewDidLoad 中已經是這麼做了。

如前所述,緊接著我們將繼續處理收到的資料。

接收資料

現在範例 App 已經可以傳送訊息了,而在收到訊息時我們必須想辦法加以處理。關於這個部分,我們使用的是 MPCManagerChatViewController 類別。針對此專案,我們將會實作 MPCSessionDelegate 協定的新函式。

開啟 MPCManager.swift 檔案,加入下列的函式:

func session(session: MCSession!, didReceiveData data: NSData!, fromPeer peerID: MCPeerID!) {
    let dictionary: [String: AnyObject] = ["data": data, "fromPeer": peerID]
    NSNotificationCenter.defaultCenter().postNotificationName("receivedMPCDataNotification", object: dictionary)
}

函式的內容只有 2 行,但是這 2 行非常重要。首先,收到的資料物件以及傳送端同伴都會被新增到字典中。接著,我們發出名為 receivedMPCDataNotificaton 的通知( NSNotification )。在 ChatViewController 中,我們將會觀察此通知,並且進行適當的處理,好讓傳送者的顯示名稱及其訊息能夠顯示在表格視圖中。在前一個小節裡,我曾經說過 ChatViewController 不會被設定為 MPCManager 類別的委派,我們會使用其他作法從類別中取得訊息。此方法就是運用前面所發出的通知。

現在,讓我們再度回到 ChatViewController.swift 檔案,在 viewDidLoad 函式中我們必須觀察上述的通知。作法非常簡單,只需要一行程式碼:

override func viewDidLoad() {
    ...    

    NSNotificationCenter.defaultCenter().addObserver(self, selector: "handleMPCReceivedDataWithNotification:", name: "receivedMPCDataNotification", object: nil)
}

有了這行程式碼,每次當收到新的資料時,便會發出通知,而 ChatViewController 類別將會得知一切。

在此我們還差最後一步,就是實作 handleMPCReceivedDataWithNotification(notification:) 自訂函式。此函式會在收到通知時被呼叫到。

在研究其實作之前,讓我們先來探討大概會需要做哪些事情:

  • 一開始,我們從發出的通知中取得字典,並且「擷取」其中包含的資料和同伴。
  • 我們將資料物件轉換成 Dictionary ,以便存取訊息。
  • 此時我們需要訂下一個約定,同意以某個特殊用語來代表結束聊天。此用語就是「 _end_chat_ 」訊息。
  • 除了上述特殊用語之外的訊息,我們都會建立新的字典來包含傳送者的顯示名稱及其訊息。此字典會被新增到 messagesArray 陣列中。此外,我們也會更新表格視圖。
  • 假使訊息發出了結束聊天的訊號,我們便會向使用者顯示通知視圖,讓他知道對方結束了對話,視圖控制器即將關閉。我們將在下一個小節實作這部分的程式碼。

讓我們來檢視所有上述行為的程式碼。額外的註解可以幫助你更容易理解:

func handleMPCReceivedDataWithNotification(notification: NSNotification) {
    // 從通知中取得包含了資料及來源同伴的字典
    let receivedDataDictionary = notification.object as Dictionary

    // 從收到的字典中「擷取」資料與來源同伴
    let data = receivedDataDictionary["data"] as? NSData
    let fromPeer = receivedDataDictionary["fromPeer"] as MCPeerID

    // 將資料( NSData )轉換成 Dictionary 物件
    let dataDictionary = NSKeyedUnarchiver.unarchiveObjectWithData(data!) as Dictionary

    // 檢查是否存在索引鍵為「 message 」的項目
    if let message = dataDictionary["message"] {
        // 確定訊息不是「 _end_chat_ 」
        if message != "_end_chat_"{
            // 建立新的字典,並且設定其傳送者與收到的訊息
            var messageDictionary: [String: String] = ["sender": fromPeer.displayName, "message": message]

            // 將此字典新增到 messagesArray 陣列中
            messagesArray.append(messageDictionary)

            // 使用主執行緒來重新載入表格視圖的資料並且捲動至最底部
            NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
                self.updateTableview()
            })
        }
        else{

        }
    }
}

我們已經準備好了。現在一旦收到新的訊息,範例 App 都能夠處理,也就是將訊息顯示在表格視圖上。

結束聊天

在範例 App 真正能夠使用之前,還有一些事情還沒有做完。其中一件事就是必須要能夠結束聊天,無論是某一方提出了要求,或是工作階段不再處於已連線的狀態了。

在「聊天視圖控制器」場景中,上方工具列裡面有一顆按鈕,在被點擊時將會呼叫 endChat(sender:) 動作函式。我們將利用此動作函式來傳送最後一個訊息給另一方同伴,告知聊天即將結束,然後關閉視圖控制器。此訊息想當然耳就是前一個小節約定好的「 _end_chat_ 」用語。

現在讓我們來檢視實作:

@IBAction func endChat(sender: AnyObject) {
    let messageDictionary: [String: String] = ["message": "_end_chat_"]
    if appDelegate.mpcManager.sendData(dictionaryWithData: messageDictionary, toPeer: appDelegate.mpcManager.session.connectedPeers[0] as MCPeerID){
        self.dismissViewControllerAnimated(true, completion: { () -> Void in
            self.appDelegate.mpcManager.session.disconnect()
        })
    }
}

如你所見,我們關閉了視圖控制器,並且使用 MCSessiondisconnect() 函式來中斷工作階段中的同伴。上述的實作給予工作階段足夠的時間,能夠存活直到最後的訊息被送出為止。

在上一個小節中,我們實作了一部分的 handleMPCReceivedDataWithNotification(notification:) 自訂函式。我之所以說一部分,是因為針對收到結束聊天訊息的情況,我們並沒有撰寫全部的程式碼。現在該是補齊程式碼的時刻了,在這之前我想先說明一下,我們會向使用者顯示通知控制器,告知另一方同伴結束了聊天。然後我們會中斷工作階段,並且關閉視圖控制器。此函式遺漏的程式碼如下所示:

func handleMPCReceivedDataWithNotification(notification: NSNotification) {
    ...

    // 檢查是否存在索引鍵為「 message 」的項目
    if let message = dataDictionary["message"] {
        ...
        else{
            // 如果是收到「 _end_chat_ 」訊息的情況
            // 向使用者顯示通知視圖
            let alert = UIAlertController(title: "", message: "\(fromPeer.displayName) ended this chat.", preferredStyle: UIAlertControllerStyle.Alert)

            let doneAction: UIAlertAction = UIAlertAction(title: "Okay", style: UIAlertActionStyle.Default) { (alertAction) -> Void in
                self.appDelegate.mpcManager.session.disconnect()
                self.dismissViewControllerAnimated(true, completion: nil)
            }

            alert.addAction(doneAction)

            NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
                self.presentViewController(alert, animated: true, completion: nil)
            })            
        }
    }
}

當另一方的同伴直接關閉 App 時,或是因為某些原因導致裝置之間的連線中斷時,同樣也會結束聊天。我們也必須考慮這些情況。要做的事情,就是在 MPCManager.swift 檔案中,透過 browser(browser:lostPeer:) 委派函式來發出新的通知。接著,我們必須觀察並處理此通知,就跟我們處理前一個通知時所做的事情一樣。但是這次我並不打算展示如何實作。為了讓本文閱讀起來比較有趣一點,我將這部分留給讀者們當作習題。我已經說明過需要完成哪些部分了,所以如果你願意的話,請勇往直前自行嘗試看看吧。

這樣我們就差不多實作完成了。只差一個小步驟,我們就可以準備測試整個 App 了。

最後的修飾

距離測試範例 App 只差臨門一腳了,因為還缺少一些東西。也就是在 MPCManager 類別中定義 MCSessionDelegate 協定的委派函式,儘管我們不會實際使用到,但是那些函式仍然必須存在。這麼做的話,任何原本看得見的 Xcode 錯誤都將消失無蹤。

直接開啟 MCPManager.swift 檔案,複製並貼上下列這些函式:

func session(session: MCSession!, didStartReceivingResourceWithName resourceName: String!, fromPeer peerID: MCPeerID!, withProgress progress: NSProgress!) { }

func session(session: MCSession!, didFinishReceivingResourceWithName resourceName: String!, fromPeer peerID: MCPeerID!, atURL localURL: NSURL!, withError error: NSError!) { }

func session(session: MCSession!, didReceiveStream stream: NSInputStream!, withName streamName: String!, fromPeer peerID: MCPeerID!) { }

測試範例 App

你可以在 2 台裝置(至少)上執行範例 App ,或者一台裝置搭配模擬器。然後藉由開啟和關閉 App 的發佈功能,開始測試瀏覽者和發佈者的功能。接著選取另一台同伴,點擊其名稱,開始聊天。傳送與接收一些訊息,然後結束對話。在完成測試之後,調整任何你想要修改的程式碼,也可以試著加入額外的功能。

我馬上就會給你看幾張螢幕截圖。請留意,我是在 iPhone 和模擬器上執行範例 App 。

發現了附近的同伴:

t27_1_list_peers

關閉發佈者:

t27_6_turn_off_advertiser

詢問使用者的聊天意願:

t27_2_ask_chat_alert

聊天:

t27_3_chatting

結束聊天:

t27_7_end_of_chat

結語

這篇關於多點連線的文章已經講完了,希望讀者們能夠從本文的實作和程式碼中獲益。今天這篇文章只是展示 MPC 的用法,以及幾條必須遵循的規則。至於其他未竟的部分,必須由你自行決定 MPC 的流程,或者何時以及如何實現所有不同的任務與動作(亦即何時傳送邀請)。關於 MPC ,顯然我們有些部分並沒有談論到,而有些地方則只運用了部分的功能。我們所實作的範例 App 還缺少許多功能,所以看起來可能有點普通,但是對我而言已經足夠展示我想要介紹的部分。最後容我說一句,我希望本文能夠作為大多數讀者的快速上手指南,並且引導他們找到更多的資訊和資源。多點連線框架可以為無數的新興 App 帶來一線生機,所以請不要再等待了!現在就開始認真思考這件事吧!

歡迎下載完整專案。

譯者簡介:陳佳新 – 奇步應用共同創辦人,開發自有 App 和網站之外,也承包各式案件。譯有多本電腦書籍,包括 O’Reilly 出版的 iOS 、 Android 、 Agile 和 Google Cloud 等主題,也在報紙上寫過小說。現與妻兒居住在故鄉彰化。歡迎造訪 https://chibuapp.com ,來信請寄到 [email protected]

原文Building a Chat App in Swift Using Multipeer Connectivity Framework

作者
Gabriel Theodoropoulos
資深軟體開發員,從事相關工作超過二十年,專門在不同的平台和各種程式語言去解決軟體開發問題。自2010年中,Gabriel專注在iOS程式的開發,利用教程與世界上每個角落的人分享知識。可以在Google+或推特關注 Gabriel。
評論
更多來自 AppCoda 中文版
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。