Angelo Stavrow [dot] Blog

per

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

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.

Delegating to delegates

With the form laying out the way I'd like, I now have to get the data that a user enters into the form from that view to the view controller it's embedded in. And what's the best way to do this? The delegate pattern!

(I'm kinda wiped out after a rough night, so this isn't going to be a big, in-depth post — and today wasn't an especially productive day of writing code, either.)

But here's the gist of it, implemented kinda hastily just to remind myself how this all works. There's a view controller (in this case, ProductDetailContentViewController) that adds a child view that has a bunch of UITextFields in it called ProductDetailFormView. Each of the text fields have a delegate that capture events like textFieldDidEndEditing() so you can do stuff, like formatting numbers or whatever. And you want your view controller to be able to get that data from the child view, but you have no way of knowing what is in that child view.

So you create a delegate protocol for the view controller that the child view can talk to!

Very simply, here's what I've got right now:

protocol ProductDetailContentViewControllerDelegate {
    func createProductFromFormInput(_ product: String)
}

class ProductDetailContentViewController: UIViewController {
  // The class implementation
}

extension ProductDetailContentViewController: ProductDetailContentViewControllerDelegate {
    func createProductFromFormInput(_ product: String) {
        // Just print whatever we get back for testing purposes
        print(product)
    }    
}

Side note: I like handling delegate conformance in an extension like this because it keeps the class implementation itself nice and clean. If it's a simple protocol you could probably just have it all in your class, but that just feels like yet another path to massive view controllers if you're not careful.

Anyhow, in my child view, I just need to add this as a delegate:

class ProductDetailFromView: UIView, UITextFieldDelegate {
  var delegate: ProductDetailContentViewControllerDelegate?

  // The rest of the class implementation

  func textFieldDidEndEditing(_ textField: UITextField) {
    delegate?.createProductFromFormInput(textField.text)
  }
}

At a very high level, having the text field delegate call the createProductFromFormInput() method of the ProductDetailContentViewControllerDelegate when editing finishes raises a flag saying, “hey, I'm done with this text field, whoever owns this can do something with it now!” And so the extension I showed you above will catch that flag and say, “okay, cool, got it — let me print it to the console!”

Oh, one silly little thing that I forgot: when you declare your child view in the view controller's viewDidLoad() method, don't forget to tell it what its delegate is. 😅

Something like this:

class ProductDetailContentViewController: UIViewController {
  // Some of the class implementation

  override func viewDidLoad() {
    super.viewDidLoad()

    productDetailFormView = ProductDetailFormView()
    productDetailFormView.delegate = self

    // The rest of the viewDidLoad() stuff
  }

  // The rest of the class implementation
}

Now this isn't super helpful because it just sends back whatever is in a text field when that text field loses focus. There are three different text fields in the form, so tomorrow I'll tackle differentiating between them, so that the quantity field sends back a quantity, the price field sends back a price, and the units field sends back units.

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

Constraints

Yesterday, I started subclassing UIView to get the setup and layout of the product-details form out of its parent view controller, but was having a heck of a time trying to get it to look the way it was supposed to.

Here's the situation. The view controller doesn't know the size of the child view that is being added, so that child view gets initialized with a .zero frame — that is, it's placed at the origin (0,0) of the view controller's bounds, with height and width equal to zero.

The child view has its layout constraints pinned to the top, left, and right of its layoutMarginsGuide. Cool.

Of course, as I realized, it's not enough to say view.addSubview(childView) and be done with it when we're doing our layout in code; the child view has its constraints relative to its layoutMarginsGuide, but it doesn't have any constraints set up in relation to its parent view!

That's mostly straightforward —add constraints to the top, left, and right of the parent view— until you try to pin another child view to the bottom of the form. Remember, that form was initialized with a .zero frame, so to UIKit, it's technically got zero height unless you add that constraint. And, at least with the standard UI controls I'm using, that height can be determined by the intrinsicContentSize.height property.

Pop quiz: what's the intrinsicContentSize of a UIStackView?

Trick question! A stack view has no intrinsic size of its own. You've got to figure that out based on the intrinsic size of the controls within the stack view.

Which is exactly what I did: I exposed a formHeight property in the child view that is computed by a function called getHeight():

func getHeight(of stackView: UIStackView) -> CGFloat {
if stackView.arrangedSubviews.count < 1 { return 0.0 }

if (stackView.axis == .horizontal) {
    var heights = [CGFloat]()
    
    stackView.arrangedSubviews.forEach { subView in
        if (subView.isKind(of: UIStackView.self)) {
            heights.append(getHeight(of: subView as! UIStackView))
        } else {
            heights.append(subView.intrinsicContentSize.height)
        }
    }
    
    return heights.max() ?? 0.0
} else {
    var totalHeight: CGFloat = 0.0
    
    stackView.arrangedSubviews.forEach { subView in
        if (subView.isKind(of: UIStackView.self)) {
            totalHeight += getHeight(of: subView as! UIStackView)
        } else {
            totalHeight += subView.intrinsicContentSize.height
        }
    }
    
    totalHeight += CGFloat(stackView.arrangedSubviews.count - 1) * stackView.spacing
    
    return totalHeight
}

When you pass in a stack view, this function will either return the tallest arranged subview in a .horizontal stack view, or the sum of heights of all arranged subviews in a .vertical stack view, plus the spacing between them. If, as it walks through the arranged subviews, it finds another subview, it'll recursively call itself on that stack view. It works really well for this use case!

So, now I can set the heightAnchor constraint to the the value of the child view's formHeight property, and I'm set — the layout looks fine, and tomorrow I can work on getting the values from the text fields to create the 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.

Subclassing UIViews

Yesterday I had the ProductDetailContentViewController working (more or less), but I was unhappy with just how big it was getting. It's got two main components: a form in which the user enters product details (price, quantity, units), and a pair of buttons to add the product to the list, or cancel the action altogether.

Today, I started work on subclassing UIView to move that form component into its own ProductDetailFormView, to pull all of those controls (three UITextFields, two UIStackViews, and a partridge in a pear tree UILabel) and the form's own layout into its own class.

Once again, Frank Courville's got a handy article for this! So far, I've started writing the setupView() and setupConstraints() methods for the class. One little change that I like is to declare my controls as lazy, so that I don't have to worry about unwrapping optionals (force-unwrap and guard let both feel like the wrong way to reason about views, and John Sundell agrees):

class ProductDetailFormView: UIView {
    lazy var quantityTextField = UITextField()
    lazy var unitsTextField = UITextField()
    lazy var measurementStackView = UIStackView()
    lazy var forLabel = UILabel()
    lazy var priceTextField = UITextField()
    lazy var formStackView = UIStackView()

    override init(frame: CGRect) {
        super.init(frame: frame)
        
        setupView()
        setupConstraints()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        
        setupView()
        setupConstraints()
    }
    
    // Create the views and define some basic styling.
    func setupView() {
        quantityTextField.placeholder = "0"
        quantityTextField.textAlignment = .right
        quantityTextField.keyboardType = .decimalPad
        quantityTextField.borderStyle = .roundedRect
        
        unitsTextField.placeholder = "units"
        unitsTextField.textAlignment = .center
        unitsTextField.isEnabled = false          // Deal with units later
        unitsTextField.borderStyle = .roundedRect
        
        measurementStackView.axis = .horizontal
        measurementStackView.distribution = .fillEqually
        measurementStackView.alignment = .center
        measurementStackView.spacing = 16
        
        measurementStackView.addArrangedSubview(quantityTextField)
        measurementStackView.addArrangedSubview(unitsTextField)
        
        forLabel.text = "for"
        forLabel.textAlignment = .right
        
        priceTextField.placeholder = "0.00"
        priceTextField.textAlignment = .right
        priceTextField.keyboardType = .decimalPad
        priceTextField.borderStyle = .roundedRect
        
        formStackView.axis = .vertical
        formStackView.distribution = .equalSpacing
        formStackView.alignment = .fill
        formStackView.spacing = 16
        
        formStackView.addArrangedSubview(measurementStackView)
        formStackView.addArrangedSubview(forLabel)
        formStackView.addArrangedSubview(priceTextField)
        
        addSubview(formStackView)
    }

    func setupConstraints() {
        formStackView.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            formStackView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor, constant: 16),
            formStackView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor, constant: 16),
            formStackView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor, constant: -16)
        ])
    }
}

