Angelo Stavrow [dot] Blog

perrewritediary

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.

Thinking Clearly

I'm only able to do a little bit today because reasons, but there's enough time to tackle the first of yesterday's goals for the week: a way to clear the product list.

First, the Product model needs a way to clear the list — Swift's removeAll() array method to the rescue!

mutating func clearItems() {
    _products.removeAll()
}

Then, I need a way to trigger this from the UI — and another UIBarButtonItem next to the add-product button should do it:

clearListBarButtonItem = UIBarButtonItem(barButtonSystemItem: .trash,
                                                      target: self,
                                                      action: #selector(handleClearListBarButtonItemTapped))

The handleClearListBarButtonItemTapped action is then pretty straightforward (remember from day 7 that the contentViewController is the one that wraps the table view):

@objc func handleClearListBarButtonItemTapped(sender: UIBarButtonItem) {
    contentViewController.productList.clearItems()
    contentViewController.loadView()
}

That's about it! I went back and sprinkled some .isEnabled here and there to make sure that the button is disabled if there's nothing in the list, and that was enough to call this done.

Tomorrow, I'll start work on a form for entering product item details in the ProductItemContentViewController, which will have me diving into unfamiliar territory: creating layout in code.

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

Thinking ahead

I mentioned yesterday that I'd work on understanding a Simulator bug that I'm seeing when dismissing a detail view controller today. I'm stepping back from writing code today, though, to get a feeling for where I am and what I hope to accomplish this week.

I'm really happy with progress so far! I've gone from an empty Xcode project to something with a couple of views and some models and the skeleton of a useful app, more or less. I'm learning (I mean, aren't we all, always?) and have gotten a lot done in about an hour a day for the last week and a half. I want to keep up this hour-a-day cadence, but I think it's worth thinking about how to best approach that hour too.

The app isn't yet usable, so my goal for this week is to have it in installed on my iPhone and using it for doing at least dimensionless price comparisons. That shouldn't be too hard to do, as I'm almost there — just need to add some labels and text fields to the detail view controller.

BUT! There's also a complete lack of tests, so I'd like to slow down the forward momentum and add some coverage for unit tests. I'm rusty with testing a Swift app, so that's something I'll spend time learning as part of my Per time this week, too.

Here are the goals for this week:

A way to clear the product list

Right now, I can add products to the list for comparison, but the only way to clear them out is to force-quit the app (or wait for it to be terminated in the background). A way to clear the list is important.

A product detail view that lets me enter quantity, units, and price

Right now the product detail view only adds a randomly-generated ProductItem and that's... not very helpful, so I want to add a basic form that lets me enter the price, quantity, and units for a product. I don't want to worry about dealing with units yet, though, so I'm going to default to dimensionless units.

Unit tests for the product and product-list models

At a minimum, having solid testing coverage for your models is pretty important, and I will look at testing the view controllers later as they're likely to be refactored a lot between now and the end of this project. I've never really explored UI testing on iOS, so that's going to be something that I worry about later.

More to come 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.

Delegate, Delegate, Delegate

So I'm now working on a simple UI to add product details to the table view. Presenting a new product detail content view controller from the product list context view controller isn't too hard — when the ➕ button is tapped in the navigation bar, we just need to responding by pushing a new view controller onto the navigation stack:

@objc func handleAddProductBarButtonItemTapped(sender: UIBarButtonItem) {
    let productDetailVC = ProductDetailContentViewController()
    self.present(productDetailVC, animated: true, completion: nil)
}

But we also need to be able to add a item to the product list, so we create a delegate property in this new product detail content view controller:

weak var delegate: ProductListContextViewController!

The tells the detail VC, “hey, delegate any work back to the product list context VC”; in this case, Per will collect the details of the new product you want to add in the ProductDetailContentViewController and pass them back when to its delegate when you tap an “Add” button:

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

