Giới thiệu Multipeer Connectivity Framework và xây dựng ứng dụng chát đơn giản sử dụng MPC Framework1

1.Giới thiệu Multipeer Connectivity Framework (MPC Framework)

  • MPC cung cấp sự hỗ trợ cho dịch vụ tìm kiếm các thiết bị iOS gần với thiết bị tìm kiếm thông qua wifi, peer-to-peer wifi hay Bluetooth sau đó kết nối các thiết bị để giao tiếp với nhau thông qua việc gửi các dữ liệu dạng message, streaming, và các tài nguyên khác.
  • MPC có 4 class quan trọng sau:
    • Peer (MCPeerID): một peer thực tế là 1 thiết bị. Nó cũng cần được cài đặt như các đối tượng của một class. Nó chứa 1 thuộc tính quan trong là displayName là tên của thiết bị khi xuất hiện với các thiết bị khác, các peer khác.
    • Session (MCSession class): Đây là một kết nối được thành lập giữa 2 peer ngay sau khi peer thứ 2 chấp nhận 1 lời mời từ peer thứ nhất. Mỗi session chỉ là kết nối giữa 2 thiết bị.
    • Browser (MCNearbyServiceBrowser class): Các phương thức của class này dùng để tìm kiếm các thiết bị gần thiết bị tìm kiếm và gửi lời mời các thiết bị kết nối với thiết bị tìm kiếm.
    • Advertiser (MCNearbyServiceAdvertiser):clas này chịu trách nhiệm quảng bá cho một thiết bị, hay nói cách khác nó làm cho thiết bị hiển thị hoặc không trước một thiết bị khác, nhận lời hoặc từ chối một lời mời từ thiết bị khác.
  • Logic của MPC rất đơn giản: một thiết bị sử dụng browser của nó để tìm kiếm các thiết bị khác xung quanh nó. Một thiết bị cần phải thông báo sự tồn tại của nó để nó có khả năng được tìm thấy bởi các thiết bị khác. Đầu tiên nó sẽ tìm kiếm các thiết bị, sau đó gửi các lời mời để tạo 1 kết nối với 1 session. Lời mời có thể được gửi một cách tự động tới một thiết bị khác khi nó được tìm thấy hoặc sau khi một user nào đó quyết định gửi 1 lời mời. Sau khi 1 lời mời được chấp nhận thì một session được thành lập và hai thiết bị trong session có khả năng gửi nhận dữ liệu.

2.Xây dựng ứng dụng demo sử dụng MPC Framework

  • Chúng ta sẽ xây dựng một ứng dụng chát đơn giản sử dụng MPC Framework. Trong ứng dụng này sẽ có 2 view controller. Một là table view để hiển thị các thiết bị xung gần với thiết bị của chúng ta được tìm kiếm bởi MPC. Khi một thiết bị trong danh sách được chọn thì chúng ta sẽ gửi tới thiết bị đó một lời mời kết nốiv ới nhau để tạo thành 1 session. Nếu thiết bị đó không đồng ý thì nó không có chuyện gì xảy ra. Nếu lời mời được chấp nhận thì sẽ mở ra ViewController thứ hai. Khi đó hai thiết bị có thể trao đổi gửi nhận các các message.
  • MPCManager: là class chứa các phương thức của framwork, và có một protocol để sử dụng delegation pattern để gọi đến class.
    • dòng đầu tiên của class này chính là việc chúng ta sẽ import framwork vào project:
    import MultipeerConnectivity
- Sau đó ta khai báo các biến cần sử dụng:
    var session: MCSession!
    var peer: MCPeerID!
    var browser: MCNearbyServiceBrowser!
    var advertiser: MCNearbyServiceAdvertiser!
    var foundPeers = [MCPeerID]()
    var invitationHandler: ((Bool, MCSession!)->Void)!