I'm running into some trouble figuring out the constraints right now. Specifically, the form take the full width of the top of the superview, and its height should be the height of its contents (the formStackView). That height is where I'm a bit stuck, because when I call the ProductDetailFormView initializer from its superview, I have to hand it a frame; it's clear to me that the frame's origin is (0, 0) and that its width would be view.bounds.width, but I'm haven't quite figured out the best way to set its height.

I could give it a third of the height of the superview (view.bounds.height / 3), but that's not adaptive, so if I want to change, say, font sizes in the form view, I need to ensure that it still fits whatever portion of the superview height. That's silly.

In the form view's initializer, I could throw away whatever frame height I get, but it's still not clear to me how I get the form's inherent height in the initializer. In Frank's article, he calls it with a .zero frame (i.e., at the origin, with zero size); if I do that, then I get a broken layout and a warning:

[LayoutConstraints] Unable to simultaneously satisfy constraints.
	Probably at least one of the constraints in the following list is one you don't want. 
	Try this: 
		(1) look at each constraint and try to figure out which you don't expect; 
		(2) find the code that added the unwanted constraint or constraints and fix it. 
	(Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints) 

If I set

productDetailFormView.translatesAutoresizingMaskIntoConstraints = false

then the warning disappears, and the layout kind-of shows up, but it's sized entirely according to the intrinsic size of the form's controls, not the full width of the superview, and trying to anchor other things to the form view's anchors doesn't work properly because it's still got that .zero frame.

