Turning Swift compile-time safety into safety for your users

Sometimes good coding practices lead to good UX

What happened?

Technically, today nothing stops you as a developer from unintentionally or accidentally making irreversible changes (e.g. deleting user data) without confirmation from the user. Of course, we try to mitigate this risk as much as possible, writing UIAlertController code all over the place (or even making cute convenience closure-based functions for this purpose), but the APIs we write do nothing to prevent this from happening.

For example, let’s imagine we have a class Images which is being injected straight to our view controllers

final class Images {

private var images: [UIImage]

func image(at index: Int) -> UIImage {
return images[index]
}

func add(_ image: UIImage) {
images.append(image)
}

func delete(imageAt index: Int) {
images.remove(at: index)
}

}

And when a user taps “Delete” button, we can easily write this:

@objc func didPressDeleteButton(sender: UIButton) {
images.delete(imageAt: currentImageIndex)
}

No compile-time error (obviously), no warnings, nothing — and so we’ve just deleted a piece of user data without any confirmation. This is bad.

What’s the conventional solution for this?

Most of the time the developer would try to approach this problem at the view layer — aka creating and displaying UIAlertController right there, at didPressDeleteButton. This, of course, solves the problem most of the time, but it has its obvious disadvantages:

  • The developer can just forget to write this “security measure” code in some places.
  • Despite that, the developer still can accidentally delete the image not at the view layer, but somewhere in the business logic — without thinking twice about it.

So what’s the “right” solution?

What we really want is our Images class simply not allowing to perform a delete without user confirmation. To help us achieve that functionality, let’s create a UserConfirmationRequired struct:

struct UserConfirmationRequired {

private let performDestructiveAction: () -> ()

init(destructiveAction: @escaping () -> ()) {
self.performDestructiveAction = destructiveAction
}

func performWithUserConfirmation(alertTitle: String, alertMessage: String, alertDestructiveActionTitle: String, completion: @escaping (Bool) -> ()) {

// retrieving view controller to show alert from
        guard let window = UIApplication.shared.delegate?.window else {
print("No window")
completion(false)
return
}
guard let viewController = window?.rootViewController else {
print("No view controller")
completion(false)
return
}

// creating and showing an alert
        let alert = UIAlertController(title: alertTitle, message: alertMessage, preferredStyle: .actionSheet)

let cancel = UIAlertAction(title: "Cancel", style: .cancel, handler: { _ in completion(false) })

let destructive = UIAlertAction(title: alertDestructiveActionTitle, style: .destructive, handler: { _ in
self.performDestructiveAction()
completion(true)
})

alert.addAction(cancel)
alert.addAction(destructive)
viewController.present(alert, animated: true)
}

}

Phew, that’s a lot going on here. So, UserConfirmationRequired is basically a wrapper around () -> () closure that simply makes impossible to perform an action without gaining a user confirmation first. The part where we grab a view controller from UIApplication.shared looks kinda singleton-ish, but that actually makes sense here.

Then, let’s modify our Images class:

func deleteAction(ofImageAt index: Int) -> UserConfirmationRequired {
return UserConfirmationRequired(destructiveAction: {
self.images.remove(at: index)
})
}

Instead of deleting an image right away, we return UserConfirmationRequired instance — so the caller would need to gain a user confirmation in order to perform a destructive action.

And now, if we try to just write as we used to, we get a warning:

Signalizing to us that there’s more to be done. As you guess, now we have to ask for a user confirmation

@objc func didPressDeleteButton(sender: UIButton) {
let title = "Delete an image"
let message = "This action can't be undone. Are you sure?"
let delete = "Delete"
images.deleteAction(ofImageAt: currentImageIndex).performWithUserConfirmation(alertTitle: title, alertMessage: message, alertDestructiveActionTitle: delete) { (deleted) in
print("Deleted:", deleted)
}
}

And now, we get a nice alert:

Compile-time safety just saved our day.

The main advantage of UserConfirmationRequired is that you just can’t unintentionally perform a “destructive” action without asking a user first — regardless of place you try to do that.


I think it’s pretty amazing that we can use the power of a strong type system not only to make our code safer for developers, but also to make our apps safer from a user perspective.

Thanks for reading the post! Don’t hesitate to ask or suggest anything in the “responses” section below. You can also contact me on Twitter or find me on GitHub. If you have written a piece (or stumbled upon one) exploring similar topic — make sure to post a link to it in the responses so I can include it right below.


Hi! I’m Oleg, the author of Women’s Football 2017 and an independent iOS/watchOS developer with a huge passion for Swift. While I’m in the process of delivering my next app, you can check out my last project called “The Cleaning App” — a small utility that will help you track your cleaning routines. Thanks for your support!