Day 93 - 100 Days of Swift

5 minute read

Project 28 (part 2)

Day 93 is the second part of the twenty-eighth project. You review what you learned and then he gives you three challenges. He challenges you to add a “Done” button to the app, to let the user lock it themselves if they want. He challenges you to let the user set a password so they can fallback on that if they don’t have Face ID or Touch ID. And he challenges you to go back to project 10 and require the user to authenticate before the names and faces are shown.

For the first challenge I just added a bar button whenever the user successfully unlocked the message:

1
2
3
4
5
// In unlockSecretMessage
navigationItem.rightBarButtonItem =
    UIBarButtonItem(barButtonSystemItem: .done,
                    target: self,
                    action: #selector(saveSecretMessage))

And then removed it whenever they save it:

1
2
// In saveSecretMessage
navigationItem.rightBarButtonItem = nil

The second challenge was a little more involved. First, I added a method to set a password:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@objc private func setPassword() {
    let alertController = UIAlertController(title: "Choose a password",
                                            message: nil,
                                            preferredStyle: .alert)
    var passwordField: UITextField!
    alertController.addTextField { textfield in
        textfield.placeholder = "Your new password"
        textfield.isSecureTextEntry = true

        passwordField = textfield
    }

    let set = UIAlertAction(title: "Set Password",
                            style: .default) { _ in
        guard let password = passwordField.text else { return }
        KeychainWrapper.standard.set(password,
                                     forKey: Key.password)
    }

    alertController.addAction(set)
    alertController.addAction(.cancel)

    present(alertController, animated: true)
}

Then I added a method to unlock with a password:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@objc private func unlockWithPassword() {
    let alertController = UIAlertController(title: "What's the password?",
                                            message: nil,
                                            preferredStyle: .alert)
    var passwordField: UITextField!
    alertController.addTextField { textfield in
        textfield.placeholder = "Your password"
        textfield.isSecureTextEntry = true

        passwordField = textfield
    }

    let unlock = UIAlertAction(title: "Unlock",
                            style: .default) { _ in
        if passwordField.text ==  KeychainWrapper.standard.string(forKey: Key.password) ?? "" {
            self.unlockSecretMessage()
        }
    }

    alertController.addAction(unlock)
    alertController.addAction(.cancel)

    present(alertController, animated: true)
}

Then I just called those methods in the appropriate places:

1
2
3
4
5
6
7
8
9
10
11
12
// In unlockSecretMessage
navigationItem.leftBarButtonItem =
    UIBarButtonItem(title: "Set Password",
                    style: .plain,
                    target: self,
                    action: #selector(setPassword))

// In saveSecretMessage
navigationItem.leftBarButtonItem = nil

// In authenicate, if biometry isn't available
unlockWithPassword()

With that, it looks like this (for some reason, it did not record the keyboard while typing in the password, I am guessing because they are a secure entry fields, but in the gif I (1) set a password, (2) enter the wrong password and (3) enter the right password):

Gif of project 28

For the third challenge, to keep things relatively simple, I just made a view that covers everything else up until the user has authenticated:

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
lazy var lockedView: UIView = {
    let view = UIView()
    view.backgroundColor = .white
    view.translatesAutoresizingMaskIntoConstraints = false

    let button = UIButton(type: .system)
    button.setTitle("Authenticate", for: .normal)
    button.addTarget(self,
                     action: #selector(authenticate),
                     for: .touchUpInside)

    button.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(button)

    NSLayoutConstraint.activate([
        button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
        button.centerYAnchor.constraint(equalTo: view.centerYAnchor)
    ])

    return view
}()

private func setupLockView() {
    view.addSubview(lockedView)
    NSLayoutConstraint.activate([
        lockedView.topAnchor.constraint(equalTo: view.topAnchor),
        lockedView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        lockedView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
        lockedView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
    ])
}

I added a couple of methods to lock and unlock the view:

1
2
3
4
5
6
7
8
9
10
11
12
13
@objc private func unlockView() {
    navigationItem.rightBarButtonItem =
        UIBarButtonItem(barButtonSystemItem: .add,
                        target: self,
                        action: #selector(addNewPerson))

    lockedView.isHidden = true
}

@objc private func lockView() {
    navigationItem.rightBarButtonItem = nil
    lockedView.isHidden = false
}

Then I added the authenticate method which is almost identical to the one from Project 28:

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
@objc func authenticate() {
    let context = LAContext()
    var error: NSError?

    if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics,
                                 error: &error) {
        let reason = "Use your fingerprint to unlock your saved faces."

        context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics,
                               localizedReason: reason) {
            [weak self] success, authenticationError in

            DispatchQueue.main.async {
                if success {
                    self?.unlockView()
                } else {
                    let title = "Authentication failed"
                    let message = "You could not be verified; please try again."
                    self?.presentErrorAlert(title: title,
                                            message: message)
                }
            }
        }
    } else {
        let title = "Biometry unavailable"
        let message = "Your device is not configured for biometric authentication."
        self.presentErrorAlert(title: title,
                               message: message)
    }
}

Finally, I added an observer to make sure the view gets locked whenever the application will resign active:

1
2
3
4
NotificationCenter.default.addObserver(self,
                                       selector: #selector(lockView),
                                       name: UIApplication.willResignActiveNotification,
                                       object: nil)

With that, it looks like this:

Gif of project 10

You can find my version of these projects at the end of day 93 on Github here.