• Building a Chat App in Swift Using Multipeer Connectivity Framework
  • GABRIEL THEODOROPOULOS
  • Translator: yrq110

Multipeer Connectivity- Multi-point connection

In iOS programming, there are some SDKS that are more interesting and appealing to developers than others, and Multipeer Connectivity is one of them. As you know, the MPC framework isn’t new in iOS8, it’s been around since 7. I’ve written a few tutorials on this before, but to be honest, I’m surprised at how much interest there is in it. Now that I’ve written a new tutorial some time later, I’m sure there are a few things that need to be explained.

You might be wondering why this is an old topic, but not a new feature in iOS8. Well, I did it for three reasons:

  1. In answering emails from readers who want to know how to handle a variety of different tasks in the Multipeer Connectivity framework, I found some issues that I hadn’t noticed before, and some of them weren’t easy to find.
  2. In previous tutorials I used a default, created view controller for user invites and connections. Readers have reported that they want to see a manual implementation, which is the next step.
  3. I find using Swift to implement MPC very useful and helpful for beginners.

Since the birth of Multipeer Connectivity, there have been many possibilities for developers to implement new ideas. It’s tempting to have a simple way to connect devices, which is why developers are so happy to integrate it into their apps. But if you haven’t used the MPC framework yet, I have to warn you: it’s not always as stable and robust as you’d like it to be. I’ve seen this in my projects, and some developers have told me about it. The MPC uses Bluetooth and WiFi to connect to nearby devices, which sounds great and promising, but sometimes the connection fails or slows down due to problems with communication. This situation is important to consider if you are transferring important data. I recommend that you use an alternate communication solution (like a Web service) and make sure there is an alternate path for your app to continue working when MPC fails. Having said that, I still believe MPC is a great tool for all iOS developers and deserves another tutorial.

I’m not going to get into the details of the MPC. If you want to have a taste, check out this tutorial. Otherwise, take a look at my summary of the MPC.

The MPC contains four important classes that correspond to four concepts:

  1. Peer-node (MCPeerID): A node is really a device, and this is usually a priority because it is used as a parameter when the next class instance is initialized. It contains an important attribute – displayName, which is the name that a device displays for nearby nodes.
  2. Session-session (MCSession class): This is a connection established between two nodes if the first node invites the second, and the second accepts the invitation. A session is associated with only two devices. A third-party device could not connect to an existing session.
  3. Browser-searcher (MCNearbyServiceBrowser Class): Methods of this class are used to find nearby devices and invite them to join a session. As a precondition, the other devices must first broadcast themselves. Use this class in this tutorial to manually invite other devices.
  4. Advertiser – broadcast (MCNearbyServiceAdvertiser) : this class is responsible for broadcasting and a device, control of the equipment for other visible or invisible, to accept or reject a session invitation to other nodes.

The logic of an MPC is simple: a device (a node) uses its searcher to find other devices around it. A device must broadcast itself, otherwise it will not be detected. Once it finds one or more nodes during its search, it sends it an invitation for a session connection. This invitation can be sent automatically when a nearby node is found, or the user can decide manually whether to send it or not, depending on how the application China is implemented. In any case, as soon as the invitation is accepted, the session is established and data, resources, and streams can be sent and received between the two nodes.

My goal in this article is to show you how to programmatically search, invite, and connect to other nodes. Keep in mind that what you’ll see later is just one way to implement the code, and it’s clear that you can implement the functionality of your app in any way you want by using all the tools available in MPC. The following is just an example of an MPC framework implementation that implements a streamlined, start-to-finish programming process without using any additional SDKS. I hope you will find some of the answers you are looking for at the end of this tutorial.

Finally, if you’ve never used the MPC framework before, I suggest you take a look at apple’s official documentation with my previous tutorials. Oh, stop wasting your time and start programming.

