100 Days of Swift – Day 89

Hacking with Swift – Learning Project 25

Our goal in today’s app is to learn about peer-to-peer networking so, without much further ado, let’s get started!

Setting up

By now this is fairly simple: we need to create a new Single View iOS app, call it Selfie share and create it in a sensible position. I personally created a folder called “25_P2P Connectivity”. We are warned that we will need at least two copies of our app, one on the Simulator and one on a physical device.

Importing photos again

Woah, this was fast! I know Paul was going fast in general but this time I think he finished the app before I could even realise it… Never mind, I’ll walk you back step by step at a slower pace for us mere mortals…

First of all we need to make our ViewController class inherit from UICollectionViewController then move (as quickly as you can otherwise Xcode will throw an exception called “Too slow to become an iOS developer”) to the storyboard and erase the current view controller. Replace it with a “Collection view controller” and make it inherit from our ViewController class. Embed it in a Navigation Controller and don’t forget to move the entry point arrow to before this one, otherwise you will get a “Destination unreachable” warning when trying to build and run the app.

Now, select the Collection View in the document outline and go to the Size Inspector: now change the cell size width and height to 145 and each of the four section insets to 10. Then, drag an Image View into the collection’s cell and, after selecting it, change its view values in the Size Inspector to x: 0, y: 0, width: 145, height: 145 so that it occupies all of the cell.

Once this is done go to Editor > Resolve Auto Layout Issues > Reset to Suggested Constraints. Last but not least, select the collection cell and, in the Attributes Inspector, change its identifier to be “Image View”. Then select the image view and, in the same inspector, change its tag to be 1000. This will help us identify this view in code.

Back to ViewController.swift

Now we need to create a property to store our images, called images, which is an array of UIImages. Then, in viewDidLoad we set the title of the view controller to “Selfie share” and create a right bar button item of .camera style with a selector action called importPicture.

Before writing this method we need to implement the collection view data source basic methods, that is the numberOfItemsInSection method which will return the .count property of the images array and the cellForItemAt method which will dequeue a reusable cell with the given identifier and then optionally bind the cell.viewWithTag(1000) as? UIImageView object to an imageView constant and set its .image property to be the appropriate element of the images array for the index path’s row before returning the cell.

Now for the image picker methods: first be sure to make your class conform to UINavigationControllerDelegate and UIImagePickerControllerDelegate or nothing of this will ever work. Write the importPicture@objc method with, inside, an instance of UIImagePickerController, then setting its possibility to edit to true, its delegate to the current view controller and then presenting it. Finally, the didFinishPickingMediaWithInfo: we check that there is an image in the .editedImage key of the info dictionary, then dismiss the picker controller, insert the image at the beginning of the images array and reload the collection view data.

That’s it! This was supposed to be a stripped down version of the Project 10 and Project 13 code but I sincerely didn’t remember a single line of it … I mean, come on, after only one project done about a month ago are we really expected to remember all this? Sincerely? NO!

Going peer to peer: MCSession, MCBrowserViewController

This session was very confusing and … confused … The text on the website is not in sync with the video so following one while listening to the other (which is what I usually do because of how fast Paul goes) is not only not helpful but plain damaging… So, let’s see if I managed to follow somehow.

First of all we add a left bar button item with the .add system item, a self target and a showConnectionPrompt selector action. We then immediately write down this method which will declare an alert controller with a title of “Connect to others”, no message and an alert preferred style. It will then add three actions: “Host a session”, which in turn calls the startHosting method, “Join a session”, calling joinSession and a good old “Cancel” action. The alert controller is then presented with animation.

We are then introduced to the four classes we need to work with from now onward: MCSession, MCPeerID, MCAdvertiserAssistant, MCBrowserViewController. Let’s briefly look at them in the Documentation.

MCSession

An MCSession object enables and manages communication among all peers in a Multipeer Connectivity session. To set up a session we need to create a peerID that represents the local peer (or retrieve one that was previously archived) using the init(displayName:)method, then use the init(peer:) method to initiate the session object, invite peers to join the session using either an MCNearbyServiceBrowser or, as in our case, an MCBrowserViewController object and set up an advertiser assistant object to allow other devices to ask our app to join a session that they create.

The Documentation is very descriptive in all this so be sure to check it out if you want to delve deeper into it.

MCPeerID

This is an object that represents a peer in a multiplier session. The code shown in the Documentation is in Objective-C so I will not express myself on that (I kind of understand what it does but all those pointers scare me!)

MCAdvertiserAssistant

The MCAdvertiserAssistant is a convenience class that handles advertising, presents incoming invitations to the user, and handles users’ responses.

MCBrowserViewController

The MCBrowserViewController class presents nearby devices to the user and enables the user to invite nearby devices to a session.


