Day 92 - 100 Days of Swift

4 minute read

Project 28 (part 1)

Day 92 is the first part of the twenty-eighth project. It is an encrypted note app which uses keychain to securely store encrypted notes, Touch ID/Face ID to authenticate the user, and only shows the contents of the notes if the user can be authenticated.

First, you set up a pretty simple view that mostly consists of a UITextView and a UIButton with the title “Authenticate”. You also add the same notification observers as in Project 19 for adjusting the insets of the text view when the keyboard is shown:

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
// In viewDidLoad
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self,
                               selector: #selector(adjustForKeyboard),
                               name: UIResponder.keyboardWillHideNotification,
                               object: nil)

notificationCenter.addObserver(self,
                               selector: #selector(adjustForKeyboard),
                               name: UIResponder.keyboardWillChangeFrameNotification,
                               object: nil)

@objc private func adjustForKeyboard(notification: Notification) {
    let key = UIResponder.keyboardFrameEndUserInfoKey
    guard let keyboardValue = notification.userInfo?[key] as? NSValue else {
                return
    }

    let keyboardScreenEndFrame = keyboardValue.cgRectValue
    let keyboardViewEndFrame = view.convert(keyboardScreenEndFrame,
                                            from: view.window)

    if notification.name == UIResponder.keyboardWillHideNotification {
        secret.contentInset = .zero
    } else {
        let bottom = keyboardViewEndFrame.height - view.safeAreaInsets.bottom
        secret.contentInset = UIEdgeInsets(top: 0,
                                               left: 0,
                                               bottom: bottom,
                                               right: 0)
    }

    secret.scrollIndicatorInsets = secret.contentInset

    let selectedRange = secret.selectedRange
    secret.scrollRangeToVisible(selectedRange)
}

Next, you add an open-source wrapper around the keychain conveniently called KeychainWrapper and use it in two functions to save and access the note:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private func unlockSecretMessage() {
    secret.isHidden = false
    title = "Secret stuff!"

    secret.text = KeychainWrapper.standard.string(forKey: Key.secretMessage)
}

@objc private func saveSecretMessage() {
    guard !secret.isHidden else { return }

    KeychainWrapper.standard.set(secret.text, forKey: Key.secretMessage)
    secret.resignFirstResponder()
    secret.isHidden = true
    title = "Nothing to see here"

}

// I chose to make an enum for holding the key,
// so I could get autocompletion and
// not risk misspelling a string.
enum Key {
    static secretMessage = "SecretMessage"
}

To make sure the message is always locked unless the user has successfully authenticated you call saveSecretMessage whenever the application will resign active:

1
2
3
4
5
// In viewDidLoad
notificationCenter.addObserver(self,
                               selector: #selector(saveSecretMessage),
                               name: UIApplication.willResignActiveNotification,
                               object: nil)

Finally, you only want to unlock the secret message if the user can authenticate themselves, and we want to use biometric authentication, so you first check that it is available, and then attempt to authenticate:

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
import LocalAuthentication

// In authenticate()
let context = LAContext()
var error: NSError?

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

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

        DispatchQueue.main.async {
            if success {
                self?.unlockSecretMessage()
            } 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)
}

Again, I added a couple of extensions to make presenting an alert a little cleaner:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
extension UIAlertAction {
    static let ok = UIAlertAction(title: "OK",
                                  style: .default)
}

extension ViewController {
    func presentErrorAlert(title: String, message: String?) {
        let ac = UIAlertController(title: title,
                                   message: message,
                                   preferredStyle: .alert)
        ac.addAction(.ok)
        self.present(ac, animated: true)
    }
}

With that, you just need to add a usage description to the Info.plist for Face ID and it all just works.

Gif of working app.

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