Angelo Stavrow [dot] Blog

Missives and musings on a variety of topics.

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.

Slow down and sleep

To test what unit type (if any) the list should be limited to, I changed the private _unitType property in the ProductList to

private(set) var unitType: Unit?

This value is set when the first ProductItem is added to the list. Then I can add it as a property with the same access level to the ProductDetailContentViewController:

protocol ProductDetailContentViewControllerDelegate: AnyObject {
    var numberOfProductItems: Int { get }
    // Other protocol stuff
}

This exposes it to the view controller's subviews, but here's something I was perplexed by. In the view controller's viewDidLoad() method, I (via a call to a setupViews() method) instantiate a subview, and set its delegate. But when I was trying to access that delegate from within the subview's initializer, it would return nil. It took me longer than I care to admit to realize that the delegate isn't initialized until after my subview is:

//... other viewDidLoad() stuff in the view controller
productDetailFormView = new ProductDetailFormView(frame: .zero);
productDetailFormView.delegate = self

In retrospect, this is obvious. You've got to wait until the view's initializer does its thing, and then the view's delegate is set — so don't try to access it in the view's initializer.

What's fascinating to me is that —while I know all this— it was incomprehensible why this was happening while I was in the thick of programming, until I gave up and took a break. Why? Probably because I had a lousy night's sleep, and was rushing to get something done early this morning.

The takeaway: get a good night's sleep, and don't rush your work.

Tomorrow, I continue working on this.

#per #perRewriteDiary #ios

Discuss...

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.

The plan

The shipping version of Per pre-dates the Units and Measurement libraries, so I had to implement all unit conversion carefully myself. Now that it's built into Foundation, I don't have to do that work anymore (thanks Apple!) and instead can focus on the app itself.

Unit conversion itself is handled in the ProductItem model, specifically in the pricePerUnit computed property (as discussed on day 4). Unit-matching enforcement (i.e., so the user doesn't try to compare price-per-pound by price-per-litre) is handled by the ProductList (as discussed on day 5). All this validation is happening at the model layer, so then it's just a question of implementing the UI to do the following:

  1. When adding the first product, an additional bit of UI to select between weight units, volume units, or dimensionless units. In the shipping version, this is a segmented control, and that's probably fine here as well.
  2. Once selected, the units input field (currently a disabled text field) becomes some kind of control where you can choose the units. In the shipping version, this is implemented as a UIPicker, but there's just something about that control I don't like. Maybe some kind of popover?
  3. In any other products added, the UI should default to only showing the the units that match those of the first product. That's easy enough to check for, via the ProductList's _unitType optional property. It's currently marked as private, but that's not really necessary for the getter — only the setter.

Tomorrow, I get cracking on this work!

#per #perRewriteDiary #ios

Discuss...

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.

Nicer table view cells

Yesterday, I mentioned that each cell should show how much more you're paying over the best-value option. I got that all implemented today, and here's how I did it.

First, I added a property to the ProductList called bestValue that's updated with the lowest pricePerUnit in the array of ProductItems when we add to it. It's a pretty straightforward affair:

mutating func add(_ item: ProductItem, sort: Bool = false) throws {
	// ...the rest of the function
	bestValue = _products.sorted().first?.pricePerUnit ?? 0.0
}

I also added a couple of properties in my ProductListCellView:

  • a valueForMoney string that, when updated, sets a UILabel
  • an isBestValue flag that, when updated, sets the color of the above label's text

I set these in the ProductListContentViewController when adding a new cell, but after checking whether this particular product we're adding to the list is the best-value option:

if (productItem.pricePerUnit == productList.bestValue) {
    cell.valueForMoney = "Best value!"
    cell.isBestValue = true
} else {
    cell.valueForMoney = "You're paying \(Int(100 * (productItem.pricePerUnit - productList.bestValue) / productList.bestValue))% more!"
    cell.isBestValue = false
}

Here's what it looks like now:

"A screen capture of the iPhone Simulator showing a list of products compared by price per unit"

Much better! I'm hard-coding the currency symbol, which is bad — I need to add a proper formatter for the quantity and price values, and handle localization (something that the current shipping version of Per doesn't do, which is also bad). I've opened an issue for this.

A display bug

Something that irks me in the above screenshot is a display bug that I don't quite understand how to resove. You can see empty cells in the table, and that looks ugly. I'd rather they be hidden.

According to all of my research, when the table view is empty, I should be able to hide any empty cells by setting:

productTableView.tableFooterView = UIView(frame: .zero)

That… doesn't work, and I'm not sure why. If you've got any ideas, let me know!

Tomorrow, I'll start work on unit conversion!

#per #perRewriteDiary #ios

Discuss...

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.

Design Thinking

There's not really much to report today. I spent the day thinking about how to best present information in each ProductListCellView so that it's easily understood; right now ProductItems are sorted in the list by best-value, but there's no indication of how much better the value is. I'm experimenting with some ideas and I think I've got an idea, though that'll mean adding something to the ProductList that stores either the best or worst value for your money, and updates it with each new product that's added.

Then, each cell could show how much worse (or better) it is than that baseline value. So I'll tackle that tomorrow.

#per #perRewriteDiary #ios

Discuss...

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.

Custom Table View Cells

Right now, the ProductListContentViewController is using a very basic UITableViewCell to show basic details of each ProductItem entry. That's fine, but it's going to get messy when that cell gets a custom layout. Better to contain that in a separate view!

So, to begin with, I just wanted to refactor out that code into a new class. Creating one that conformed to UITableViewCell gave me some stubbed methods to add, and I added some properties to the class. First, a UILabel that I can later customize:

private let productDetailLabel: UILabel = {
    let label = UILabel()
    return label
}()

Then a product that, when set, updates the productDetailLabel:

var product: ProductItem {
    didSet {
        let productUnits = product.units?.symbol ?? "unit"
        productDetailLabel.text = "\(product.quantity) \(productUnits)s for \(product.price) costs \(product.pricePerUnit) per \(productUnits)"
    }
}

Right now, this means that I have to initialize the product property with some default value when it's initialized:

override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
    self.product = ProductItem(price: 1.00, quantity: 1.99, units: nil)
    super.init(style: style, reuseIdentifier: reuseIdentifier)
    addSubview(productDetailLabel)
    setupConstraints()
}

The setupContstraints() method does the same thing that it does in other UIViews — sets the view to not translate its autoresizing mask into constraints, and then activates the constraints I want. Pretty straightforward. Then it's just a question of using this custom cell in ProductListContentViewController instead of the default style:

override func viewDidLoad() {
    // The other viewDidLoad stuff is done here
    // [...]
    // Then the custom cell is registered:
    productTableView.register(ProductListCellView.self, forCellReuseIdentifier: "productListCell")
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let productItem = productList.getItems()[indexPath[1]]
    let cell = productTableView.dequeueReusableCell(withIdentifier: "productListCell", for: indexPath) as! ProductListCellView
    cell.product = productItem
    return cell
}

That's it! Something that I tend to forget when laying things out in code is to set translatesAutoresizingMaskIntoConstraints to false so that everything works as expected, but I'm getting the hang of doing that first in any setupConstraints() method now.

Tomorrow, I'll customize this a bit further with multiple lines.

#per #perRewriteDiary #ios

Discuss...

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...

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.

Refactoring layout code

I'm a little short on time today, so I'll grab some low-hanging fruit for a quick win. Yesterday, I wrote that “the ProductDetailContentViewController is kinda sloppy — some of its UI is from an embedded custom view, and some of it is created in viewDidLoad(), which should at least be refactored out into separate setupView() and setupConstraints() methods.”

That's easy enough. I'm marking those setup functions as private, and taking advantage of NSLayoutConstraint.activate() for setting up and activating constraints. I don't think it's necessarily less readable than chaining .isActive = true to the end of each constraint call, but as an array I can fold the code in the editor so it takes up less space, which I something I take advantage of.

So that's one papercut fixed! Just over a week to go before the end of the month; let's see how close I can get to the current shipping features! Tomorrow I'll tackle one of the FIXME issues — specifically, making ProductList.add() throw on trying to add a mismatched-unit ProductItem.

#per #perRewriteDiary #ios

Discuss...

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.

A List of PaPerCuts

We're talking about “papercuts” and the name of the app (“Per”) is in “papercut” — get it?

Okay, sorry — bad joke. What do I mean by papercuts? These are the little rough edges and tiny bugs that make it annoying to use something. Pile up enough of them, and you have a death by a thousand papercuts — the failure of an effort due to a multitude of fairly minor bits of unpleasantness.

Models

The ProductItem and ProductList models feel fairly complete, but the list model only prints a message to the console if you try to add a product item whose units don't match what's already in the list (e.g., adding a product whose units are grams to a list full of items that are otherwise sold by volume). It should instead, throw an error.

Coordinator View Controllers

ProductListCoordinatorViewController takes advantage of extensions on both UIViewController and UIView to embed a child view controller into the full frame of the parent view controller. I'll be on the lookout for other places where I can use that. Otherwise, this is a tiny, 20-line view controller. Perfect.

Context View Controllers

The only context view controller is ProductListContextViewController, which also leverages the same extensions that the coordinator does. It's also a fairly small view controller, only 56 lines, but I want to rethink the use of UIBarButtonItems for adding to and clearing the list of products — this could be refactored into some kind of child view.

Content View Controllers

The ProductListContentViewController is a table view controller using a very basic cell to display results; that should absolutely be refactored into a custom view.

The ProductDetailContentViewController is kinda sloppy — some of its UI is from an embedded custom view, and some of it is created in viewDidLoad(), which should at least be refactored out into separate setupView() and setupConstraints() methods. The Add button should only be enabled if there's text in both the quantity and price fields for now.

There's also maybe something to think about in combining or re-using the UI from the add-item/clear-slist UIBarButtonItems and the Add/Cancel buttons in the ProductDetailContentViewController.

Custom Views

Finally, we come to the ProductDetailFormView. This one needs work — if I want to enable/disable the Add button based on how much of the form is filled out, I'll need to do more UITextFieldDelegate work, which means duplication a lot of the logic that identifies which text field is being edited. That's not too hard to refactor.

I'm still setting aside any handling of units until I better understand how it works in practice. But it would be helpful to start adding some formatting for the textfields, especially currency. It feels like it's worth refactoring any text field delegation into a separate class here, to better encapsulate these changes.

Other Thoughts

There are comments that explain things that probably don't need to be explained in the body of a code block, and should instead be made part of the function's documentation.

There are also no tests, whatsover.

I'm not adding these as papercuts, though — that's a whole other thing to tackle.

Working down the list

Xcode's jump bar (or whatever it's called) along the top of the actual code-editing view in the Editor is really useful for this kind of work; if you use comments that start with // TODO: or // FIXME:, you'll see them listed there among your functions, letting you navigate to them very quickly.

"The Xcode jump bar shows FIXME and TODO comments"

Alternatively, you can mark them as #warning("TODO: Description of task") or #error("FIXME: Description of problem") to have these come up as either a ⚠️ warning or an 🛑 error in the Issue navigator.

"Xcode's Issues navigator shows comments marked as #warning or #error"

It feels awkward to me to set these as compiler warnings —that's absolutely not what they are— so I'm sticking with the comments. Instead, I'm going to add them as issues to work on.

Tomorrow, I start working on this list!

#per #perRewriteDiary #ios

Discuss...

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.

Whoopsie

So the plan today was to pause on writing code, and do what I think of as “catch-up” work. There are some inconsistencies among, for example, content view controllers — some have all kinds of view layout and initialization code in viewDidLoad(), others dump that stuff into a child UIView with setupView() and setupConstraints() methods. Pushing through on a daily basis with spike solutions and experiments makes for a lot of forward momentum, but it's important to take a step back and make sure you're tidying as you go — hence the list of 'papercuts,' or little issues that aren't a big deal on their own, but a real problem if they're left to accumulate.

Aside: Remember that this is a fairly small and simple app being built by a single person, so it really doesn't need a fancy and overcomplicated methodology. Experimenting and 'trying silly ideas' are not only allowed, they're encouraged.

So, that was the plan. But overnight I realized that something felt… unsettled in my brain. Every time I create a new form view, am I making sure the memory is being deallocated when it's dismissed?

I thought so — but you know that feeling. The one telling you that you've probably missed something.

So today I sat down and fired up Instruments, watching allocations as I navigate in and out of the add-product form. Sure enough, every time I present it, we get a ProductDetailContentViewController being allocated, but not being de-allocated.

Well, crap.

Who's got two thumbs and didn't give a delegate a weak reference? This guy. This, as you may know, creates a retain cycle, where the view controller can't be destroyed because it's got a strong reference to its delegate object. So, okay, add a weak keyword and we're done, right?

🚫🚫🚫🚫🚫

Nope. Xcode refuses to compile the code and gives me the following error if I try that:

'weak' must not be applied to non-class-bound
 'ProductDetailContentViewControllerDelegate'; 
 consider adding a protocol conformance that has a class bound.