- mảng foundPeers dùng để lưu trữ thông tin các thiết bị gần đó đã được tìm thấy.
- Tiếp theo chúng ta sẽ custom MPCManager để nó phù hợp với MPC protocol. Những phương thức delegate của protocol sẽ giúp ta có khả năng xử lý việc kết nối _multipeer_, và làm việc với các _browser_, _advertiser_ và _session_.
    class MPCManager: NSObject, MCSessionDelegate, MCNearbyServiceBrowserDelegate, MCNearbyServiceAdvertiserDelegate
- Tạo các phương thức khởi tạo cho class. Chúng ta sẽ nhìn với mỗi lần phương thức được gọi sẽ khởi tạo một đối tượng là _peer_. Mỗi _peer_ này sẽ được tất cả các thiết bị khác xung quanh đó tìm thấy. Vậy nên ngay khi khởi tạo thì mỗi _peer_ này đều cần cung cấp một cái tên. Tên hiển thị này sẽ được tất cả các thiết bị khác nhìn thấy, tên có thể đặt là bất kì một chuỗi kí tư nào mà bạn muốn.
    override init() {
        super.init()
        peer = MCPeerID(displayName: UIDevice.currentDevice().name)
    }
- Ở đây chúng ta sẽ lấy tên của thiết bị thông qua _UIDevice_. Với thông tin như trên thì một peer đã được khởi tạo tuy nhiên ta vẫn cần phải thêm vào phần còn lại của phương thức khởi tạo bởi mỗi đối tượng peer đều được gọi lần đầu tiên bằng hàm khởi tạo nên trong phương  thức khởi tạo cần có các parameter khác như:
    override init() {
        ...
        session = MCSession(peer: peer)
        session.delegate = self
        browser = MCNearbyServiceBrowser(peer: peer, serviceType: "appcoda-mpc")
        browser.delegate = self
    }
Phương thức khởi tạo cần có hai parameter: Thức nhất là một đối tượng _peer_, thứ hai là giá trị không thay đổi trong hàm khởi tạo bởi nó liên quan tới việc tìm kiếm của _browser_. Hay nói cách khác nó xác định duy nhất với các _peer_ khác để trong ứng dụng để MPC có khả năng tìm kiếm được, ngoài ra nó cũng cần phải được cài đặt sao cho quảng cáo nó được tốt nhất. Có hai luật để cần phải theo cần phải tuân theo khi cài đặt giá trị cho parameter này là:
    - Không được dài quá 15 kí tự.
    - Chỉ chứa các chữ in thường, số và dấu gạch nối.

    Trong trường hợp nếu bạn phá vỡ một quy tắc thì sẽ có một ngoại lệ khi chạy ứng dụng sẽ làm cho ứng dụng bị crash.
- về _advertiser_:
    override init() {
        ...
        advertiser = MCNearbyServiceAdvertiser(peer: peer, discoveryInfo: nil, serviceType: "appcoda-mpc")
        advertiser.delegate = self
    }
- Và để kết thúc của phần này chúng ta cần tạo ra một _protocol_ để thưc thi _delegation pattern_. Đầu tiên chúng ta cần khai báo một phương thức _delegate_ mà chúng ta cần trong suốt quá trình phát triển nâng cao ứng dụng này. Thêm đoạn mã sau vào phía trên phần định nghĩa lớp là ta đã có được protocol của nó:
    protocol MPCManagerDelegate {
        func foundPeer()
        func lostPeer()
        func invitationWasReceived(fromPeer: String)
        func connectedWithPeer(peerID: MCPeerID)
        }
ở phần này chúng ta sẽ không thảo luận nhiều về phần protocol này tuy nhiên chúng ta sẽ hiểu thêm rất nhiều về phần này ở các phần sau của bài viết.
- Cuối cùng chúng ta khai báo một đối tượng delegate trong lớp MPCManager
    var delegate: MPCManagerDelegate?
