Day 83 - 100 Days of Swift

7 minute read

Project 25 (part 1)

Day 83 is the first part of the twenty-fifth project. It is similar to project 10 in that it is an app where you add pictures to a collection view, but the difference is that this time they are shared via multi-peer connectivity to everyone in your local session. You quickly get the collection view and photo adding part set up, then you add the ability to host or join a multi-peer connectivity session, and then you add the ability to actually share the image data over that session.

For the first part, you just add a UICollectionViewController in the storyboard, set the cell to be 145x145 and give it a reuse identifier, make the insets 10 on all sides, and add an image view with the tag 1000.

Screenshot of storyboard

Then you make ViewController subclass from UICollectionViewController instead of UIViewController. You add bar button for sharing an image and set the title in viewDidLoad:

1
2
3
4
5
6
// In viewDidLoad
title = "Selfie Share"
let cameraButton = UIBarButtonItem(barButtonSystemItem: .camera,
                                   target: self,
                                   action: #selector(importPicture))
navigationItem.rightBarButtonItem = cameraButton

You add an array of images and make the collection view’s number of items be based off of that array:

1
2
3
4
5
6
var images: [UIImage] = []

override func collectionView(_ collectionView: UICollectionView,
                             numberOfItemsInSection section: Int) -> Int {
    return images.count
}

Then you build the cell in cellForItemAt using that tag to find the UIImageView

1
2
3
4
5
6
7
8
9
10
11
override func collectionView(_ collectionView: UICollectionView,
                             cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ImageViewCell",
                                                  for: indexPath)

    if let imageView = cell.viewWithTag(1000) as? UIImageView {
        imageView.image = images[indexPath.item]
    }

    return cell
}

Finally, you add the methods to present the image picker and handling when an image is picked:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@objc func importPicture() {
    let picker = UIImagePickerController()
    picker.allowsEditing = true
    picker.delegate = self
    present(picker, animated: true)
}

func imagePickerController(_ picker: UIImagePickerController,
                           didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
    guard let image = info[.editedImage] as? UIImage else { return }

    dismiss(animated: true)

    images.insert(image, at: 0)
    collectionView.reloadData()
}

At this point, the app runs and you can pick images from your own library to display in the collection view, much like project 10.

For the second part, you add another bar button item which will present an action sheet with options to either host or join a session:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let addButton = UIBarButtonItem(barButtonSystemItem: .add,
                                target: self,
                                action: #selector(showConnectionPrompt))
navigationItem.leftBarButtonItem = addButton

@objc func showConnectionPrompt() {
    let ac = UIAlertController(title: "Connect to others",
                               message: nil,
                               preferredStyle: .alert)
    ac.addAction(UIAlertAction(title: "Host a session",
                               style: .default,
                               handler: startHosting))
    ac.addAction(UIAlertAction(title: "Join a session",
                               style: .default,
                               handler: joinSession))
    ac.addAction(UIAlertAction(title: "Cancel",
                               style: .cancel))
    present(ac, animated: true)
}

Then you import Multipeer Connectivity and add a few variables to keep track of some stuff, and set up the session in viewDidLoad:

1
2
3
4
5
6
7
8
9
10
11
12
import MultipeerConnectivity

var peerID = MCPeerID(displayName: UIDevice.current.name)
var mcSession: MCSession?
var mcAdvertiserAssistant: MCAdvertiserAssistant?

// In viewDidLoad
mcSession = MCSession(peer: peerID,
                      securityIdentity: nil,
                      encryptionPreference: .required)

mcSession?.delegate = self

Then you add the functions used by the alert actions for hosting and joining a session:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private func startHosting(action: UIAlertAction) {
    guard let mcSession = mcSession else { return }
    mcAdvertiserAssistant = MCAdvertiserAssistant(serviceType: "hws-project25",
                                                  discoveryInfo: nil,
                                                  session: mcSession)
    mcAdvertiserAssistant?.start()
}

private func joinSession(action: UIAlertAction) {
    guard let mcSession = mcSession else { return }
    let mcBrowser = MCBrowserViewController(serviceType: "hws-project25",
                                            session: mcSession)
    mcBrowser.delegate = self
    present(mcBrowser, animated: true)
}

For the third part, you make ViewController adopt the protocols MCSessionDelegate and MCBrowserViewControllerDelegate and add the required methods for those. Three of the methods you just leave empty because you don’t need them for the functionality of this app, two you just add dismiss calls, to dismiss a view controller, one just has some print statements for debugging, and one is used to add build an image and add it to the array when data is received:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
extension ViewController: MCSessionDelegate, MCBrowserViewControllerDelegate {
    func session(_ session: MCSession,
                 peer peerID: MCPeerID,
                 didChange state: MCSessionState) {

        switch state {
        case .connected:
            print("Connected: \(peerID.displayName)")

        case .connecting:
            print("Connecting: \(peerID.displayName)")

        case .notConnected:
            print("Not Connected: \(peerID.displayName)")

        @unknown default:
            print("Unknown state received: \(peerID.displayName)")
        }
    }

    func session(_ session: MCSession,
                 didReceive data: Data,
                 fromPeer peerID: MCPeerID) {

        DispatchQueue.main.async { [weak self] in
            if let image = UIImage(data: data) {
                self?.images.insert(image, at: 0)
                self?.collectionView.reloadData()
            }
        }
    }

    func session(_ session: MCSession,
                 didReceive stream: InputStream,
                 withName streamName: String,
                 fromPeer peerID: MCPeerID) {

    }

    func session(_ session: MCSession,
                 didStartReceivingResourceWithName resourceName: String,
                 fromPeer peerID: MCPeerID,
                 with progress: Progress) {

    }

    func session(_ session: MCSession,
                 didFinishReceivingResourceWithName resourceName: String,
                 fromPeer peerID: MCPeerID,
                 at localURL: URL?,
                 withError error: Error?) {

    }

    func browserViewControllerDidFinish(_ browserViewController: MCBrowserViewController) {
        dismiss(animated: true)
    }

    func browserViewControllerWasCancelled(_ browserViewController: MCBrowserViewController) {
        dismiss(animated: true)
    }
}

Finally, you just add a simple method for sending the image data when the user selects an image:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private func sendImage(_ image: UIImage) {
    guard let mcSession = mcSession else { return }
    guard mcSession.connectedPeers.count > 0 else { return }
    guard let imageData = image.pngData() else { return }

    do {
        try mcSession.send(imageData,
                           toPeers: mcSession.connectedPeers,
                           with: .reliable)
    } catch {
        let ac = UIAlertController(title: "Error Sending Image",
                                   message: error.localizedDescription,
                                   preferredStyle: .alert)
        ac.addAction(UIAlertAction(title: "OK",
                                   style: .default))
        present(ac, animated: true)
    }
}

// At the end of didFinishPickingMediaWithInfo
sendImage(image)

With that, the app works. You can start a session on one device, join it from another and then any photos you add on one will show up on the other.

You can find my version of this project at the end of day 83 on Github here.