Angelo Stavrow [dot] Blog

Missives and musings on a variety of topics.

This post is part of a series about rewriting my iOS app, Per. Per is a price per unit comparison app with a bunch of neat convenience figures, but it hasn't been updated in years, so I'm rewriting it from scratch to eliminate a bunch of technical debt. Just because it's not an open-source app doesn't mean I can't share what I learn as I go!

See the rest of the series here.

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

As you might know if you subscribe to this site's RSS feed (which syndicates to Micro.blog and Twitter), I've been working on a from-scratch rewrite of my iOS app, Per. More interesting to me is the context around how and why I'm doing this rewrite, as well as what I've been learning from the process.

The goal is to spend an hour a day making one small change to the codebase. Not only that, I'm also writing a short blog post each day discussing what I got done. The plan is to have a functionally capable (if somewhat plain) iOS app that replicates Per's current v1.2 functionality by the end of February, with maybe a couple of new features.

I haven't touched Swift or UIKit “in anger” since I shipped the current version of Per on the App Store, over 4 years ago. As you can imagine, lots has changed, so much of the time I'm spending is about learning, trying new things, and forming opinions.

(Opinion: creating views in code rather than with Interface Builder just… speaks to me.)

Perhaps the biggest takeaway is just how much progress I've made with such a relatively small amount of time dedicated to this work per day. Writing about that little bit of progress every day —for anyone to read— has been a motivator (public accountability!) to keep at it, day after day, while also serving as a record of what I did the day before.

As I work, the tasks ahead of me begin to take shape, and the further I progress, the easier it becomes to understand what tomorrow's iteration should be. And —I feel this is key— I also make sure to end my session by noting down what I want to tackle tomorrow. This means that when I sit down, I can open up Xcode and have total clarity on what my goal for the day is.

This is important! I don't waste time spinning my wheels, trying to figure out what to do today, because I already decided that yesterday. Nor do I don't feel overwhelmed by a monumental task ahead of me, because I don't plan anything that will take more than an hour.

This is something I've been applying more generally, and I've come upon a system that's been working well for me that combines a set of text files to pull together a daily journal, weekly retrospectives, project-specific update logs, and [TaskPaper]-formatted to-do lists. I'm nearly done fleshing the system out —mostly, I want to add some automation because it currently involves cutting and pasting things between various text files— and I'll be writing more about it when I do!

All of this comes from having formed a habit of journaling daily for the last two years — capturing the baby steps we take between having an idea and bringing it to reality. I'm excited about this in ways I didn't expect, and I look forward to sharing them with you all!

Interesting reads this week

Elsewhere on the web

I published my first article on DEV.to on how to get create-react-app and Express to play nicely together on Glitch, complete with a starter project you can remix to get started.

#journaling #productivity

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

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.

In The Details

On Monday I worked on a plan for the week. I tackled a simple clear-the-list feature yesterday, and today I'm going to start —but probably not finish— work on the product detail view(s) that let users enter, uh, product details.

Again, some planning is worth the effort before jumping into writing code. The goal of this part of the rewrite is to get something functional up and running before doing any custom design, and to learn, but making it easy to change a codebase is more invoved than you'd think.

In its current form, a product is entered into Per as a set of three text fields, as shown. And it's... not wonderful.

The design sacrifices a lot for the sake of compactness — in my initial sketches, I wanted Per to handle as much input as possible in a single view, to make it very quick and easy to get in and out of the app while, say, doing your groceries.

For v2, users aren't limited to comparing two products, so we necessarily need a separate view for product entry. This gives me a lot more room to breath, as it were, so starting off with three sets of label-plus-textfield input areas is a good start!

This is where stack views are super helpful. I feel that flexbox is a good analogue to stack views in the web development world; you create either a row or a column of views and align them along the main and cross axes according to some rules, and voilà! You have a basic layout. And you can nest stack views, for something like this:

+------------------+   ^
|                  |   |
+------------------+   |
|                  |   |
+------------------+  Vertical stack view
|                  |   |
+==================+   |
||       ||       || <-+-- Horizontal stack view
+==================+   V

Tomorrow, I'll start implementing something like this by putting the existing Add and Cancel buttons in a horizontal stack 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.

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

Enter your email to subscribe to updates.