khi kết thúc phần này ta sẽ thấy có một số lỗi XCode báo tuy nhiên không đáng lo ngại bởi trong các phần sau khi chúng ta thực thi các phương thức cho protocol thì các lỗi báo sẽ mất hết.
  • Tìm kiếm Peers: MCNearbyServiceBrowserDelegate protocol có 3 phương thức hỗ trợ chúng ta xử lý việc tìm kiếm và mất peer, một vài lỗi xuất hiện trong quá trình tìm kiếm. Chúng ta sẽ phát triển ứng dụng này dựa trên việc phát triển ba phương thức trên.
    • Bước đầu chúng ta sẽ tìm hiểu phương thức đầu tiên trong ba phương thức trên, phương thức này được gọi khi có một thiết bị được MPC tìm thấy:
    func browser(browser: MCNearbyServiceBrowser!, foundPeer peerID: MCPeerID!, withDiscoveryInfo info: [NSObject : AnyObject]!) {
    foundPeers.append(peerID)
    delegate?.foundPeer()
    }
Việc đầu tiên và quan trọng nhất cần thực hiện trong phương thức này chính là thêm peer vừa tìm thấy được vào trong mảng _foundPeers_. Sau đó chúng ta dùng mảng này như một _datasource_ cho tableView hiển thị nó.
- Phương thức thứ hai là phương thức ngược lại so với phương thức thứ nhất là xử lý việc khi thấy mất một peer.
    func browser(browser: MCNearbyServiceBrowser!, lostPeer peerID: MCPeerID!) {
    for (index, aPeer) in enumerate(foundPeers){
        if aPeer == peerID {
            foundPeers.removeAtIndex(index)
            break
        }
    }
    delegate?.lostPeer()
    }
- Phương thức cuối cùng cần hiện thực đó là phương thức xử lý lỗi xảy ra trong quá trình tìm kiếm:
    func browser(browser: MCNearbyServiceBrowser!, didNotStartBrowsingForPeers error: NSError!) {
        println(error.localizedDescription)
    }
  • Hiển thị các Peer: Đến phần này thì ứng dụng của chúng ta có thể tìm kiếm các thiết bị lân cận, có thể thêm hay xoá thiết bị này khỏi danh sách các thiết bị được tìm thấy. Ở đây chúng ta sẽ cho hiển thị các thiết bị được tìm thấy trong danh sách ở ViewController đã chứa tableView mà chúng ta đã nhắc tới bên trên. Trước tiên chúng ta cần phải khai báo một đối tượng MPCManager trong lớp AppDelegate. thêm dòng code sau vào phía trên của lớp này
    var mpcManager: MPCManager!
khởi tạo nó trong phương thức _application(application:didFinishLaunchingWithOptions:)_
    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    // Override point for customization after application launch.
    mpcManager = MPCManager()
    return true
}
- Bây giờ chúng ta có thể hiện thực phần code trong lớp ViewController.swift. Chắc chắn là chúng ta sẽ thêm nhiềm code hơn trong lớp này, và chúng ta sẽ cập nhật, chỉnh sửa lại các phương thức của tableView.
    let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate
Tiếp theo chúng ta sẽ cài đặt _ViewController_ như là một _delegate_ của lớp  _MPCManager_.
    override func viewDidLoad() {
        ...
        appDelegate.mpcManager.delegate = self
        }
XCode sẽ thông báo lỗi bởi chúng ta chưa khai báo lớp này kế thừa _MPCManagerDelegate_ protocol, chúng ta chỉ cần thêm các delegate này vào dòng code định nghĩa lớp
    class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, MPCManagerDelegate
- Sau đó chúng ta tiếp tục bằng cách hiện thực hai phương thức delegate mà chúng ta đã khai báo trong protocol ở phần trước là _foundPeer_ và _lostPeer_. Trong cả hai trường hợp này chúng ta đều cần làm mới lại tableView, việc hiện thực rất đơn giản như sau:
    func foundPeer() {
        tblPeers.reloadData()
    }
    func lostPeer() {
        tblPeers.reloadData()
    }