All the needed instructions are in the documentation for MCSession.

That said, we import the MultipeerConnectivity framework and create three properties: a MCPeerID(displayName: UIDevice.current.name), an optional MCSession and an optional MCAdvertiserAssistant.

Inside viewDidLoad, now, let’s initialise the mcSession property to have a peerID equal to the peer ID property, no security identity and a .required encryption preference. We then set the session’s delegate to be self, our view controller.

We then go on writing the startHosting and joinSession methods, both of which, being action methods, require a single UIAlertAction parameter. In both of them we check for the session to be there and, in the first the initialise the advertiser assistant to have a serviceType equal to “hws-project25” (or any other 15 characters long identifier that is appropriate for what you are doing), no discovery info and our current session as session. In theory now this method should call the .start() method but the video doesn’t mention it so I just wrote it inside a comment. The second method will instead initialise our browser view controller with the same service type as before and our current session. It will then set the view controller as its delegate and be presented.

We now have a whole bunch of errors which require us to conform to the MCSessionDelegate and MCBrowserViewControllerDelegate protocols… which in turn will just cause even more errors…

Invitation only: MCPeerID

So… yeah… this last run was a bit of a mess but in the end not to difficult.

First things first, we need to add the needed protocol stubs as suggested by Xcode. Automatically Xcode will add them to the beginning of the class, just after the declaration so I moved them down. There are three of them which needs to be left empty because, in this app, we do not need them, and they are: session(_:didReceive:), session(_:didStartReceivingResourceWithName:) and session(_:didFinishReceivingResourceWithName:). Then we have two methods which just need to call dismiss(animated: true) and they are browserViewControllerDidFinish(_:) and browserViewControllerWasCancelled(_:).

The only two methods that need some work are session(_:peer:didChange:) and session(_:didReceive:fromPeer:). In the first one we need to switch on the state parameter and just print some debugging messages, while in the second we need to push the work on the main thread with a DispatchQueue.main.async call, optionally bind the data received to an UIImage, insert it in our array of images and reload the collection view’s data.

Still, our app will not work so we need to complete the didFinishPickingMediaWithInfo method then: check that we have an active connection, check if there are any peers to send to, convert the new image to a Data object by calling the optionally bound object with the .pngData() method and then send it to all peers being sure to show the proper errors.

Last, do you remember that code I commented out because it was written but not spoken about? Just uncomment it to start the session.

Now, run the app on the simulator, start hosting a session, then run it on a device and join a session. My experience was quite different from the one in the video but well, such is life, right? Also I got plenty of messages and errors in the console, which I think is not right but we have no right to know at this point, I guess.

So, now, the app is finished and you can find it here.

Review

I still have some time for today I will go through the review and try to face some of the challenges. Here is what we learned today in this intense, though quick, lesson:

  1. All instances of UIView or its subclasses have a tag property. We can use this to locate specific views, although using properties is a better idea.
  2. We don’t need to request the user’s permission before using UIImagePickerController. Because this view controller is entirely controlled by UIKit, iOS knows we can’t abuse it and so permission isn’t needed.
  3. viewWithTag() looks for any subview that has a specific tag integer, which may be the view we’re searching itself. If you have lots of views in your layout, this might involve a lot of work.
  4. We can convert String to Data and Data to String. Strings are data in a specific format, just like images, arrays, and even integers. This was a good joke, because the other option was that “data sent using the .reliable transmission mode is guaranteed to arrive”, to which Paul answered “nothing is guaranteed in networking…”. I mean thanks the… yes, that…, the documentation says this: “The framework should guarantee delivery of each message, enqueueing and retransmitting data as needed, and ensuring in-order delivery.” … and then one is surprised if we get confused…
  5. Each multipeer network needs a service type string that identifies it uniquely. Apple recommends you include your company and app name to make sure this string is unique.
  6. We can pass a function to the handler of a UIAlertAction. Sometimes you’ll want to use a closure and other times a function; do what works best for you.
  7. The @unknown default case is there to catch cases that get added to an enum after we wrote our code. This stops our app ending up in an unknown state if something changes in the future.
  8. UICollectionView supports cell re-use as a performance optimisation. This works just like table views: as a cell scrolls off screen it goes into a re-use queue.
  9. The MultipeerConnectivity framework can present its own user interface for finding nearby networks. Using the system user interface helps us find and join networks easily. …as if we were ever shown this UI before running the app or as if we had ever been told about other possibilities…
  10. Every participant in a multipeer network needs a display name. This is just a string, and it can be whatever we want.
  11. Multipeer networking can take advantage of Bluetooth and Wi-Fi. We don’t actually care what networking system is used – that’s an implementation detail that iOS takes care of for us.
  12. Multipeer networking can send any kind of Data instance. As long as you can encode it to Data, iOS can send it to other peers.

