Better Get Crackin'
Sometime overnight, I got an email from Apple's App Review team stating that I've got thirty days to submit a new version of Per or it'll be pulled from the App Store. I'm not even mad.
Sometime overnight, I got an email from Apple's App Review team stating that I've got thirty days to submit a new version of Per or it'll be pulled from the App Store. I'm not even mad.
Just because I closed out the Per rewrite journal in late March, doesn't mean I'm not still working on the app! Here's what's been happening.
At the end of April, I closed out last issue in the Minimum Dogfoodable Product (MDP) milestone. This represents the first internal release that gets the rewritten app to feature parity with v1.2, the currently-shipping version in the App Store. At that point, Christina and I replaced the shipping version of the app on our iPhones to start, as the milestone name implies, dogfooding it.
With that done, I moved on to issues in the Minimum Launchable Product (MLP) milestone. This release represents an internal beta that collects bugfixes from using the MDP along with a set of launch-blocking features to be implemented before releasing the first external beta. That work wrapped up yesterday, so now we're on to the Minimum Viable Product (MVP)!
The MVP, once work is complete, will be the first externally-available (via Testflight) release of the app, and includes all features planned for the public release. The plumbing and framing was completed in the MLP work, so now —new bugs notwithstanding— it's about UI and UX polish. I've been building Per with mostly-stock UIKit components and typography, such that I get things like accessibility and dark-mode support (mostly) for free. But as Christina's pointed out, there's lots of little UX improvements that can be made, and a little bit of UI/personality would be nice, too.
You've already seen one of the new features: increasing the number of compared products beyond two.
New features will, for the most part, live behind a small in-app purchase. Because the current version is free, I don't want anyone to lose out on existing functionality; free users will get to compare up to three products, with paying customers being able to compare unlimited products (or some technologically-constrained number that approximates “unlimited”).
The in-app purchase will also unlock something I've personally been wishing for: a way to save a product for later. Like many, I know where to go to get the best price on certain staples, but I don't always remember exactly what that price or quantity is. So, when I come across some sale on that product at another store, I can't be sure if it's a better deal or not. 🤔
Well, with the next release of Per, you'll be able to save that information for later. It's been a fun feature to implement, including setting up persistence with SQLite/FMDB, but it still needs some more work to get it where I'd like. I hope you'll find it useful!
Will there be other features? Yes, eventually, but probably not for this release. So, as always, more TK.
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.
Today is the fiftieth —and last— entry into this daily development diary. I'm not going to be discussing further work on Per v2 here, because at this point the work I'm doing is new features, and because I think it's probably getting tiresome for some readers.
(There are no analytics gathered on this site, so I don't know if readership has increased or decreased with this series.)
The goal was to get Per from a blank Xcode project to feature parity with the currently-shipping version in an hour a day, and to record my progress publicly as I went. I hoped to complete that work in February (so, 29 days). Non-goals included any kind of custom design work. As much as possible, I'm only using standard iOS 13 UI elements. That's not to say that custom design work won't be coming, but that wasn't part of this project.
Also, this rewrite is being done entirely in code — no Interface Builder work, no XIBs, and no storyboards (aside from the one required for the app's launch screen).
Without further ado, here's my takeaway on fifty days of public development journaling:
Public journaling means public accountability. While I technically took a day here or there where I didn't really post anything beyond saying, “I'm taking a break today,” I felt like I had to do something every day just because I'd set that expectation. I made more progress in these past seven weeks than I did in the last… ugh, has it really been four and a half years since the last update?
Limiting this to one hour (-ish) per day maintains forward momentum. Because I only dedicate an hour or so per day to this task, I have to make sure that I plan my work accordingly. I've tried to end every entry with a simple description of what I'll tackle the next day, a trick I use in my daily work. Knowing that I only have an hour or so to work on it means that I scope the work down to fit.
It's a record of my work. Writing it down makes it something I remember, but it's also something I can refer back to if I need to revisit insights. This is way more useful than comments in the source code. I only wish I'd thought to do something like a daily git commit
with a link to the day's journal entry.
It can be stressful. Beyond the COVID-19 pandemic, there's a lot going on in my life right now, and this extra commitment has sometimes felt overwhelming. Those tend to be the days when I say, “yeah, I'm taking the day off.” Never trade your health in the name of “not breaking the streak.” But if you feel some capacity to do a little work, it's not a bad idea to do some catch-up and planning on days like that!
So, this has been fun, and very helpful. I'm going to continue working on Per, and I'll probably share something here now and again — but if you're interested in finding out more as the app gets closer to release, check out the Dropped Bits blog and Twitter account.
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've discovered that —at least the way I'm calling things— no matter what I do, I just can't resize a UIPickerView
after it's been instantiated, and instantiating it with the frame I want just gets ignored. I need to dig further into this because I'm clearly doing something wrong, but as I'm only working on this an hour a day, that's slow going.
So, in the spirit of making progress and not getting sidetracked with details that ultimately aren't that important, whenever I have to show my picker view, I call this method:
func showPickerView(frame: CGRect, sender: UITextField) {
let stackView = UIStackView(frame: frame)
stackView.axis = .vertical
let safeInsetView = UIView(frame: CGRect(x: 0, y: 0, width: frame.width, height: view.safeAreaInsets.bottom))
stackView.addArrangedSubview(pickerView)
stackView.addArrangedSubview(safeInsetView)
sender.inputView = stackView
}
Fuck it. It works, so long as I set my calculator keyboard's frame height to 216
(the height of a picker view) plus the bottom safe inset area.
I hate magic numbers in my code, so I'll continue investigating this, but for now... it's fine.
I've decided that tomorrow will be my last day posting these. Fifty days of uninterrupted blogging... not bad. But I'm also pretty much at feature parity with the shipping version now, so I can start adding features instead.
So tomorrow, a wrap-up, and then back to your regularly scheduled programming.
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.
On Tuesday, I shared this image comparing the height of the two main input views (the calculator keyboard and the picker) in Per between an iPhone 8 and an iPhone 11:
I could explicitly set the height of both input views to something like 240 instead of giving it a .flexibleHeight
auto-resizing mask, but that didn't feel like the right approach. I asked about this in the Core Intuition Slack and Daniel Jalkut suggested I poke around in Xcode's view debugger to see what's going on there.
Here's what I've been able to surmise.
I layout a custom input view like CalculatorKeyboard
as follows (it's really a set of nested stack views, but I'm leaving that out — details of the approach are from this Stack Overflow answer):
func setupView() {
autoresizingMask = [.flexibleWidth, .flexibleHeight]
addButtons()
}
func addButtons() {
let stackView = createStackView(axis: .horizontal)
stackView.frame = bounds
stackView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
addSubView(stackView)
// —code to layout all of the buttons goes here—
}
This basically just sets the frame of the custom keyboard to that of the system keyboard's bounds. Note that it doesn't do anything to add a safe area inset at the bottom of the keyboard. The view debugger tells me this is 375×291 on an iPhone 11 Pro, and 375×216 on an iPhone 8, a difference of 75 points.
I'm way less specific about setting the frame of the picker. In fact, I just set:
textField.inputView = pickerView
and call it a day. The view debugger tells me its size is 375×216 on both iPhone 11 Pro and iPhone 8 — the same size.
I'm getting closer here. I think part of this has to do with the intrinsic content size of the custom keyboard vs the picker, and also that I should probably be doing more in the parent view controller than just hoping things work out with autolayout in the views.
I'll dig deeper on this tomorrow!
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 taking a break from this work today because it feels like everything is falling apart right now and I need some time to sort out my own needs. That's way more important than not breaking the streak.
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.
Papercuts are the little scratches you get as you plow headlong through the prickly underbrush of implementing features.
One such papercut that I sorted out this morning was a weird UIToolbar
iOS 13 bug requiring me to set a size on the input accessory view's frame lest I get that annoying “unable simultaneously satisfy constraints” warning. Previously, you could instantiate the toolbar with a simple
let inputAccessoryView = UIToolbar()
but now you'll need to do something like:
let inputAccessoryView = UIToolbar(frame: CGRect(x: 0, y: 0, width: 100, height: 100)).
I'm running into a weird bug, however, where the input view changes size on some devices between my custom calculator keyboard and the picker view:
Notice how those input views are the same size on an iPhone 8 (on the left), for example, but very different on an iPhone 11 (on the right).
I'll dig into this some more tomorrow.
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.
Just a short update today: as planned, I added the input accessory view to the detail form today. This only adds a simple previous/next toolbar button to skip between text fields in the form. It was nice to use SF Symbols for the left and right carets!
That gets us to feature parity — but now we've got the papercuts to deal with, with the hope that I'll have this installed on my phone for testing by the end of the week.
Tomorrow I take care of planning what to tackle first!
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 couple of little calculator-related papercuts were sorted out today.
12+
” entered, it'll clear the “+
” symbol from the text field).The only thing I want to finish up before installing going back to clean up any other papercuts is to add an input accessory view —the toolbar that sits just on top of the iOS keyboard, like a hat— that includes a previous and next button to skip between fields, which I'll add tomorrow.
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.
Slowly unravelling work on Per's calculator feature has been interesting. I had worked on getting the UX/UI of the calculator working before getting the actual arithmetic sorted out, such that certain keys would be enabled or disabled based on state, and then tried to add the math functionality on top of that, which made for a very messy pile of code.
I went through and removed anything that enables or disables keys, and cleared out the big, ugly updateResult()
method that handled the actual (attempt at) arithmetic. I'm pushing a lot of the work into a property observer, and that's probably not going to be the final solution for this class, but it's really handy for simplifying how a thing should react to a change.
At any rate, here's how it's working now.
Every time a key in the keyboard is tapped, a character is appended to a currentString
property — either a digit, a decimal, or an arithmetic operator. The main use for this string is to track what's in the relevant UITextField
.
I'm using the didSet:
observer on that property to parse that string every time it's updated, such that I can set a pair of Double
s for the left- and right-hand-side operands. This is done by checking a flag that tells me if an operator button was tapped. If that's false, we're setting the LHS operand; if it's true, we're setting the RHS operand.
When you tap an operator button, the class also makes a note of what that symbol was. Then, when you tap the equals button, it calls a much-simplified updateResult()
method that looks at the value of the LHS and RHS operands, and performs the arithmetic based on the last operator button tapped (so, if you had tapped the +
key, it'll add the operands) before resetting a few properties. The solution to that calculation is set as the new value of currentString
(which sets this as a new LHS operand), and is also returned as a string (which the caller uses to update the target text field).
Oh, if you're creating a custom input view for your app and need a way to clear the text field, here's a handy recursive function you can use:
func clearTextField(_ sender: UIButton) {
guard let isNotEmpty = target?.hasText else { return }
if (isNotEmpty) {
target?.deleteBackward()
clearTextField(sender)
}
}
Tomorrow, I'm going to add back automatic enabling and disabling of various keys based on the state of the keyboard.