directory

  • About the Demo App
  • A custom class
  • Search node
  • Displays the nodes searched
  • To deal with radio
  • Invite node
  • Connecting to a session
  • An easy way to send data
  • Send data between nodes
  • Receive data
  • Terminate the chat
  • The last modification
  • To test the app

A custom class

Now that we know what this project does let’s create a custom class that will handle all the MPC framework methods we need to implement, and finally create a new protocol that uses the proxy pattern.

To do this, press Cmd and N in Xocde and in the boot view that appears, select Create a new Cocoa Touch Class:

In the second step, make the new class inherit from the NSObject class and name it MPCManager

Follow the wizard to make sure you are working with the mpcManager.swift file.

In the first line of the code, you need to import the Multipeer Connectivity framework, so add the following code to the file header:

import MultipeerConnectivityCopy the code

Next, declare the object of the MPC framework class you want to use. Add the following line at the top of the class:

var session: MCSession!

var peer: MCPeerID!

var browser: MCNearbyServiceBrowser!

var advertiser: MCNearbyServiceAdvertiser!Copy the code

In addition to the above, we need to declare two variables that we will use later:

var foundPeers = [MCPeerID]() var invitationHandler: ((Bool, MCSession!) ->Void)!Copy the code

The foundPeers array holds all the discovered nodes. Note that at this point no connections are made to these nodes, just that you need to know that these nodes are around. The array is initialized at the same time it is declared, so there is no additional need to set it to nil, expecting the array to be ready when the new object is found.

The invitationHandler is a fully declared handler, but it will be ignored for now until it is used.

Next, you need to make the custom class compliant with the specific MPC protocol. The delegate methods of these protocols allow us to handle multipeer connectivity related operations such as search, broadcast, session, etc. Modify the first line of the class as follows to add the protocol:

class MPCManager: NSObject, MCSessionDelegate, MCNearbyServiceBrowserDelegate, MCNearbyServiceAdvertiserDelegateCopy the code

I don’t think it’s necessary to explain the purpose of each agreement.

Now create an initializer to initialize all MPC objects. One by one, starting with the peer object, which represents a display name that needs to be provided at initialization. This display name will be visible to other nodes and can be set to any string. To keep things simple, I’ll just use the device name as the display name, but I don’t recommend doing this in a real app. Maybe you should let the user type in the desired name, or use another method to generate unique node names. In the code, initialize as follows:

override init() {
    super.init()

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

Get the device name from the UIDevice class.

After the peer object is successfully initialized, operations are performed on other objects. Note that the peer must be initialized first, because all subsequent objects need to use it. The session object:

override init() {
    ...

    session = MCSession(peer: peer)
    session.delegate = self
}Copy the code

As you can see, a session initialization takes only one parameter, the previous peer. The session object is delegated to the current class during initialization.

Next comes the searcher object:

override init() {
    ...

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

The initialization of this object takes two parameters: the first is the peer. The second is a value that cannot be changed after initialization, specifying the type of service that the searcher can find. Simply put, it is used for accurate identification among a large number of nodes, so that the MPC knows what to search for, and broadcasters must be set to the same service type (as you’ll see in a moment). There are two rules to follow when setting this value :(a) no more than 15 characters, and (b) only lowercase ASCII characters, digits, and hyphens. If you don’t follow this rule a Runtime exception will pop up and the app will crash.

For broadcasters:

override init() {
    ...

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

Note that you set the same service type as before, as well as a parameter called discoveryInfo, which is a dictionary that you need to set when you want to send additional information to other nodes that you find. Note that the keys of this dictionary must be strings. For convenience, set this parameter to nil.

With the initialization method ready, here’s the complete code:

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
}Copy the code

Before we finish, create a protocol that implements the delegate pattern. Note that we declare all delegate methods currently required during app development and will not deal with the protocol until they are needed.

Add the following code snippet above the custom class:

protocol MPCManagerDelegate {
    func foundPeer()

    func lostPeer()

    func invitationWasReceived(fromPeer: String)

    func connectedWithPeer(peerID: MCPeerID)
}Copy the code

The purpose of each of the above functions will be discussed next, in more detail in the next section.

Finally, declare a delegate object in the MPCManager class:

var delegate: MPCManagerDelegate?Copy the code

Having successfully completed the first major part of the project so far, you can rest your feet. I would like to mention that the current errors in Xcode are normal and will disappear after implementing MPC delegate methods.

Search node

MCNearbyServiceBrowserDelegate agreement has three methods allow us to handle the found and lost node, and the possibility of error in the search process. Now I’m going to implement these methods to continue developing the demo app, but it’s so simple that it doesn’t take much effort.

Let’s start with the first method, which is called by the MPC when a nearby node is discovered (in other words, when other devices are discovered). Take a look at the implementation code:

func browser(browser: MCNearbyServiceBrowser! , foundPeer peerID: MCPeerID! , withDiscoveryInfo info: [NSObject : AnyObject]!) { foundPeers.append(peerID) delegate? .foundPeer() }Copy the code

One important action to start with is adding the found nodes to the foundPeers array (declared earlier, remember?). Then use this array as the data source for the list view in the ViewController class, listing all the nodes found. To do this, you need to call the foundPeer delegate method in the MPCManagerDelegate protocol, which needs to be implemented in the ViewController class (in the next section), where it reloads the list data to show the user the newly discovered node.

Now that you’ve dealt with the case when a node is discovered, you also need to consider the opposite case, where you need to focus on what happens when the node is no longer available (discoverable). Therefore, to implement the next proxy method:

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

There’s nothing to say. The code is pretty clear. We first find the node’s position in the foundPeers array, and then remove it so that it no longer exists in our list, so we need to remind the ViewController to refresh the list. So you need to call the lostPeer proxy method, which overloads the list’s data in its implementation.

Finally, there is a proxy method that needs to be implemented to manage possible error messages and search for unavailability. Obviously, no serious errors are handled here, just error messages are displayed, as shown in the following code:

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

Before we end this section, note that using the delegate method above to prompt the ViewController class to respond to node changes, you should use the key-value encoding and observer mechanism to track changes in the foundPeers array. However, thanks to the MPCManagerDelegate protocol, there is no need to write additional code. If you don’t need the other methods in this protocol, it’s better to use KVC and KVO instead of the delegate mode. In this case, we stick to implementing delegate methods with faster and cleaner execution (PS: I admit there is a problem here (´ ω · ‘)).

Once that’s done, the app can search for nodes. Now, add the necessary code to display the nodes found.

To deal with radio

In addition to searching, the Multipeer Connectivity framework allows devices to broadcast themselves to nearby nodes, without which searching would be meaningless. In fact, they are mutually reinforcing and equally important. Broadcast, which means whether a device is visible to other devices. If the broadcast function is enabled in the app, the device will be visible to other nearby nodes. Otherwise, other nodes will not notice. Take a look at the details in this section, enabling and disabling the broadcast function of demo App.

If you’re already familiar with starting a project, you’ll remember that in the ViewController scenario there’s a toolbar that contains a button. This button is then used to turn on and off the broadcast function by implementing the startStopAdvertising(sender:) method. Clicking on the button displays an action form with two buttons: one to switch between the two broadcast states, and the other to control whether the Action form dialog box opens. To make it more interesting, make the text on the first button change with the current state.

Before implementing this in code, declare a new Bool property that determines whether the device is in broadcast state. To make sure you’re working with the viewController.swift file, add the following line to the top of the class:

var isAdvertising: Bool!Copy the code

In viewDidLoad this variable is assigned a value, set to true, which means that the device is currently broadcasting itself, but also needs to turn on this function, so in viewDidLoad we need to implement both:

override func viewDidLoad() {
    ...

    appDelegate.mpcManager.advertiser.startAdvertisingPeer()

    isAdvertising = true    
}Copy the code

Now you can implement it in the Action method. I’ll show you the code and explain it later:

@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) }Copy the code

