The Per Rewrite Diary: Day 2

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.

Angelo Stavrow

Montreal, Canada
Email me

Mobile/full-stack developer. Montrealer. Internet gadabout. Your biggest fan.