My Favorite iOS 7 APIs: Multipeer Connectivity

Multipeer Connectivity allows you to discover and share data with other iOS devices within Bluetooth radio range or on the same WiFi subnet. It is much easier to use than Bonjour.

I wrote a simple MPC chat program in Xamarin.iOS.

There's necessarily a few hundred lines of code, but 90% of it is just the scaffolding necessary to support a four-view application. The actual discovery and communication is done with just a handful of code.

There are two phases for Multipeer Connectivity: Discovery and the Session phase. During the Discovery phase, one device acts as a coordinator or browser, and many devices advertise their interest in connecting. Devices advertise their interest in sharing a protocol defined by a string.

I created a base class DiscoveryViewController : UIViewController for both the advertising and browsing:

[code lang="csharp"]
//Base class for browser and advertiser view controllers
public class DiscoveryViewController : UIViewController
{
public MCPeerID PeerID { get; private set; }

public MCSession Session { get; private set; }

protected const string SERVICE_STRING = "xam-chat";

public DiscoveryViewController(string peerID) : base()
{
PeerID = new MCPeerID(peerID);
}

public override void ViewDidLoad()
{
base.ViewDidLoad();

Session = new MCSession(PeerID);
Session.Delegate = new DiscoverySessionDelegate(this);
}

public void Status(string str)
{
StatusChanged(this, new TArgs\<string>(str));
}

public event EventHandler\<targs \<string>> StatusChanged;
}

[/code]

This base class holds a PeerID (essentially, the nickname for the device in the chat), an MCSession (the actual connection), and a SERVICE_STRING that specifies what type of MPC session I support ("xam-chat"). Additionally, it exposes an event StatusChanged (which is subscribed to by a UILabel in the DiscoveryView class (not shown, because it's trivial).

Events relating to the MCSession are handled by ChatSessionDelegate, but those occur after discovery, so putting that aside for now, let's look at how simple are the AdvertiserController and BrowserController subtypes of DiscoveryViewController:

[code lang="csharp"]
public class AdvertiserController : DiscoveryViewController
{
MCNearbyServiceAdvertiser advertiser;

public AdvertiserController(string peerID) : base(peerID)
{
}

public override void DidReceiveMemoryWarning()
{
// Releases the view if it doesn't have a superview.
base.DidReceiveMemoryWarning();

// Release any cached data, images, etc that aren't in use.
}

public override void ViewDidLoad()
{
base.ViewDidLoad();

View = new DiscoveryView("Advertiser", this);
var emptyDict = new NSDictionary();
Status("Starting advertising...");

advertiser = new MCNearbyServiceAdvertiser(PeerID, emptyDict, SERVICE_STRING);
advertiser.Delegate = new MyNearbyAdvertiserDelegate(this);
advertiser.StartAdvertisingPeer();
}
}

class MyNearbyAdvertiserDelegate : MCNearbyServiceAdvertiserDelegate
{
AdvertiserController parent;

public MyNearbyAdvertiserDelegate(AdvertiserController parent)
{
this.parent = parent;
}

public override void DidReceiveInvitationFromPeer(MCNearbyServiceAdvertiser advertiser, MCPeerID peerID, NSData context, MCNearbyServiceAdvertiserInvitationHandler invitationHandler)
{
parent.Status("Received Invite");
invitationHandler(true, parent.Session);
}
}

public class BrowserController : DiscoveryViewController
{
MCNearbyServiceBrowser browser;

public BrowserController(string peerID) : base(peerID)
{
}

public override void DidReceiveMemoryWarning()
{
// Releases the view if it doesn't have a superview.
base.DidReceiveMemoryWarning();

// Release any cached data, images, etc that aren't in use.
}

public override void ViewDidLoad()
{
base.ViewDidLoad();

View = new DiscoveryView("Browser", this);

browser = new MCNearbyServiceBrowser(PeerID, SERVICE_STRING);
browser.Delegate = new MyBrowserDelegate(this);

Status("Starting browsing...");
browser.StartBrowsingForPeers();
}

class MyBrowserDelegate : MCNearbyServiceBrowserDelegate
{
BrowserController parent;
NSData context;

public MyBrowserDelegate(BrowserController parent)
{
this.parent = parent;
context = new NSData();
}

public override void FoundPeer(MCNearbyServiceBrowser browser, MCPeerID peerID, NSDictionary info)
{
parent.Status("Found peer " + peerID.DisplayName);
browser.InvitePeer(peerID, parent.Session, context, 60);
}

public override void LostPeer(MCNearbyServiceBrowser browser, MCPeerID peerID)
{
parent.Status("Lost peer " + peerID.DisplayName);
}

public override void DidNotStartBrowsingForPeers(MCNearbyServiceBrowser browser, NSError error)
{
parent.Status("DidNotStartBrowingForPeers " + error.Description);
}
}
}

[/code]

Quite a few lines, but very straightforward: the advertiser uses the iOS class MCNearbyServiceAdvertiser and the browser uses the class MCNearbyServiceBrowser. The browser's delegate responds to discovery by calling MCNearbyServiceBrowser.InvitePeer and the advertiser's delegate responds to an invitation by passing true to the invitationHandler.

The Chat Session

When the invitation is accepted, it's time for the ChatSessionDelegate to take over:

[code lang="csharp"]
public class ChatSessionDelegate : MCSessionDelegate
{
public DiscoveryViewController Parent{ get; protected set; }

public ChatViewController ChatController
{
get;
set;
}

public ChatSessionDelegate(DiscoveryViewController parent)
{
Parent = parent;
}

public override void DidChangeState(MCSession session, MCPeerID peerID, MCSessionState state)
{
switch(state)
{
case MCSessionState.Connected:
Console.WriteLine("Connected to " + peerID.DisplayName);
InvokeOnMainThread(() => Parent.NavigationController.PushViewController(new ChatViewController(Parent.Session, Parent.PeerID, peerID, this), true));
break;
case MCSessionState.Connecting:
Console.WriteLine("Connecting to " + peerID.DisplayName);
break;
case MCSessionState.NotConnected:
Console.WriteLine("No longer connected to " + peerID.DisplayName);
break;
default:
throw new ArgumentOutOfRangeException();
}
}

public override void DidReceiveData(MCSession session, MonoTouch.Foundation.NSData data, MCPeerID peerID)
{

if(ChatController != null)
{
InvokeOnMainThread(() => ChatController.Message(String.Format("{0} : {1}", peerID.DisplayName, data.ToString())));
}
}

public override void DidStartReceivingResource(MCSession session, string resourceName, MCPeerID fromPeer, MonoTouch.Foundation.NSProgress progress)
{
InvokeOnMainThread(() => new UIAlertView("Msg", "DidStartReceivingResource()", null, "OK", null).Show());

}

public override void DidFinishReceivingResource(MCSession session, string resourceName, MCPeerID formPeer, MonoTouch.Foundation.NSUrl localUrl, out MonoTouch.Foundation.NSError error)
{
InvokeOnMainThread(() => new UIAlertView("Msg", "DidFinishReceivingResource()", null, "OK", null).Show());
error = null;

}

public override void DidReceiveStream(MCSession session, MonoTouch.Foundation.NSInputStream stream, string streamName, MCPeerID peerID)
{
InvokeOnMainThread(() => new UIAlertView("Msg", "DidReceiveStream()", null, "OK", null).Show());

}
}
[/code]

Again, this is mostly scaffolding, but be sure to note that it expects to be called on a background thread and uses InvokeOnMainThread to manipulate the UI. It also relies on the ChatViewController:

[code lang="csharp"]
public class ChatViewController : UIViewController, IMessager
{
protected MCSession Session { get; private set; }

protected MCPeerID Me { get; private set; }

protected MCPeerID Them { get; private set; }

ChatView cv;

public ChatViewController(MCSession session, MCPeerID me, MCPeerID them, ChatSessionDelegate delObj) : base()
{
this.Session = session;
this.Me = me;
this.Them = them;

delObj.ChatController = this;
}

public override void DidReceiveMemoryWarning()
{
// Releases the view if it doesn't have a superview.
base.DidReceiveMemoryWarning();

// Release any cached data, images, etc that aren't in use.
}

public override void ViewDidLoad()
{
base.ViewDidLoad();

cv = new ChatView(this);
View = cv;

cv.SendRequest += (s, e) => {
var msg = e.Value;
var peers = Session.ConnectedPeers;
NSError error = null;
Session.SendData(NSData.FromString(msg), peers, MCSessionSendDataMode.Reliable, out error);
if(error != null)
{
new UIAlertView("Error", error.ToString(), null, "OK", null).Show();
}
};
}

public void Message(string str)
{
MessageReceived(this, new TArgs\<string>(str));
}

public event EventHandler\<targs \<string>> MessageReceived = delegate {};
}
[/code]

Again, it's the simplicity that stands out: Session.SendData is used to transmit a string. The SendRequest event is wired to a UITextField and the MessageReceived event is wired to a UILabel:

[code lang="csharp"]
public class ChatView : UIView
{
readonly UITextField message;
readonly UIButton sendButton;
readonly UILabel incoming;

public ChatView(IMessager msgr)
{
BackgroundColor = UIColor.White;

message = new UITextField(new RectangleF(10, 54, 100, 44)) {
Placeholder = "Message"
};
AddSubview(message);

sendButton = new UIButton(UIButtonType.System) {
Frame = new RectangleF(220, 54, 50, 44)
};
sendButton.SetTitle("Send", UIControlState.Normal);
AddSubview(sendButton);

incoming = new UILabel(new RectangleF(10, 114, 100, 44));
AddSubview(incoming);

sendButton.TouchUpInside += (sender, e) => SendRequest(this, new TArgs\<string>(message.Text));
msgr.MessageReceived += (s, e) => incoming.Text = e.Value;
}

public event EventHandler\<targs \<string>> SendRequest = delegate {};
}
[/code]

The ChatViewController.Message method is called by the ChatSessionDelegate.DidReceiveData method.

And that's really all there is to it.