A brief description of what the above implementation code does:

  • First, an Action form controller is initialized with a message and the appropriate style.
  • Next, set the title of the first button in the action form by assigning the corresponding value to the actionTitle local variable, based on the current value of isAdvertising.
  • With the correct title, create a new alert action that is triggered when the user clicks the first button.
  • The most important part: Stops or starts broadcasting for the device, depending on isAdvertising values. Don’t forget to set the isAdvertising value to the opposite.
  • Create an (empty)action for the Cancel button.
  • Both actions are added upside down to the Action form controller.
  • Finally, open the Show animation Settings jump view.

You’ll see how the above action method works in later app tests. Once you’ve done the above, you can change the discoverability state of the device.

Connecting to a session

Sessions are represented using the Session object in the MPCManager class, the ultimate goal when using Multipeer Connectivity. When both nodes are connected to a session, they can exchange data and resources with each other and perform streaming. A session has three states: A connection has been established, or the connection is Not Connected.

The Multipeer Connectivity framework gives us control over each state through the Delegate method of the MCSessionDelegate protocol. Usually this approach is not difficult to implement, just set the behavior for each state. In the code section below, an additional MPCManagerDelegate protocol delegate method is called for the connected state, and for the other two states, only a message is displayed on the console so that the current session state can be accurately known during testing. Note when pasting the following code that the current operating file is 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)") } }Copy the code

Use the connectedWithPeer(peerID:) delegate method to notify the ViewController class that the device has connected to a session with a nearby node (as determined by the peerID parameter above).

Go back to the viewcontroller.swift file again to implement the connectedWithPeer(peerID:) method. In this demo you want to start the chat as soon as the node connects to the session, so just jump to the ChatViewController scenario. Is simple:

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

Note that this segues both devices (inviter and invitee), and the session state of both becomes connected.

We’re going to do the ChatViewController class, but before we do that, let me emphasize one thing: we’re not going to deal with the user terminating the chat invitation, which I don’t think is very important, so I’ll leave it to you.

Send data between nodes

All messages sent and received by the node during the chat session are displayed in the list of The ChatViewController. The last message is always the most recent, reloading the list every time a message is sent or received.

As you might have guessed, an array is used to store all the messages, which will obviously be used as the data source for the list. Importantly, each object in this array is a dictionary of string-type keys. Why dictionaries? For each message sent or received, you need to assign a pair of data: the sender of the message and the message itself. When our device is the message sender, the message sender in the device is set to “self” and our node name is sent to other devices.

Back to coding, now there are a few things to do. The first step is to declare and initialize the message array (the data source for the list), and then declare and initialize an application delegate object, thus accessing the mpcManager property of the APPDelegate class. Open the chatViewController.swift file and add the following two lines at the top of the class:

var messagesArray: [Dictionary] = []

let appDelegate = UIApplication.sharedApplication().delegate as AppDelegateCopy the code

The messagesArray is initialized to an empty array. Instead of setting the ChatViewController as a delegate to mpcManager, we’ll use another way to get messages from the mpcManager class.

In chat, whenever a new message is edited and the send button is pressed, some actions are triggered. Here are some actions we need to do: 1. Hide the keyboard. 2. Create a dictionary based on the message and call the custom method from the previous section to send the message to the other nodes. 3. Create another dictionary with the sender and message as its contents and store it in the messagesArray array. 4. Refresh the list. 5. Clear the input field after the message is sent.

This happens in the UITextFieldDelegate protocol in the delegate method textField shouldreturn (textField:). If you look at the viewDidLoad method, you’ll see that the ChatViewController class has been set up as a delegate object for TextField.

The code implementation is as follows:

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
}Copy the code

Note: call the custom method sendData publishes the event (dictionaryWithData: toPeer:), enter messageDictionary created before. Interestingly we use appDelegate. MpcManager. Session. ConnectedPeers [0] object to indicate the target node, to be sure MCSession class contains a call connectedPeers array properties, Every node connected to our device is recorded. In our code, we already know that only 1 node will be added to the session, so we simply access the first element of the array.

If the data is successfully sent, prepare a new dictionary for the sender and the message. If it is our message, the sender is set to “self” and the dictionary is then added to the array using the Append method of messagesArray. Finally, I call the updateTableview method to refresh the list, which is a custom method that I’ll implement in a moment.

If an error occurs, a message is displayed on the console. No matter what happens, the input field is emptied at the end of the above method.

The updateTableview method to be written has two purposes: first, to refresh the list and display new messages. Second, scroll to the bottom of the list automatically so that the latest news is always displayed. The code is as follows:

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)
    }
}Copy the code

When the content size of the list is greater than the box height of the list, it must be scrolled, using the method above.

Now import the Multipeer Connectivity framework at the top of the class and fix the error in textField shouldreturn (textField:) :

import MultipeerConnectivityCopy the code

One more thing that needs to be done before the end of this section is to refine the list-related approach. We first need to allocate the number of existing elements in the messagesArray array to the number of rows:

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

In tableView (tableView: cellForRowAtIndexPath:) method needs to be checked in the sender’s message. If the sender’s value is “self,” the child label is purple and the message prefix “I said:” is displayed. Otherwise, set it to orange and “X said:” is displayed, where X is the name of the other node. The code is as follows:

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 cellc
}Copy the code

There is nothing difficult about the above method, so I won’t discuss it.

We need to focus on the height of a list row. Obviously we can’t be sure what the height is because the length of the message changes and the height of each row needs to change dynamically. So use an iOS 8 feature called Self sizing Cells. Check out this tutorial from Simon. Set the number of lines in the text label of the list bar to 0, and then set the following two properties in the viewDidLoad method:

TblChat. EstimatedRowHeight = 60.0 tblChat. RowHeight = UITableViewAutomaticDimensionCopy the code

I already have that code in viewDidLoad, so I’ll leave it to iOS.

Once you’ve done that, you’re ready to receive the data.

Terminate the chat

There are only a few things left for the application to fully implement, and one of them is to terminate the chat, which happens when one of the nodes wants to terminate or the session is not connected.

In the ChatView Controller scenario, there is a button on the top toolbar that, when clicked, calls the endChat(sender:) method. Use this method to send termination messages to the other nodes telling them that the chat is over, and then return to the previous view controller. This message, of course, is the “_end_chat” phrase described in the previous section.

Let’s look at the implementation:

@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()
        })
    }
}Copy the code

As you can see, here we dismiss the view controller and then disconnect the session with the node using the Disconnect () method of the MCSession class. Disconnect the session after the jump animation is complete, giving the session enough time to live.

Method to realize the custom in the last section handleMPCReceivedDataWithNotification (notification) part of the code, and why I said “part”, for they had not to add any code when sending end chat message processing. It’s time to add, but before we do that, we need to show the user an Alert controller to remind him that the other nodes have finished the chat, and after that, we’ll break off from the session and dismiss view controller. Here’s what’s missing from the code:

func handleMPCReceivedDataWithNotification(notification: NSNotification) {
    ...

    // Check if there's an entry with the "message" key.
    if let message = dataDictionary["message"] {
        ...
        else{
            // In this case an "_end_chat_" message was received.
            // Show an alert view to the user.
            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)
            })            
        }
    }
}Copy the code

When other nodes close the app, the chat will also be terminated, or the connection between the two devices will be lost for some reason, which needs to be considered. All you need to do is add a new notification in the browser(Browser :lostPeer:) proxy method of the mpcManager.swift class, and you also need to listen for and process it, just like the previous notification. But THIS time I won’t show you the concrete implementation, as an exercise, please do it yourself.

We’re almost there. We’ll test it with a few little additions.