Oh. Okay… so what does that mean? I asked Frank about it, and here's what he explained:

“Make your protocol inherit from AnyObject. Essentially, the compiler is making sure your weak variable is a reference type and not a value type, because a weak value type doesn’t make sense.”

And yes, making the ProductDetailContentViewControllerDelegate protocol conform to AnyObject fixed the issue. This was a case of me looking for the issue somewhere that was just far enough removed from the actual problem, that I couldn't see the fairly obvious solution.

A thing I'm noticing: if you feel like you're fighting the language/compiler, asking questions like “why doesn't removeFromParent(), y'know, remove from parent?” — step back. Re-evaluate and make sure you're asking the right question.

Oh yeah, and one final bit: why doesn't a weak value type make sense? Well, weak or strong is in relation to the reference one object has to another. You can't have a reference to a value type — you don't point to them, you copy them. You can only have a reference to the aptly-named reference type.

So, okay. Tomorrow, we're back to the papercuts.

#per #perRewriteDiary #ios

Discuss...

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.

Of delegates and datasources

The ProductDetailFormView has a set of three UITextFields for entering details (quantity, price, and units) when the user wants to add a new product. Figuring out which text field has been updated is easy enough — give them each a tag and then in the UITextFieldDelegate's textFieldDidEndEditing() method, check for the tag and... do what with it? It's possible that the user go back and make changes so it's helpful to have a temporary way to track the latest value from the form.

That temporary object can then be read from and turned into a new ProductItem object when the user hits the Add button. So how do I set it up?

I started by creating a simple VolatileFormData struct in the ProductDetailContentViewController:

struct VolatileFormData {
    var price: String
    var units: String
    var quantity: String
}

Then, I give the ProductDetailFormView a datasource property:

class ProductDetailFormView: UIView, UITextFieldDelegate {
  var delegate: ProductDetailContentViewControllerDelegate?
  var datasource: VolatileFormData?

  // The rest of the class implementation goes here
}

Change the ProductDetailContentViewControllerDelegate to have an updateVolatileFormData() method that can be called when a text field's editing-ended event is triggered with the new values, and when the Add button is tapped, a new ProductItem will be created from the (parsed String) data in the VolatileFormData struct. Right?

H*ckin' completion blocks

Not exactly. Why? Well, the action for the Add button looked something like this like this:

@objc func addButtonTapped(_ sender: UIButton!) {
  delegate.add(createProductFromFormData())
  self.dismiss(animated: true, completion: nil)
}

This means I'm trying to create the ProductItem based on what was in the temporary form-data object when the Add button was tapped, and then I dismiss the ProductDetailFormView.

So here's what happens in that case (keeping in mind that for now, units are not considered):

  1. User taps on the quantity field, enters a quantity
  2. User taps on the price field, the quantity field fires the editing-ended event, and volatileFormData is updated
  3. User enters price, taps on the Add button
  4. The view controller fires the addButtonTapped action, which tries to create the ProductItem
  5. If it succeeds, the view controller dismisses itself, so the price field fires the editing-ended event, and volatileFormData is updated

See the issue?

The form data doesn't get updated until after the view controller tries to create the product. But the dismiss(animated:) method includes an optional completion: block that can be run after the view controller dismisses itself. Seems like a good time to create the ProductItem, right?

So now the action looks like this:

@objc func addButtonTapped(_ sender: UIButton!) {
    self.dismiss(animated: true, completion: {
        self.delegate.add(self.createProductFromFormData())
    })
}

And thus the flow looks like this:

1. User taps on the quantity field, enters a quantity
2. User taps on the price field, the quantity field fires the editing-ended event, and `volatileFormData` is updated
3. User enters price, taps on the **Add** button
4. The view controller fires the `addButtonTapped` action, so the view controller prepares to dismiss itself
5. The price field fires the editing-ended event, and `volatileFormData` is updated
6. The view controller is now gone, so the completion block fires and creates the new `ProductItem`

Hurray!

I think this is a good time to stop and take stock of where the app is at. There's been a lot of forward progress, but it makes sense to have a look at the little paper cuts that are building up. Before going any further, I think it's worth reviewing all the code that's been written so far, and see how it can be cleaned up, re-organized, and —most importantly— thoroughly tested. So, tomorrow, I'm not writing any code; I'm going to create a list of TODOs, FIXMEs and HACKs.

#per #perRewriteDiary #ios

Discuss...

Enter your email to subscribe to updates.