Angelo Stavrow [dot] Blog

ios

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.

Untangling Stack Views

One thing I always have struggled with is visually reasoning about nested arrays — which, if you kinda squint and look at from the right angle, is very similar to what you've got with nested stack views.

The actual code to set up a stack view? Fairly straightforward, really. Apple did a nice job with this API. But untangling my own set up of the various horizontal and vertical stack views to create the keyboard was a bit... gross. I should have probably started by thinking about the top-level stack view in terms of columns rather than rows — blindly copy-pasting a solution from Stack Overflow often gets you to a solution while creating other problems, I guess.

At any rate, that's cleaned up now. I ended up exploring three options, and I'm not in love with any of them:

"Three options of calculator keyboard layouts"

For now I'm going with the one on the far right, with the equals button in its own column.

(Yes, I'm testing with the iPhone 8 Simulator right now. I always tend to test with an older device's screen size, though I don't have a good reason for why — but I will have to take care to ensure the layout makes sense with the home bar indicator thing on the iPhone X/Xs/11.)

Tomorrow, I get back to the actual calculator functionality!

#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 A Difference A Dream Makes

I got a pretty good night's sleep last night, so I'm feeling much better today as far as being able to think clearly. Before I just start writing code, here's the plan for how the calculator should work from a user's point of view:

  1. The user taps on a text field:
    • if it's empty, proceed to step 2.
    • if it's already got a first number (LHS operand) in it, proceed to step 3.
  2. Enter the LHS operand via the keypad; it shows up in the text field.
  3. Tap an operator symbol on the keypad; it shows up in the text field.
  4. If the user then:
    • switches to a different field, remove the operator symbol from the text field and go back to step 1.
    • enters a second number (RHS operand), proceed to step 5.
  5. As the user enters the RHS operand, it shows up in the text field.
  6. If the user then:
    • switches to a different field, perform the arithmetic and replace the text field contents with the result.
    • taps an operator symbol on the keypad, perform the arihtmetic and replace the text field contents with the result as a new LHS operand, then append the new operator symbol to the text field.

This works out for the current state of the calculator keyboard, but I'm not thrilled about that last step: it feels like there should be an equals button (“=”) that the user can press at any time to perform the arithmetic and update the text field to the result.

Another thing that the current shipping version of Per does is only allow one operator symbol to show at any given time. In other words, you have to solve the two-operand equation before you can continue, and this is enforced by disabling the operator symbols. Operator precedence is tricky to reason about (ever second-guessed yourself answering a “skill-testing question” on a contest?) so Per removes that option entirely.

Combining these approaches, I could add an equals button to complete arithmetic, and also disable other operator symbols (except for the equals key) whenever the user is entering a RHS operand. The only ugly part here is how to add a single button to the currently well-balanced 16-key grid. Here's what I'm thinking, using one of my terrible ASCII layouts:

 ----------------------
|                      | + |
|                       ---
|                      | - |
|                       ---
|                      | ⨉ |
|     NUMERIC KEYS      ---
|                      | ÷ |
|                       ---
|                      |   |
|                      | = |
 ---------------------- ---

So, a separate set of stack views for the numeric keys, and then another set of stack views over to the right for the operator keys and a double-tall equals key.

It's alwayhs better to work from a plan. Looking forward to kicking off the implementation 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.

Back To Work, Sorta

Another night of terrible sleep has me struggling to reason with the best way to get the calculator keyboard to actually, y'know, calculate.

So I'm taking baby steps: first, figure out when the user is entering a LHS (left-hand side) vs RHS (right-hand side) operand for the equation. I can do that with a Boolean flag (isSettingLHSOperand). Okay, that's working as expected.

Then, I need to keep track of each of those operands (two Doubles, lhsOperand and rhsOperand), as well as the last operator symbol (“+”, “–”, “⨉”, “÷”) entered (a String called lastOperatorSymbol). From there, I can get my solution based on the operator passed in and move ahead with my work.

Yup, it took me an hour to struggle through this, and I attribute that squarely on having had only about 4 hours of sleep in the last two days.

I'm going to keep harping on this because it's important: if you have difficulty sleeping, do what you can to remedy it. It's easier said than done, I know — but it's important.

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

It's Okay To Take A Break

Nothing to report here today. I'm exhausted after a rough night and when my alarm went off at 5AM, I chose “roll over and go back to sleep” over “get up and hustle” because really, I don't hustle so good when I'm exhausted.

Build cool things. Create learning opportunities. And make sure to listen to your body, too.

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

More Fun With Custom Input Views

Yesterday I started work on replacing the system decimal-pad keyboard with something more like a calculator (which lives in a class called CalculatorKeyboard).

What I'm learning is this: the easy part is adding the keys. The hard part is accounting for their behaviour.

For example: the operator keys (i.e., +, −, ×, ÷) and delete key shouldn't be enabled if there's no text in the text field. But I can't actually hook into the UITextField to see what the current text is programmatically; instead, I need to track that with each keypress.

Which seemed straightforward... until I realized that the user could tap the clear button in the textfield, and I'd have no way of knowing beyond constantly checking target.hasText or having something happen in the text field's textFieldShouldClear(_:) delegate method (I chose the latter option).

Or that the user could type something into the field, navigate to another field, and then come back to that first field. If the initializer for the calculator keyboard doesn't account for this by having an argument for whatever the current text of the text field may be, then its internal representation of what is being type will be out of sync with the textfield's text property.

I also want the operator keys to be disabled just after one is tapped, but then re-enabled if you add a number after that. That way, you can't enter multiple operators between operands in the equation.

So today's work largely focused on handling this kind of behaviour. I always feel a bit uncomfortable working on this stuff because I might miss an edge case somewhere, but using property observers to trigger updates helps a lot. It changes my mental model of the class to a state machine of sorts, which is a lot easier to sketch — and I've always found that if I can sketch out a decision tree or flowchart or whatever, I can write the relevant code fairly easily.

Tomorrow, I start work on implementing the actual calculation!

#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 Input Views

In the currently-shipping version of Per, I use an inputAccessoryView that adds a couple of buttons for things like navigating between fields and, more importantly, keys for performing simple arithmetic.

I started off my work today in re-implementing this and then realized that, hey, rather than doing this, why not instead replace the system's .decimalPad keyboard that I'm currently using with a calculator-type keyboard?

It could look something like this:

 --- --- --- ---
| 1 | 2 | 3 | + |
 --- --- --- ---
| 4 | 5 | 6 | - |
 --- --- --- ---
| 7 | 8 | 9 | ⨉ |
 --- --- --- ---
| . | 0 | ⌫ | ÷ |
 --- --- --- ---

In case the poor ASCII art doesn't make it very clear, the intention is having the decimal keypad, along with an extra column of arithmetic keys along the right.

One thing I got some comments on was that people didn't really take note of the calculator feature, likely because the keys in the input accessory view were too subtle. Doing it this way should be more prominent and hopefully improve feature adoption.

I started working on a CalculatorKeyboard class based on this Stack Overflow answer and it's working nicely well so far. It also gives me the opportunity to skin the keyboard to better match the rest of the app's design later on, though the picker view will probably look out of place if I go too far on this. Can you even skin a picker view? I haven't really looked into this.

With the system decimal keypad's functionality replaced, I'll start work on adding the calculator keys and their functionality 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.

Fun With textDidChangeNotification

The first papercut I'm tackling since revisiting the list yesterday is a FIXME that affects the Add button in the product detail form view. We want the button to be disabled unless the form has “valid input”, which is to say, it's got at least a price and quantity set.

The UITextFieldDelegate lets you hook into various events related to a text view, and the one we're most interested here is when the text was changed. I kicked off this work by running a callback on textFieldDidEndEditing(_:) method that would call a method in the form view's delegate, updateVolatileFormData() — this struct provides a temporary datastore for the price, quantity, and (optionally) units as Strings from each text field.

Instead, on each change of the text in the form, I can call that same update method — and do a little bit of additional checking. If the form data is complete, enable the Add button; otherwise, disable it.

So how do we hook in to this event? At the end of my form view's setupView() method, I subscribe to textDidChangeNotification:

func setupView() {
    /* Set up the controls and other views here */

    NotificationCenter.default.addObserver(
        self,
        selector: #selector(textDidChange(_:)),
        name: UITextField.textDidChangeNotification,
        object: nil
    )
}

And then I create that selector:

@objc func textDidChange(_ notification: Notification) {
    if let textField = notification.object as! UITextField? {
        switch textField.tag {
        case 100:
            datasource?.quantity = textField.text ?? ""
        case 101:
            datasource?.units = textField.text ?? ""
        case 102:
            datasource?.price = textField.text ?? ""
        default:
            print("Unknown tag")
        }
        
        delegate?.updateVolatileFormData()
    }
}

Notifications include an object property that you can pass to subscribers, and in this case, that object payload is the UITextField that triggered the notification (which is why I force-cast notification.object as a UITextField? here — remember that I'm subscribing to UITextField.textDidChangeNotification, so I'm pretty sure that cast is guaranteed to succeed).

Now, here's a fun fact.

For reasons that I'm not clear on, these notifications are not fired when you change the content of a text field like so:

someTextField.text = "blah blah blah"

Instead, if you want to set the value of your text field programmatically, you'll have to do it this way:

someTextField.text = ""
someTextField.insertText("blah blah blah")

That will fire the text-changed notification. Because I'm using a picker view whose didSelectRow: delegate method sets the value of the units text field, I had to change it from the former to the latter approach.

I guess this makes sense because inserting/removing text counts as changing the text that's already there, whereas the straight assignment in the first approach doesn't because that String object may not even exist yet? I'm not entirely sure, but it's been a source of confusion in iOS for a while.

Anyways, with this work done, you can't crash the app if you hit the Add button before you've got appropriate input values in your form. The last bit of work to get this to feature parity with the shipping version of Per is adding an inputAccessoryView for navigating fields and enabling the simple calculator feature, so I'll kick off work on that tomorrow, then sort out all of the remaining papercuts before I move on to new v2.0 features (beyond being able to add multiple products, that is).

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

Revisiting the Papercut List

Two weeks ago, I paused on writing code and took stock of the “papercuts” that I wanted to deal with before moving forward. Now that my work on automatic unit conversion is wrapped up, it seems like a good time to do it again.

That said, there wasn't much to add to the list beyond stuff I'd already noted here:

  • The picker view code in ProductDetailContentViewController could be factored out into a separate class to make it a bit more maintainable. This isn't totally necessary, but it makes things a little cleaner and easier to maintain.
  • One thing that is necessary is to change the way I set units text field in the ProductDetailFormView when the picker's didSelectRow: value. I'm thinking of just adding a setUnitsTextFieldValue() method to the form view that can be called here.

This joins the outstanding issues:

  • The Add button should only be enabled when the form has sufficient information to create a product.
  • The displayError() method should be available to all view controllers.
  • The form view's UITextFieldDelegate code should be refactored into a separate class.

In the last two weeks, I've closed three papercut issues:

  • Created a custom table view cell for the product list.
  • Marked ProductList.add() as throws.
  • Refactored the UI layout code in the product detail content view controller.

So, I started with six papercuts, closed three, and am left with five. As Brent Simmons says, bug math is weird.

Again, these aren't necessarily bugs, but they are quality-of-life improvements in the sense that it'll make it easier to maintain and reason about the code.

There's also one UX issue to think about. The product list view has buttons in the navigation bar, and the product detail view has them under the form. I really dislike nav-bar buttons on anything but a 3.5” iPhone screen because of reachability issues, so I'd like to rethink how to combine this into, say, a floating button that changes functionality as you go from screen to screen. This isn't a goal for this function-focused initial rewrite, though.

In fact, beyond these papercuts there's only one feature left to implement to have feature parity with the currently shipping version of Per: adding a very simple calculator in an input accessory view.

Tomorrow I'll work on enabling the Add button only when there's enough form content to create a new 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.

Optionally Conditionally Yours

Here's where we're at right now.

When the form to add a product to the list is first shown, Per inserts a segmented control that allows the user to choose whether the product in question is sold by weight, by volume, or by units. When adding a second or third product, this isn't shown, to enforce that all products are comparable on a price-per-unit basis.

This first product sets the unitType property (either UnitMass or UnitVolume) for the product list.

After that initial product is added, showing the form should obey the following rules:

  1. If the product list's unitType is nil (dimensionless units), keep the unit text field disabled.
  2. Otherwise, enable the unit text field and show the appropriate picker view (either with weight or volumne units, based on the list's unitType).

To do this, I made a couple of changes to the the product detail content view controller (which creates and presents the form view) delegate:

  1. Added a listUnitType: Unit? property that gets the product list's unitType;
  2. Added a setPickerTo(_ unit: Unit?) method that looks at the type of unit passed in, and then sets the picker view's data source appropriately.

The added method also allowed me to refactor the delegate method that was called by the segmented control that I discussed here.

Now, when the form view is created, I can add a check in the delegate property observer:

weak var delegate: ProductDetailContentViewControllerDelegate? {
    didSet {
        if self.delegate?.numberOfProductItems == 0 {
            insertUnitTypeSelectorControl()
            formHeight = getHeight(of: formStackView)
        } else {
            if let listUnitType = self.delegate?.listUnitType {
                unitType = listUnitType
                unitsTextField.text = listUnitType.symbol
                unitsTextField.isEnabled = true
            }
        }
    }
}

That checks for and conditionally sets an optional unitType property on the form when the delegate is set, if it's not the first product being added and the product list has a unitType. If the checks pass, it enables the units text field and sets it to some value.

Then in the textFieldDidBeginEditing(:) delegate method, I can check the unitType property when a user taps on the units text field and call delegate?.setPickerTo(unitType) to setup the picker view's units.

And with that, automatic unit conversion now works! Tomorrow, I'm going to go through all of this and make a list of what new papercuts have come up.

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

Setting Units

Yesterday, I set each entry in my unit KeyValuePairs to look something like this:

"kilograms": UnitMass.kilograms.symbol

And that way I could directly get the String representation of the unit's symbol (in this example, “kg”) to drop into the picker view and text field.

Instead, I could make the entries look like this:

"kilograms": UnitMass.kilograms

When I need to get the string symbol for the unit, I can do that — no need to do that work ahead of time.

Now, with the addition of an optional tuple selectedUnit: (String, Unit), I can handle setting the units for the first product (and thus the unit type for the entire ProductList) in the picker view's didSelectRow: delegate method:

func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
    // FIXME: We're going through both unit data sources when we only need to go through one.
    _ = pickerWeightDataSource.contains { key, value in
        if (value.symbol == pickerTextFieldOutput[row]) {
            selectedUnit = (key, value)
            return true
        }
        return false
    }
    
    _ = pickerVolumeDataSource.contains { key, value in
        if (value.symbol == pickerTextFieldOutput[row]) {
            selectedUnit = (key, value)
            return true
        }
        return false
    }
    
    // FIXME: We're reaching into the subview to directly manipulate a textfield. THIS IS BAD!
    productDetailFormView.unitsTextField.text = pickerTextFieldOutput[row]
}

There are now a couple of FIXMEs in that one delegate method. I don't love that I'm going through both the picker's weight and volume data sources to see which symbol we've got; the method should be smarter than this.

So I can fix that this way, and —because Swift lets you define a function within a function— can clean up the duplicated code at the same time:

func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
    func checkSymbolAndSetUnit(key: String, value: Unit) -> Bool {
        if (value.symbol == pickerTextFieldOutput[row]) {
            selectedUnit = (key, value)
            return true
        }
        return false
    }
    
    if (pickerTextFieldOutput[0] == pickerWeightDataSource[0].value.symbol) {
        _ = pickerWeightDataSource.contains { key, value in
            checkSymbolAndSetUnit(key: key, value: value)
        }
    } else {
        _ = pickerVolumeDataSource.contains { key, value in
            checkSymbolAndSetUnit(key: key, value: value)
        }
    }
    
    // FIXME: We're reaching into the subview to directly manipulate a textfield. THIS IS BAD!
    productDetailFormView.unitsTextField.text = pickerTextFieldOutput[row]
}

That feels much cleaner. Since pickerTextFieldOutput is an array of all the unit symbol, I can test to see if the first element matches the symbol of the first element in my pickerWeightDataSource (a KeyValuePairs collection). If it is, find the right UnitMass and set that as the selectedUnit. If it's not, go through the pickerVolumeDataSource instead.

That sorts things out for setting the units on the first product and the unit type for the ProductList, but then I can't choose the units for subsequent products that I add to the list. I'll work on that tomorrow!

#per #perRewriteDiary #ios

Discuss...