(I've just got a placeholder “Add” button that calls the createRandomProduct() method I mentioned on day 7).

Now, I can go back to the product list context view controller and add itself as a delegate to the product detail content view controller:

@objc func handleAddProductBarButtonItemTapped(sender: UIBarButtonItem) {
    let productDetailVC = ProductDetailContentViewController()
    productDetailVC.delegate = self
    self.present(productDetailVC, animated: true, completion: nil)
}

And we add the method called:

func add(_ item: ProductItem) {
    self.contentViewController.productList.add(item, sort: true)
    self.contentViewController.loadView()
}

So, to summarize, here's what happens:

  • The ProductListContextViewController creates a ProductDetailContentViewController and tells it, “hey, I'm your delegate!” before presenting it.
  • The ProductDetailContentViewController presents a UI for collecting product info (price, quantity, units), and then calls the add() method of its delegate (the ProductListContextViewController) before dismissing itself.
  • The ProductListContextViewController receives the call to its add() method and updates the product list and table view accordingly.

There's something weird about how my detail view controller dismisses itself, though — sometimes it's a smooth animation, and sometimes it just disappears. I'll figure out why 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.

Sorting order

In the original version of Per, you entered the details for two products, and hit a big red “Compare” button to see which option gave you the most value for your money. That isn't going to be a great way to deal with more than two products — imagine having to initiate a comparison every time you add something?

Instead, the app can simply float the best option(s) to the top of the list every time you add a new product! That makes feedback really fast and really easy.

Turns out, it's really easy to implement this, because of a couple of things working together.

First, because I made the Product protocol conform to Comparable based on the pricePerUnit computed property on day 3, all I really need to do is call _products.sort() whenever I add a new product to the ProductList. Remember that sort() sorts an array in place, whereas sorted returns a sorted array.

But I can't just add _product.sort() at the end of the add(item: ProductItem) method I wrote on day 5 without creating a bug: if there's already something in the _products array, I (unnecessarily) return after appending a new ProductItem, so those early returns have to be removed.

Furthermore, I'd be changing the semantics of add(item: ProductItem) as well — this method no longer just adds an item to the product list, it adds an item and then sorts the list. I am the only person working on this, so I could leave it as is, but I would rather be kind to my future self and create a new method that I call instead:

mutating func add(_ item: ProductItem, sort: Bool = false) {
    
    // The original add(item:) method implementation goes here

    if (sort) {
        _products.sort()
    }
}

Adding this new sort parameter with a default value of false to the add(item:) method means I don't have to change calls to the method unless I want the list sorted.

I only call it in one place right now —the handleAddProductBarButtonItemTapped action in the product list context view controller, from yesterday— and it should sort the list when called there, because we want to sort the list before updating the product list table view:

self.contentViewController.productList.add(createRandomProduct(), sort: true)

And that's it! We can now add (randomly-generated) items to the product list and they'll sort themselves automatically.

Tomorrow feels like a good day to start working on the UI for adding an actual product's details, so that I can get the app doing what it's supposed to do — compare products.

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

Where's UINavigationItem?

On day 1, we created a hierarchy of view controllers — coordinators, context, content, containers. In retrospect, this may have overcomplicated how I reason about where things go. Something like adding a UIBarButtonItem should be fairly straightforward:

addBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add,
                                                  target: self,
                                                  action: #selector(handleAddTapped(sender:)))
navigationItem.rightBarButtonItem = addBarButtonItem

(Remember, I'm using stock UI components and functionality for now.)

So I added that to viewDidLoad() in my ProductListCoordinatorViewController, since its root view controller is a UINavigationController. But when I ran the app... no button.

I could change the navigation bar's color without issue, but adding a button to it? Nope. Turns out, I had to drop down a level, to my ProductListContextViewController and add the button there. Why? We can always turn to Hacking With Swift for an answer:

Note: usually bar button items don't belong to the UINavigationBar directly. Instead, they belong to a UINavigationItem that is currently active on the navigation bar, which in turn is usually owned by the view controller that is currently active on the screen.

[source]

This makes sense, because you'll want the view controller that's active to make decisions about what navigation items are relevant. Could I have gone deeper still into the embedded ProductListContentViewController, which holds our table view? Probably! Does it make more sense? Maybe! I'm going to explore this when I implement a detail view controller for adding new products.

Right now, when you tap the ➕ button in the navigation bar, the app runs the following action:

@objc func handleAddProductBarButtonItemTapped(sender: UIBarButtonItem) {
    self.contentViewController.productList.add(createRandomProduct())
    self.contentViewController.loadView()
}

All this does is add a product to the table view's data source, and then call loadView() on the content view controller to update the table view with the new entries. And we're just creating a simple, unitless random product for now:

// MARK: - Temporary methods for testing
private func createRandomProduct() -> ProductItem {
    let dollars = Double(Int.random(in: 1...5))
    let cents = Double(Int.random(in: 0..<100)) / 100
    let price = dollars + cents
    let quantity = Double(Int.random(in: 25...50))
    
    return ProductItem(price: price, quantity: quantity)
}

And so we can now add products to the product list, and see them appear in the app's table view UI. Neat!

But... this app is about comparing price per unit, not just listing products. So, tomorrow, I'm going to add a sorting feature to the ProductList model that gets called whenever you add a new item.

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

Building the UITableView in code

If you try to simply add var productTableView: UITableView = UITableView() as a property and then set its delegate and dataSource as self without touching the other boilerplate methods, you'll crash the app with this error:

*** Assertion failure in -[UITableView _dequeueReusableCellWithIdentifier:forIndexPath:usingPresentationValues:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKitCore_Sim/UIKit-3901.4.2/UITableView.m:8624
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'unable to dequeue a cell with identifier customcell - must register a nib or a class for the identifier or connect a prototype cell in a storyboard'

This makes sense! The commented-out tableView(cellForRowAt:) method needs to be uncommented, and I need to give this table view a UITableViewCell to work with. And it's not too hard, I just need to add this to the table view controller's viewDidLoad method:

productTableView.register(UITableViewCell.self, forCellReuseIdentifier: "productListCell")

Adding the ProductList data

Now I can create a ProductList in the table view controller, populate it with some fake data, and start working with it!

iOS Simulator showing three of rows of ProductItem data

Getting that to display just requires three lines in tableView(cellForRowAt:) :

let productItem = productList.getItems()[indexPath[1]]
let productUnits = productItem.units?.symbol ?? "units"
cell.textLabel?.text = "\(productItem.quantity) \(productUnits) for \(productItem.price) costs \(productItem.pricePerUnit) per unit"

Remember that the indexPath is collection of two values ([section, row]), so we want to specify its row value (indexPath[1]) as our index for the array returned by productList.getItems().

There's no formatting, and it's a static view of data that we can't add to, but we're slowly getting there! Tomorrow, I'll start working on a way to add product items to the table view.

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

Start with a Spike

I'm trying to spend less than an hour a day working on this rewrite.

One thing I've found is that trying to think through an elegant solution, write the code, and write up these little diary posts is... ambitious, especially on a daily cadence.

What does seem to work well is thinking about the problem I'm trying to solve, and then spiking a solution for it:

Create spike solutions to figure out answers to tough technical or design problems. A spike solution is a very simple program to explore potential solutions. Build the spike to only addresses the problem under examination and ignore all other concerns. Most spikes are not good enough to keep, so expect to throw it away.

(Emphasis mine.)

I like iteratively solving little problems to maintain forward momentum. It lets me easily scale back the scope of what I'm trying to achieve if the problem feels too hairy to solve in an hour. And if I keep in mind that every line of code I write is equally likely to be thrown away, I don't have to feel attached to the way I'm tackling the problem.

Of course, the point of this rewrite is to remove technical debt, so if at the end of my hour I don't feel great about my solution, I can always choose to revisit it again tomorrow.

So, with that, on to today's problem.

The Product List

The table view controller from day 1 needs a data source, and that data source should contain a collection of the Product items I've been working on the last few days.

But we can't just add any ol' object that conforms to Product to an array and call it a day. Once we add the first such object to this collection —call it a ProductList (because... that's what I called it)— we're defining what type of product it will hold, based on the type of units it uses: either something based on UnitMass for items sold by weight, UnitVolume for items sold by volume, or nil (Product.units is an optional, after all) for items that are sold by the unit.

So, if the first item I add has a UnitMass type, and then I try to add something without units (i.e., nil), the ProductList should reject it. That means it needs to remember the first type of Product added.

Here's what I came up with as today's spike. It's a struct that uses private properties, and a mutating function to add ProductItems (which conform to Product) to an internal array as well as set and check the unit type integrity. It doesn't throw an error yet, it just prints a message to console and skips adding the item.

It also adds a function to return the internal array. So it's basically a public-get, private-set value type.

struct ProductList {
    private var _products: [ProductItem]
    private var _unitType: Unit?
    
    init() {
        _products = [ProductItem]()
    }
    
    mutating func add(_ item: ProductItem) {
        // We can't just add ProductItems willy-nilly because they have to have matching units.
        // So, we need to check two things:
        //
        // 1. Is there already a ProductItem in the _products array?
        // 2. If so, what is its unit type (nil, UnitMass, or UnitVolume)?
        let incomingUnitType = item.units ?? nil
        
        if (_products.count > 0) {
            // There's stuff already in the _products array, so check the unit type.
            if (incomingUnitType == nil && _unitType == nil) {
                _products.append(item)
                return
            } else if (incomingUnitType != nil && incomingUnitType!.isKind(of: type(of: _unitType!))) {
                _products.append(item)
                return
            } else {
                print("Can't add an item with type \(String(describing: incomingUnitType)) to a list of \(String(describing: _unitType)) items")
            }
        } else {
            _unitType = incomingUnitType
            _products.append(item)
        }
    }
    
    func getItems() -> [ProductItem] {
        return _products
    }
}

I don't love the if/else if/else stuff going on here and can probably make it more elegant, but I can reason about this type of logic really easily, so it makes it very fast, if somewhat ugly, to write.

The add(item:) function should also be marked as throws, and that logged message about mismatched types should throw an error for the client to catch. I'll add that tomorrow and see what I can do about cleaning up the logic. I feel like I'm not using a lot of the really beautiful bits of Swift —like guard and nil coalescing— to their full potential here, and that's due to a lack of familiarity.

If you've got feedback, it's always welcome!

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

Funny subtitle goes here

On day 2 of the rewrite, I put together a computed property for pricePerUnit in an extension to the Product protocol that looked like this:

var pricePerUnit: Double {
    get {
        let formatter = NumberFormatter()
        formatter.numberStyle = .currency
        let formattedPricePerUnit = formatter.string(for: NSNumber(value: price / quantity))
        return Double(truncating: formatter.number(from: formattedPricePerUnit!)!)
    }
}

As I mentioned at the time, I still don't like the way I'm doing the necessary work of rounding digits using the NumberFormatter conversion to a String and back, but it works as a spike solution for now — we can come back to it another day.

I also added two static functions to the extension yesterday so that the protocol better conforms to Comparable — not only do we want to compare the pricePerUnit property, but we also want to compare the types of Units and make sure they're comparable.

Here's where I need to be careful. I can use quantity and units properties to initialize a Measurement, which gives me the ability to convert this to base units via the appropriately-named baseUnits() method on Measurement — this gives me a common unit to compare all items of the same unit type (UnitMass and UnitVolume are the two we care about). I can add the following logic to the pricePerUnit computed property getter and we're good to go:

var pricePerUnit: Double {
    get {
        var amount: Double = 0
        let formatter = NumberFormatter()
        formatter.numberStyle = .currency
        
        if let unitType = units {
            if (unitType.isKind(of: UnitMass.self)) {
                // This product is sold by weight.
                let measurement = Measurement(value: quantity, unit: unitType as! UnitMass).converted(to: UnitMass.baseUnit())
                amount = measurement.value
            } else if (unitType.isKind(of: UnitVolume.self)) {
                // This product is sold by volumne.
                let measurement = Measurement(value: quantity, unit: unitType as! UnitVolume).converted(to: UnitVolume.baseUnit())
                amount = measurement.value
            }
        } else {
            amount = quantity
        }
        // We're force-unwrapping here but these variables should never be nil. 😬
        return Double(truncating: formatter.number(from: formatter.string(for: NSNumber(value: price / amount))!)!)
    }
}

With that, we can set up a simple ProductList model and get it hooked up as a UITableViewController data source 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.

What's that smell?

So yesterday, I posted about making Per's Product model conform to Comparable, so that I can compare between Product objects — something that's pretty important in a price-comparison app.

Comparing the actual price-per-unit is pretty straightforward:

static func <(lhs: Self, rhs: Self) -> Bool {
    return lhs.pricePerUnit < rhs.pricePerUnit
}

static func ==(lhs: Self, rhs: Self) -> Bool {
    return lhs.pricePerUnit == rhs.pricePerUnit
}

We can't just leave it at that, though, because of Per's built-in unit conversion — you can input one product's quantity in pounds and another product's quantity in grams, and Per will figure out the best-value option automatically. This means that Per's Product model needs to understand how units compare; you can't compare the price per unit between one thing sold by the kilogram, and something else sold by the quart.

The Product model stores units as an optional Unit, and we're going to create the initializer such that it'll set that property as either some flavour of UnitMass (for items sold by weight) or UnitVolume (for items sold by volume), or otherwise nil (for dimensionless units). I started with an ugly nested-conditional mess for figuring out whether lhs.units and rhs.units can be compared. It works, but it feels like a code smell:

static func <(lhs: Self, rhs: Self) -> Bool {
    if let lhsUnitType = lhs.units {
        // lhs.units is not nil
        if let rhsUnitType = rhs.units {
            // rhs.units is not nil
            if (type(of: lhsUnitType) == type(of: rhsUnitType)) {
                return lhs.pricePerUnit < rhs.pricePerUnit
            } else {
                return false
            }
        } else {
            // rhs.units is nil
            return false
        }
    } else {
        // lhs.units is nil
        if rhs.units != nil {
            // rhs.units is not nil
            return false
        } else {
            // rhs.units is nil
            return lhs.pricePerUnit < rhs.pricePerUnit
        }
    }
}

We can make this much, much cleaner:

static func ==(lhs: Self, rhs: self) -> Bool {
    // If both lhs and rhs units nil, then evaluate the boolean expression:
    if (lhs.units == nil && rhs.units == nil) { return lhs.pricePerUnit == rhs.pricePerUnit }

    // If they're not BOTH nil, but any ONE is nil, return false:
    guard let lhsUnitType = lhs.units else { return false }
    guard let rhsUnitType = rhs.units else { return false }

    // If neither is nil, but they're of the same type, evaluate the boolean expression:
    if (type(of: lhsUnitType) == type(of: rhsUnitType)) { return lhs.pricePerUnit == rhs.pricePerUnit }

    // If we get to this point, they're not of the same type, so return false:
    return false
}

That feels much cleaner. Tomorrow, let's explore how that initializer reasons about unit types!

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

I'm a model, you know what I mean

View controllers are fun to work on and all, but at the end of the day, they're only meant to mediate between the user and some data model. In Per, we've got two models that I can think of right now. Today, we focus on one.

The Product

Start with a protocol, say Apple. The Product model in Per is fairly straightforward, but sure, let's do the protocol-oriented programming thing. Here's what our protocol looks like:

protocol Product: Comparable {
	var quantity: Double { get set }
	var units: Unit? { get set }
	var price: Double { get set }
	var pricePerUnit: Double { get }

Why is units an optional? Because we only care if the units are of type UnitMass or UnitVolume right now — otherwise we treat the product as dimensionless, i.e., plain ol' units.

The Product has to conform to Comparable so that we can, uh, compare multiple Products — and here, we're starting with a simplistic implementation in an extension:

extension Product {
    var pricePerUnit: Double {
        get {
            let formatter = NumberFormatter()
            formatter.numberStyle = .currency
            let formattedPricePerUnit = formatter.string(for: NSNumber(value: price / quantity))
            return Double(truncating: formatter.number(from: formattedPricePerUnit!)!)
        }
    }
    
    static func <(lhs: Self, rhs: Self) -> Bool {
        // Naïve implementation, doesn't account for unit type
        return lhs.pricePerUnit < rhs.pricePerUnit
    }
    
    static func ==(lhs: Self, rhs: Self) -> Bool {
        // Naïve implementation, doesn't account for unit type
        return lhs.pricePerUnit == rhs.pricePerUnit
    }
}

This doesn't account for the fact that if a client tries to compare something of UnitLength.meters against UnitType.pounds, it should fail. Instead, I'm simply looking at comparing the price per unit, which is a computed property in the Product extension. I don't love the way I'm rounding the value of pricePerUnit here using a NumberFormatter, so if you have any suggestions, do let me know!

Tomorrow, I'll work on an initializer for the actual struct that implements the protocol, and aim to better handle the unit comparison.

#per #perRewriteDiary #ios

Discuss...