- Trên đây là các phương thức rất quan trọng để lấy cập nhật lại dữ liệu tuy nhiên chúng ta cần phải hiện thức các hàm delegate cả tableView mới có thể hiển thị _foundPeers_
    - trả lại số cell của tableView:
        func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return appDelegate.mpcManager.foundPeers.count
}
    - Hiển thị mỗi tên của peer trên mỗi mỗi cell của tableView:
        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
}
    - Cuối cùng là chiều cao của mỗi cell trong tableView:
        func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    return 60.0
}
- Chúng ta bắt đầu tìm kiếm khi ứng dụng bắt đầu chạy nên chúng ta sẽ thêm dòng code sau vào phương thức _viewDidLoad_:
    override func viewDidLoad() {
    ...
    appDelegate.mpcManager.browser.startBrowsingForPeers()
}
  • Xử lý Advertising: Ngoài việc tìm kiếm thì MPC còn cho phép chúng ta giới thiệu bản thân với các thiết bị lân cận khác để các thiết bị có khả năng tìm thấy chúng ta. Ở đây chúng ta có thể phát triển chức năng này cao hơn đó là có thể kích hoạt, hoặc tắt chức năng quảng cáo này.
    • Như vậy chúng ta cần khai báo biến kiểu Bool để biết là thiết bị có quảng cáo hay không. Thêm dòng code sau vào phía trên cùng của lớp ViewController.swift
    var isAdvertising: Bool!
và chúng ta cần gán giá trị cho biến này ở trong phương thức _viewDidLoad_:
    override func viewDidLoad() {
        ...
        appDelegate.mpcManager.advertiser.startAdvertisingPeer()
        isAdvertising = true
}
- Tiếp theo là phương thức xử lý khi chúng ta bấm vào nút kích hoạt hoặc tắt chắc năng giới thiệu:
    @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)
    }
  • Mời Peer: ở phần này chúng ta thực hiện các việc sau

    • Gửi lời mời tới một người dùng khác khi được bấm vào name của người dùng đó.
    • Nhận lời mời.
    • Xử lý việc người dùng khác chấp nhận lời mời
    • Chấp nhận hoặc từ chối lời mời từ một user khác.

    Ở đây chúng ta bắt sự kiện người dùng bấm vào một cell của tableView bằng phương thức: _tableView(tableView:didSelectRowAtIndexPath:) _ phương thức này thực hiện việc gửi lời mời tới user có name hiển thị ở cell được bấm vào.

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)
}
Trong phương thức _invitePeer(peerID:toSession:withContext:timeout:)_ có bốn tham số truyền vào là:
    - _peerID_: Peer mà chúng ta muốn gửi lời mời.
    - _oSession_: Đối tượng session mà chúng ta khỏi tạo trong lơp MPCManager.
    - _withContext_: Tham số này có thể sử dụng khi bạn muốn gửi thêm dữ liệu vào lời mời. Nó được khuyến cáo dùng kiểu NSData.
    - _timeout_:Thời gian chờ câu trả lời của người được mời. Mặc định là 30s tuy nhiên với các ứng dụng thực chúng ta cần xem xét thời gian hợp lý.
  • Tiếp theo chúng ta cần hiện thực hai phương thức delegate của MCNearbyServiceAdvertiserDelegate protocol trong lớp MPCManager.swift
    • Phương thức invitationHandler: có hai tham số truyền vào: một là biến Bool chỉ thị cho biết là lời mời có được chấp nhận hay không, một biến là đối tượng session trong trường hợp lời mời được chấp nhân. Chúng ta có thể hiện thực phương thức delegate trước
    func advertiser(advertiser: MCNearbyServiceAdvertiser!, didReceiveInvitationFromPeer peerID: MCPeerID!, withContext context: NSData!, invitationHandler: ((Bool, MCSession!) -> Void)!) {
    self.invitationHandler = invitationHandler
    delegate?.invitationWasReceived(peerID.displayName)
}
phương thức thứ hai của _MCNearbyServiceAdvertiserDelegate protocol_ dùng để xử lý khi chức năng giới thiệu không thể bật lên.
    func advertiser(advertiser: MCNearbyServiceAdvertiser!, didNotStartAdvertisingPeer error: NSError!) {
    println(error.localizedDescription)
}
- Quay trở lại _ViewController.swift_ chúng ta hiện thực phương thức delegate _invitationWasReceived(fromPeer:)_ nó sẽ hiển thị một thông báo cho người dùng biết
    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)
    }
}
  • Kết nối 1 session chúng ta hiện thực một phương thức session trong lớp MPCManager.swift như sau:
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)")
    }
}

Trong phương thức trên chúng ta có sử dụng đến phương thức connectedWithPeer(peerID:) nên chúng ta cần hiện thực phương thức này trong lớp ViewController.swift

func connectedWithPeer(peerID: MCPeerID) {
    NSOperationQueue.mainQueue().addOperationWithBlock { () -> Void in
        self.performSegueWithIdentifier("idSegueChat", sender: self)
    }
}
  • Phương thức xử lý gửi dữ liệu: chúng ta tạo phương thức sendData trong lớp MPCManager.swift
func sendData(dictionaryWithData dictionary: Dictionary<String, String>, 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
}

Phương thức sendData(data:toPeers:withMode:error:) được gọi trên có các tham số truyền vào là: - data: dữ liệu thực sẽ đường truyền kiểu NSData - toPeers: mảng các peer sẽ nhận được dữ liệu. - withMode: kiểu truyền dữ liệu. Có hai kiểu truyền dữ liệu là ReliableUnreliable - error: NSError chứa lỗi nếu xảy ra

  • gửi dữ liệu giữa các peers: khai báo mảng dữ liệu ở phía trên của lớp ChatViewController.swift đoạn code sau:
var messagesArray: [Dictionary<String, String>] = []
let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate

Tiếp theo chúng ta cần hiện thực phương thức textFieldShouldReturn(textField:)

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
}

Sau khi các hàm này được gọi tới thì cần phải làm mới lại dữ liệu của tableView nên ta cần có một hàm hiện thực nó:

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)
    }
}

tiếp đó là một số hàm delegate của tableView:

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return messagesArray.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    var cell = tableView.dequeueReusableCellWithIdentifier("idCell") as UITableViewCell

    let currentMessage = messagesArray[indexPath.row] as Dictionary<String, String>

    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
}

Trong phương thức viewDidLoad chúng ta cần khởi tạo 2 giá trị:

tblChat.estimatedRowHeight = 60.0
tblChat.rowHeight = UITableViewAutomaticDimension
  • Nhận Dữ Liệu:
    • Trong lớp MPCManager.swift chúng ta hiện thực phương thức:
    func session(session: MCSession!, didReceiveData data: NSData!, fromPeer peerID: MCPeerID!) {
    let dictionary: [String: AnyObject] = ["data": data, "fromPeer": peerID]
    NSNotificationCenter.defaultCenter().postNotificationName("receivedMPCDataNotification", object: dictionary)
}
- Sau đó chúng ta cần thêm dòng code sau vào phương thức _viewDidLoad_ của lớp _ChatViewController.swift_
    override func viewDidLoad() {
    ...
    NSNotificationCenter.defaultCenter().addObserver(self, selector: "handleMPCReceivedDataWithNotification:", name: "receivedMPCDataNotification", object: nil)
}

3.Tổng Kêt: Trên đây giới thiệu các bước cơ bản và các phương thức delegate cần hiện thực của MPC framework để xây dựng 1 ứng dụn chát đơn giản sử dụng swift. Dưới đây là link của github của toàn bộ chương trình: see more


All Rights Reserved