This is where my relative inexperience with writing custom UIViews is throwing me for a loop, but I'll dig into this a bit more 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.

Getting to MVP

So, working with stack views is fun!

Yesterday I created a first stack view to layout the Add and Cancel buttons in the add-product view, with today's goal being to layout the rest of the view.

It was pretty straight forward, and now I have a functional-ish app! I can enter a (unitless) product and it'll get sorted according to price per unit:

"Two screenshots of Per in action, displaying the add-product screen and the product list screen"

Of course, because there's no validation going on, you can also crash the app by simply tapping the Add button when you don't enter any text. And you can't select anything other than “units” for the product you're adding. Still, technically, I could install the app on my phone now and start using it!

I've also now made a huge mess of the ProductDetailContentViewController, too. In it, there's all the setup code for the stack views (3), the text fields (3), the buttons (2), and a label. All of that should be broken up into at least two view controllers: one for the inputs, and one for the buttons, and validation can be added as necessary there.

So, breaking up this not-quite-massive-but-kinda-huge view controller is what I'll work on 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.

Stacking the Deck

Yesterday I threw together a little ASCII-rendered layout of nested stack views and mentioned that I'd start implementing it by putting the existing Add and Cancel buttons in a horizontal stack view. So the goal for today is to spike something like this in my ProductDetailContentViewController:

+==================+
||  ADD  || CANCEL|| <- Horizontal stack view
+==================+

Eventually, the goal is to refactor this out into context, container and content view controllers, but right now that's overthinking it. Remember, the goal of this rewrite is a “one small change per day” approach.

I noticed a UI bug, too: when the user taps the Add button, the action's completion block enables the clear-list button. This means that if you tap the add button to create the first product on the list, and cancel out of that action instead of adding something, that clear-list button still gets enabled.

I can't add this logic to the completion block, though — it'll check the length of the product list before the product item actually gets added, rather than after.

Completion blocks are insidious in this way. You think they're going to run, y'know, after completion, but always ask yourself: after completion of what?

We want the logic for enabling the clear-list button to be triggered by actually adding an item, so we can instead move it to the add(item:) delegate method in the product list context view controller. This gets called by the product detail content view controller when the user taps the Add button, which is all we need!

Good! So today I started implementing the layout by putting the product detail view's buttons in a horizontal stack view, and fixed a little UI bug. Tomorrow, I'm going to continue by adding a vertical stack view for all the labels and text fields.

#per #perRewriteDiary #ios

Discuss...