Challenges

Challenge 1: show an alert when a user has disconnected from our multiplier network. Something like “Paul’s iPhone has disconnected” is enough.

The Discussion section for the didChange state MC-session method explains that when a previously connected peer is no longer connected, the .notConnected state is triggered.

The alert creation code was quite straightforward but, when running, I got some errors in the console because I was not calling it on the main thread, which I quickly fixed. Here is the code I used, in the .notConnected case of the switch inside the session(didChange state) method:

DispatchQueue.main.async { [weak self] in
    let notConnectedAC = UIAlertController(title: "Disconnected!", message: "\(peerID.displayName) has left the network", preferredStyle: .alert)
    notConnectedAC.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
    self?.present(notConnectedAC, animated: true, completion: nil)
}

This works but it takes about 5-6 seconds for the alert to appear when a peer has left the network and, also, by now, the only way I have to leave a network is to force quit the app on one of the devices. Is this intended? Maybe we could add a leave network option.

This is actually possible and quite easy so I decided to implement that as well. Inside the showConnectionPrompt method I added a new action called “Leave the session” and calling the handler leaveSession. Then I wrote said method with inside just a guard statement equivalent to the ones we already had and a mcSession.disconnect() call.

That’s it! Done with bonus!

Challenge 2: try sending text messages across the network. You can create a Data from a string using Data(yourString.utf8) and convert a Data back to a string by using String(decoding: yourData, as: UTF8.self).

After reading this challenge I asked myself: “but wait… aren’t we sending images over the network? Didn’t we configure all our app as a plain image sharing app?” … Whatever the case, I found more or less what I have to do and … I had a whole lot of fun! I also made so many silly mistakes that it almost resulted in more fun than the correct part! My solution is definitely not the most elegant ever, as I should have had a new view controller instantiate with some lines for the messages … I mean… the possibilities were just endless but I really want to get on with this as quickly—though efficiently—as possible.

I’ll try to be as brief as possible: in viewDidLoad I created a flexible space button and a message bar button item with a title of “Send message”, a plain style and a selector action of writeMessage. I then added them to the toolbarItems array.

Next was the implementation of the writeMessage method: I created a new alert controller, added a text field to it, configured the send action and, inside it, checked that there was some text in the text field, checked for the existence of a session and then converted the message to Data using the suggested initialiser, before enclosing everything in a do-catch block. I then added the action to the controller together with a “cancel” action.

Finally, I updated the didReceive method so that if the received data was not an image, it would decode the message and create a new alert controller to show the message. As I felt particularly bold, I created a “reply” action that allowed any of the receivers to reply. Of course this would short-circuit very soon in case more than one peer would answer simultaneously but hey, I’m just testing right now! I would never implement such a UI with an app that needed to share both images and text.

So, done by now!

Challenge 3: add a button that shows an alert controller listing the names of all devices currently connected to the session — use the connectedPeers property of your session to find that information.

Once more, my solution is not the most elegant one but at least it works and it is already pretty late here and I would like to get some sleep. Tomorrow I have a bureaucratic day…

In viewDidLoad I added a new plain button with a showPeers selector and added it to the toolbar. In that method I checked for the existence of a session, for the connectedPeers list to be not empty and then created an alert controller showing as message that property.

It works and, yes, it can be done better but, for today, I am very happy!

Done!


So, what a great day! I managed to squeeze two learning days in one and catch up a tiny bit. You can find the code for all this and for the three challenges at this link for GitHub.

Please don’t forget to drop a hello and a thank you to Paul for all his great work (you can find him on Twitter) and be sure to visit the 100 Days Of Swift initiative page. We are learning so much thanks to him and he deserves to know of our gratitude.

He has about 20 great books on Swift, all of which you can check about here.

The 100 Days of Swift initiative is based on the Hacking with Swift book, which you should definitely check out.


If you like what I’m doing here please consider liking this article and sharing it with some of your peers. If you are feeling like being really awesome, please consider making a small donation to support my studies and my writing (please appreciate that I am not using advertisement on my articles).

If you are interested in my music engraving and my publications don’t forget visit my Facebook page and the pages where I publish my scores (Gumroad, SheetMusicPlus, ScoreExchange and on Apple Books).

You can also support me by buying Paul Hudson’s books from this Affiliate Link.

Anyways, thank you so much for reading!

Till the next one!

Published by Michele Galvagno

Professional Musical Scores Designer and Engraver Graduated Classical Musician (cello) and Teacher Tech Enthusiast and Apprentice iOS / macOS Developer Grafico di Partiture Musicali Professionista Musicista classico diplomato (violoncello) ed insegnante Appassionato di tecnologia ed apprendista Sviluppatore iOS / macOS

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: