Turning Swift compile-time safety into safety for your users
By: Oleg Dreyman
Tags: Uikit, Articles, Swift, Safety, iOS

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: