Day 43 - 100 Days of Swift

5 minute read

Project 10 (part 2)

Day 43 is the second part of the tenth project. Today you add the ability for the user to pick a photo to add to the collection view. You add a new Person class, to encapsulate the data that makes up a person (i.e. name and image). And you add the ability for the user to rename the person that they tap on.

First you add a button that will present a UIImagePickerController for the user to add a photo:

1
2
3
4
5
6
7
8
9
10
11
12
// In viewDidLoad
navigationItem.leftBarButtonItem =
    UIBarButtonItem(barButtonSystemItem: .add,
                    target: self,
                    action: #selector(addNewPerson))

@objc func addNewPerson() {
    let picker = UIImagePickerController()
    picker.allowsEditing = true
    picker.delegate = self
    present(picker, animated: true)
}

In order to set ViewController as the delegate for the image picker controller, we need for it to adopt the UIImagePickerControllerDelegate protocol, and in order to adopt that, we first have to adopt the UINavigationControllerDelegate protocol:

1
2
3
4
class ViewController:
    UICollectionViewController,
    UINavigationControllerDelegate,
    UIImagePickerControllerDelegate {

Now that ViewController is the delegate, we can add a method that will get called when the user picks an image:

1
2
3
4
5
6
7
8
9
10
11
12
func imagePickerController(_ picker: UIImagePickerController,
                           didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
    guard let image = info[.editedImage] as? UIImage else { return }

    let imageName = UUID().uuidString
    let imagePath = getDocumentsDirectory().appendingPathComponent(imageName)

    if let jpegData = image.jpegData(compressionQuality: 0.8) {
        try? jpegData.write(to: imagePath)
    }
    dismiss(animated: true)

We do several things here. We get the image out of the dictionary and make sure that it can be cast as a UIImage. If it can, we generate a name to save our own copy of it, and get the URL for that name. Then we try to get a jpeg version of the photo and if we can, we try to write it to disk, and then dismiss the view.

The helper function for getting the documents directory looks like this:

1
2
3
4
private func getDocumentsDirectory() -> URL {
    return FileManager.default.urls(for: .documentDirectory,
                                    in: .userDomainMask).first!
}

With that, the user can pick a photo and we can write it into our app’s space on the disk, but it isn’t displayed yet. To do that we add a Person class:

1
2
3
4
5
6
7
8
9
class Person: NSObject {
    var name: String
    var image: String

    init(name: String, image: String) {
        self.name = name
        self.image = image
    }
}

And add an array of Persons to ViewController:

1
var people: [Person] = []

Then we add a few lines to imagePickerController(_:didFinishPickingMediaWithInfo:) to add a new Person from the image to the people array:

1
2
3
4
5
// Right before dismiss()
let person = Person(name: "Unknown",
                    image: imageName)
people.append(person)
collectionView.reloadData()

Then we just need to update the collection view to actually pull from that data:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// In numberOfItemsInSection
return people.count

// In cellForItemAt
let person = people[indexPath.item]

cell.nameLabel.text = person.name

let path = getDocumentsDirectory().appendingPathComponent(person.image)
cell.imageView.image = UIImage(contentsOfFile: path.path)

cell.imageView.layer.borderColor = UIColor(white: 0, alpha: 0.3).cgColor
cell.imageView.layer.borderWidth = 2
cell.imageView.layer.cornerRadius = 3
cell.layer.cornerRadius = 7

Now the collection view displays the added images, but the names all say “Unknown” and there is no way to change them. To fix that you adopt a UICollectionViewDelegate method to present an alert asking the user to provide a name whenever they tap on a cell:

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
override func collectionView(_ collectionView: UICollectionView,
                             didSelectItemAt indexPath: IndexPath) {
    let person = people[indexPath.item]

    let alertController = UIAlertController(title: "Rename Person",
                                            message: nil, preferredStyle: .alert)
    alertController.addTextField() { textField in
        textField.text = person.name == "Unknown" ? "" : person.name
        textField.placeholder = "Person's Name"
    }

    let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
    alertController.addAction(cancelAction)

    let saveAction = UIAlertAction(title: "Ok", style: .default)
    { [weak self, weak alertController] _ in
        guard let newName = alertController?.textFields?.first?.text else { return }
        person.name = newName

        self?.collectionView.reloadItems(at: [indexPath])
    }

    alertController.addAction(saveAction)
    present(alertController, animated: true)
}

With that, the basic features of the app are complete:

Screenshot of working app.

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

Reflections

It looks a lot more interesting today, now that we can actually add our own data to the app. It is even usable at this point for the core function of helping you to associate names and faces. It doesn’t look great though, and it is kind of limited in that you have to have a picture of the person in your photo library before you launch the app, but maybe those are things I can tackle in the challenge day tomorrow.