The Per Rewrite Diaries: Day 22

This post is part of a series about rewriting my iOS app, Per. Per is a price per unit comparison app with a bunch of neat convenience figures, but it hasn't been updated in years, so I'm rewriting it from scratch to eliminate a bunch of technical debt. Just because it's not an open-source app doesn't mean I can't share what I learn as I go!

See the rest of the series here.

Marking As Throw

Today's work involves a FIXME for one of my biggest software pet peeves: silent failures. If I try to add a ProductItem with, say, kilogram units to a ProductList of sold-by-volume ProductItems (millilitres, pints, and such), that shouldn't work. The UI will protect against this by pre-selecting available units, but the product list doesn't (and shouldn't) know this — it should protect against this by throwing an error if someone tries this particular operation.

So first, I set up an enum that conforms to Error:

enum ProductListError: Error {
    case units_mismatch
}

Then replaced the current functionality of printing an error to the console:

print("Can't add an item with type \(String(describing: incomingUnitType)) to a list of \(String(describing: _unitType)) items")

with the throwing of an error:

throw ProductListError.units_mismatch

Great! Now that the bad path throws an error, I can handle it at the call sites.

Do Try Catch

To do this is pretty straight forward; just wrap the call to the throwing function in a do-try-catch block. In the ProductListContextViewController, we have an add() method that's called by the ProductDetailContentViewController via the delegation pattern:

func add(_ item: ProductItem) {
    do {
        try contentViewController.productList.add(item, sort: true)
        clearListBarButtonItem?.isEnabled = true
        contentViewController.loadView()
    } catch ProductListError.units_mismatch {
        print("Can't add an item with type \(String(describing: item.units)) to the list.")
        return
    } catch {
        print("Something went wrong adding \(item); abandoning the attempt.")
        return
    }
}

This will try to add the item to the product list; if that fails, it'll catch the error thrown, print an error to the console, and return.

Yes, this is technically still failing silently. But now that it's at the UI layer, I can add a simple alert for now and use that instead of the call to print():

private func displayError(_ message: String) {
    let alert = UIAlertController(title: "Whoops!", message: message, preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Default action"), style: .default, handler: { _ in
      NSLog(message)
    }))
    self.present(alert, animated: true, completion: nil)
}

Now we're failing verbosely! I'm adding a TODO here to refactor this spike solution for the alert into an extension on UIViewController, because our time for today is up.

Tomorrow, I'm going to start work on a custom table view cell for the product list.

#per #perRewriteDiary #ios

Discuss...