Adopting Network.framework for iOS peer-to-peer connectivity
Not much internet connectivity up here!
Apple introduced the Multipeer Connectivity framework back in 2013. This unheralded framework provides discovery of other nearby devices and supports exchange of data using peer-to-peer connectivity. For iOS, this would be via Wi-Fi networks, peer-to-peer Wi-Fi and Bluetooth.
At Qantas, we have been using this framework for sharing of Navigation Log entries between the pilot iPads during flight. Direct device-to-device sharing via peer-to-peer Wi-fi is ideal, as satellite internet is patchy and the onboard Wi-fi router is often overloaded and potentially insecure.
At WWDC 2019 Apple presented a new way to do peer-to-peer sharing using the relatively fresh Network framework. This blog post will go through how we implemented peer-to-peer connectivity using the Network framework.
Why switch to the Network framework?
The biggest challenge when providing peer-sharing is re-establishing connections after they have been lost. This will happen if the app is backgrounded, or the device is moves out of range.
We found that once the Multipeer Connectivity framework lost a connection, it was difficult to monitor this happening and automatically relocate and re-establish the connection. This necessitates building a user interface that allows users to monitor connections and manually re-establish them. This is not ideal, as we are aiming to provide pilots with a seamless experience, so they can focus on the task of completing the navigation log, rather than verifying data has transferred to their copilots.
In discussions with Apple Engineers at WWDC2019, it was recommended we adopt the Network framework for reliable device-to-device connectivity. Where-as the Multipeer Connectivity framework abstracts things away, the Network framework gives access to what is going on under the hood.
In contrast to the Multipeer Connectivity framework, the Network framework will limit you to communicating via Wi-Fi networks and peer-to-peer Wi-Fi, but not Bluetooth. For modern iPads this should not be a problem.
Building Blocks
The Network framework provides us with two discovery classes:
An NWListener object that listens for incoming network connections. By establishing a listener you also advertise yourself via Bonjour.
An NWBrowser object that lets you browse for advertised Bonjour services you may wish to connect to.
And finally
An NWConnection object which is the connection you establish with the other device.
Let’s go through how to set this up. We’ll start by building wrappers around the NWListener and NWBrowser objects, as per the sample code from WWDC2019. The wrappers will let us manage the NWListener and NWBrowser objects, which may need to be recreated under some situations.
We’ll configure our listener with a bonjour service name (essentially our private peer-to-peer Wi-Fi network) and a pre-shared key for security that we’ll cover later.
We’ll also need a UUID to uniquely identify each other when there are multiple peers around to connect to. This UUID will also be handy when establishing who is to initiate a connection.
Next lets create the browser to find advertised Bonjour services and initiate new connections.
We’ll limit ourselves to peer-to-peer Wi-Fi connections, for simplicity and security.
Finally let’s create a wrapper class around the NWConnection object.
Using these wrapper classes enable straight forward session management class which will manage a list of “active peers” (as distinct from connections which may or may not be ready and communicating). This level of abstraction makes reasoning a lot easier for the rest of your app, which shouldn’t need to worry about the current connection state.
We’ll also add callback closures at this level, which will help with unit testing later.
Previously we glossed over the initialisation of the connection and listener objects, which take an NWParameter object.
We’ll want secure connections so we’ll use TLS 1.2 over TCP using a pre-shared key. You’ll have to decide how each app will know the pre-shared key. In the WWDC 2019 example, a four digit code is shared between players of the game. This isn’t practical for seamless connectivity, so get that key onto the device in another secure way. Just don’t send it over the wire!
The implementation here is lifted from the WWDC 2019 sample code, and uses CyptoKit.
Who connects to whom?
The browser will discover which of the other devices are advertising.
However, if every device initiates a connection to the others, you’ll end up with double the required connections!
This is where the unique peer ID helps. Who-ever has the highest UUID can take the initiative to establish the connection.
In a scenario with two devices, you’ll end up with a host and a service connection.
With three devices, you’ll end up with one device hosting twice, one device connecting twice, and one doing both!
Exchanging data
The Network framework is suprisingly primitive when dealing with transfer of data. The WWDC 2019 Advanced Network 2 presentation walks through creating an elaborate transfer protocol specific to the demoed “TicTacToe” application. We can simplify things by implementing a basic Type-Length-Message protocol as described, but letting the users of our MultipeerSession object to exchange Codable structs on their own terms. Much saner and simpler!
Using a struct wrapping an enum and a custom Codable implementation, we can exchange just one struct and get all the messages we need.
Seamless Connectivity
Several scenarios will cause connectivity issues with peer-to-peer Wi-Fi, and this is where our own MultipeerSession object can come into its own.
The first messy situation is when the app is backgrounded and we want to shut down the connection. When the app resumes in the foreground we’ll want to re-establish connections.
The second scenario is failing or flaky connections. Despite configuring the TCP keep-alive to 2 seconds, observations suggest failed connections can linger for minutes, which does not result in a good user experience.
One solution is to add a higher level “keep-alive”, which periodically checks connections with a ping, and aggressively terminates failing connections, allowing them to be re-established. If someone out there can manage to achieve this level of reliability at the TCP level, please let me know!
This results in a fairly consistent “seamless connectivity” experience, with no user intervention required.
Test yourself by running the example project on multiple devices, then backgrounding or even terminating apps and starting again.
Unit Testing
How do you unit test something that needs multiple devices to work?
Normally such unit testing would be done by mocking out the dependencies - in this case the Network framework. However, it’s generally not a great idea to mock out objects that you don’t own, because you really don’t know how they work, and so your unit tests are only really testing against your understanding of how the framework works.
However, there is nothing stopping us from creating multiple MultipeerSession objects in the same app, and communicating between them.
On with the unit tests…
This example shows a basic “can we connect?” scenario.
We can also use this approach to test that we can
connect to multiple peers
ensure connections are only established when authentication is valid
restore lost connections
exchange of Codable data
build higher level tests based on business logic
These tests could be considered integration tests, and will run take time to run. However, I think they are invaluable for having confidence in the higher-level business logic that you will wrap around peer-to-peer connectivity, which I promise you will become quite complex. I guess that’s why third-party SDKs like Ditto exist.