Angelo Stavrow [dot] Blog

Missives and musings on a variety of topics.

I see a lot of digital ink spilled on how some particular thing is

  • broken
  • taking a quality nose-dive
  • stupid
  • irrelevant
  • &cet.

And it's good to be critical. With a bit of healthy skepticism, we avoid the reality-distortion fields that turn rational-minded folks into zealots. Nothing is so perfect that it can't be improved, and I get that this is the place that a lot of these thinkpieces come from.

But the thing is, communication should serve some purpose. It doesn't necessarily have to be some call to action, but it should, at the very least, let the audience answer one simple question:

What am I to do with this information?

In other words, if you're telling me how crappy something is, what's the point? Are you proposing some solution?

Or are you just complaining?

It's okay if you are. We're human and we have feelings about things. Talking about those feelings can help you find common ground with others who feel ways about stuff.

But let's abstain from calling it criticism. Constructive criticism moves a thing forward; it offers the receiver something to work with. This is key: it generates goodwill, and it helps make things better.

And—given that the world is made up of things—constructive criticism makes the world a slightly better place, by extension.

Which is nice.

Discuss...

I haven't posted here in a little while, thus breaking the streak that I was intending to maintain. I don't feel too bad about this, because I've been working on other stuff in the meanwhile, but it's nice to be writing again.

Specifically, in light of the coming App Store purge, I've been working on a big update to Per. While I'm not especially concerned that Per, in its current form, is at risk of being culled, it has been a long time since it was updated.

So.

For one, I'm dropping support for anything prior to iOS 10. Per has a pretty tiny user base—maybe a couple hundred downloads—and I don't see much in the way of daily active users1, but for such a tiny user base, I can't justify putting effort into supporting iOS 8 and 9.

Second on the list is a pretty hefty redesign. I fully admit that Per is… kinda homely-looking. There are some interactions that I'm planning on (and have started experimenting with) for 2.0, and while it's slow going, I like what I see so far.

I'm also thinking about taking advantage of some new input methods, but I don't know yet how well that will work. I don't want to tip my hand just yet because those may not be technically feasible, but it's got me pretty interested in seeing what can be done2.

Third on the list is the business model for Per.

Per brings in pretty much zero revenue. Most of the downloads came from a period of time when I made my apps free for a week, whereas the normally-paid (at USD$1.99) version has seen only a handful of downloads at most.

Note that I haven't marketed Per at all, save for a couple of tweets and blog posts, so this isn't unexpected. But I think it's time to change that.

Given the way the App Store economy works, it's pretty clear that charging up-front is a barrier to getting your app downloaded. This is no secret, so some business model that includes free downloads makes sense.

One option is to make Per free, with an in-app purchase (IAP) to unlock advanced features, like the unit conversion and in-field math operations it already features, and the new input methods mentioned. This means zero revenue unless people want these features. How desirable are they in a relatively simple utility app like Per?

Another option is to make Per free, but ad-supported. Thing is, I personally don't like the aesthetic that ads introduce in an app's interface and experience, and I know I'm not the only one. So there would definitely have to be an IAP to remove ads. I have a couple of ideas on where and when I'd show an ad, so they wouldn't be too annoying.

Both cases mean that current paying users would experience either reduced functionality, or an ad-filled experience. That sucks, so yet another option would be to create a new SKU. Folks that have the original version of Per get a fully-featured and ad-free upgrade, and new users could opt to pay for this version outright, whereas the other version of the app would implement one of the above options, but would list for free.

That's a fair bit of extra administrative work for me, and confusing for both old and new users, so I don't think I'll do that.

I could also leave Per as-is and create a new SKU for Per 2. No further updates for the old app, but then that also probably means it'll eventually be culled from the App Store anyhow.

Given that something like 3 users have actually paid for Per (thanks!), I think that I'll skip the two-SKU options altogether, because they're administrative headaches. This means that Per 2 will be a free upgrade, but will either be removing some features or including ads (sorry!).

Maybe there's a way to check whether a user paid for an app or not, I'm not sure. Let me know!

As I mentioned, I know that Per is really just a simple utility app, but it's something that my wife and I use pretty much every time we shop for groceries. It's certainly useful, but I also want it to be great. That not only means redesigns and refactoring, but it also means getting it in the hands of as many people as possible.

More tk.


  1. I know that this could be because most people disable sharing this data when they set up their phone.

  2. Seems to me that, for some developers, adding a new feature to an older app has less to do with a revenue bump than it does the excitement of tackling a new problem. That's how I feel, anyhow.

Discuss...

It's widely expected that the next iPhone will be announced tomorrow during the scheduled Apple Event. For months now, the rumour mill has been telling us that the most controversial change Apply is making to its flagship product is the removal of the headphone jack.

At this point, it seems like a certainty. The headphone jack in its current form has existed for decades, but Apple is notorious for advancing physical I/O past its status quo: witness the current MacBook with its single USB-C port, and recall the Lightning and 30-pin connectors on iOS devices, the adoption of Thunderbolt on current Macs, the even introduction of USB on the original iMac.

Every time Apple introduces a new port standard, or drops an old one, the ecosystem adapts. Because iPhones didn't use micro-USB connections, pundits complained that it'd be nigh-impossible to charge and sync your phone if the included cable got lost or damaged. Fast forward only a few years after the introduction of the iPhone, and 30-pin cables can be found nearly anywhere that has even the most modest of electronics departments.

But the headphone port has always been around. There's some expectation of Lightning-connected earbuds, which poses a charge-while-listening dilemma, and there will almost certainly be Lightning-to-3.5mm adapters—if not from Apple, then definitely from third parties. But adapters add bulk and are easily lost. Which leads to the next option: Bluetooth.

Wireless options have been available for a long time, but they've never really been particularly well-received: they're expensive, the sound quality isn't great, &cet. The real problem, as I see it, is that a wireless device is an active device by necessity. It has to power its own wireless radios, and in this case also power the audio drivers. That means batteries. That means battery life (hours of use per charge), and battery lifespan (number of charges per battery).

What all this means for iPhone 7 users is not forgetting to charge things, but also not charging things too often. It means not losing adapters. It means having to plan ahead to ensure you don't need to charge your phone while listening to music. It means that there's now an additional bit of cognitive load on you, which is never good.

This is my main problem with some of these changes. I want new technologies to relieve me of stuff to think about, not add more busywork and more administrative B.S. to keep on top of.

I know this is a barely a blip in the grand scheme of things—heck, it's only a headphone port—but it nicely captures The Problem With Technology As I See It. Moving the state of the art forward should always be in the service of freeing us to do more important things. And I don't think changes like this necessarily do so.

Discuss...

I noticed this earlier in the year, but my iPhone is coming apart at the seams near the volume buttons.

At first, I figured I'd managed to bend the phone, but upon closer inspection, both the back of the case and the screen are bulging outward—which is not the failure mode you'd expect for a bent phone.

The gap at the seam is nearly 2mm, such that light from the screen leaks, but it's nowhere near as bad as I've seen doing an image search. The screen doesn't seem to be cracking, though I can see weird artifacts in the backlighting.

Most signs point to a bloated battery being the culprit. This doesn't sound like a safe state for a plastic baggie full of acid and electrons, so off to the Genius Bar we go.

More tk. Update:

One look at the phone and the Genius confirmed the diagnosis. Normally, in the case of a bloated battery, they won't bother with replacing the battery, in case it's started leaking and caused damage to other internal components—they give you a new phone and your old device gets sent off to Liam for disassembly and recycling. If you're still covered by AppleCare, the exchange is free, but if not, you only pay for a replacement battery, not a replacement phone.

Discuss...

Literally writing with light, or photography, as it's more commonly known.

I used to think that I loved photography. I think I still do, I just... never really do it anymore. At one point I had a whole mess of expensive camera gear, with the fancy full-frame digital SLR and red-ring'd lenses. And the fast primes—oh, I loved those. Studio strobes? Yup. Speedlights? Several. Fifty pounds of camera gear that stayed home because, I convinced myself, I couldn't be arsed to carry fifty pounds of camera gear around with me.

Unless there was some kind of cool reason to. I helped shoot a wedding and did a men's fashion shoot. Several portraits, too. But mostly, the gear stayed at home, collecting dust.

I mean, fifty pounds. I'm a relatively fit person, but come on.

So I sold it all and downsized to a small micro-four-thirds setup. A kit zoom, a couple of fast primes, and a speedlight. Fifty pounds traded for five and a bucketful of cash. Now I'd start taking more photos. Obviously.

I didn't, of course.

Thing is, I do enjoy photography. Crafting an image is immensely satisfying. Capturing a great moment is fun. But I've gotten lazy about it, because the best camera is the one that's with you, and by gum the camera on my iPhone is pretty fantastic, all things considered.

But it's not the same. And while I don't want to become fifty-pound camera-guy again, I do think I'll take a cue from Ash Furrow and start walking around with my little PEN day-to-day.

I mean, look how pretty it is.

And of course, in true keeping-myself-honest fashion, I'm going to try to do a photo post every so often. Maybe once a month. Maybe some old photos, maybe some new photos, maybe a couple of words too.

More —as always— tk. 📸

Discuss...

Last week, I wrote about weather apps that use the Dark Sky forecast API for weather data lacking the Canadian humidex.

Those that do tend to be riddled with ads and all kinds of content that, well, I don't care about. And naturally, I started thinking about how I use weather apps. All I really want from my is a couple of things:

  • Current conditions, including humidex/ windchill values;
  • Forecast conditions with highs and lows for today and tomorrow, again including humidex and windchill;
  • Probability of precipitation for the next hour, with alerts of impending rain.

Getting the actual forecast along with alerts is a solved problem, thanks to Forecast.io. Doing conversion and such between different units is also a solved problem, thanks to the new Measurement class in Cocoa.

(Here's a great starter post on Meaurement)

So what would, say, a basic CurrentConditions object look like in my ideal app? Probably something like this:

import Foundation

struct CurrentConditions {
    var conditions: String
    var airTemperature: Measurement<UnitTemperature>
    var dewpoint: Measurement<UnitTemperature>
    
    var humidex: Measurement<UnitTemperature> {
        return Measurement(value: airTemperature.converted(to: UnitTemperature.celsius).value + 0.5555 * ((6.11 * exp(5417.7530 * ((1/273.16) - (1/dewpoint.converted(to: UnitTemperature.kelvin).value)))) - 10), unit: UnitTemperature.celsius)
    }
}

We initialize this strict with some descriptive weather conditions (e.g., “cloudy”), and values for the air (i.e., actual) temperature and dewpoint. It doesn't really matter what units we use when we pass the values in, because the calculated property humidex will convert them to Celsius and Kelvin respectively, before returning a value in Celsius.

Then you can simply add a switch in your weather app's settings asking the user if they prefer apparent temperature be calculated according to heat index or humidex.

Discuss...

I don't deal well with heat brought on by humidity. Which makes a Montreal summer pretty tough to deal with. Temperatures will routinely go over 30°C, but when combined with high humidity, it'll feel even hotter.

Like, three-showers-a-day-ain't-enough hot. Grimy, unpleasant, swampy hot.

And most weather apps out there don't get it. Sure, they have a “feels like” temperature that compensates for some of it, but it never feels quite right.

To really understand how it works, you need a weather app made by Canadians, for Canadians. Because Canadians use a humidex.

Feels like

Humidex distinguishes itself from the US heat index in that it's proportional to the dew point, not relative humidity. And generally, it's significantly higher.

I've seen days where the “actual” temperature (i.e., air temperature) is in the high twenties, but the “feels like” humidex is mid-thirties. That's a huge difference—what's comfortable to wear at one temperature may not be at the other. Or worse: if you're living with some medical conditions, the decision to leave the comfort of air conditioning may even be fatal.

A cursory look at weather apps (I've used many) shows that most use Forecast.io's API. It's a great data source, especially for its hyper-local precipitation forecasts. But—and this of course makes sense, given market sizes—it doesn't use Canadian humidex calculations to derive the apparent temperature, so I never feel like I can trust these apps for forecast highs.

All I really want from a weather app is accurate alerts for incoming precipitation, and (humidex-corrected) forecast highs for the day. I've keep telling myself that I don't want to take on new projects, but having to switch between a couple of apps every day is kind of a pain.

🤔

Discuss...

WHEREAS IT IS AGREED THAT

  1. There exist THINGS which one MUST DO; and
  2. There exist THINGS which one WOULD ENJOY DOING; and
  3. The above-listed THINGS may be MUTUALLY EXCLUSIVE;

IT IS RESOLVED THAT

  1. Where a THING is ENJOYED and MUST BE DONE, one shall REVEL for one can easily give a 💩 about it; and
  2. Where a THING is ENJOYED but whose execution is NOT REQUIRED, one shall strive to MAKE THE TIME for it, for it is good to do things one gives a 💩 about; and
  3. Where a THING is NOT ENJOYABLE but MUST BE DONE, one shall give a 💩 about executing the THING with CARE AND ATTENTION; and
  4. Where a THING is NOT ENJOYABLE and is NOT REQUIRED, one shall strive to ELIMINATE IT from the list of things one invites into their lives, for one should strive to fill their lives with things one gives a 💩 about.

AND THE MANIFESTO IS THUS RELEASED FOR APPLICATION on this day in the presence of these WITNESSES, the READERS.

Discuss...

A couple of months ago I played a little bit with PassKit and Wallet after finding this little tutorial on adding a business card to Passbook (now named Wallet).

A Wallet pass is pretty easy to create—just fill some metadata into a JSON file, create a Pass Type ID certificate in your Apple Developer account, and then run a signing utility. The whole process is pretty well described, step-by-step, here, so I won't re-iterate, but here are a couple of little perils and pitfalls to watch out for:

  • To run signpass, you'll need to download the Xcode project in the Wallet Developer Guide. Unzip the download and open signpass.xcodeproj in the signpass folder. Build it, right-click on the executable in the Products folder in Xcode's file navigator, and select “Show in Finder”; drag and drop it into your Documents folder.

  • After you run signpass from the Terminal, run open PassName.pkpass and it'll open in a Preview QuickLook window. From here, you can click on the “Add to Wallet” button and it'll go up to iCloud and download to your iOS devices, without having to upload it to a server.

Preview QuickLook

  • If the pass doesn't show up in your iOS device, there's probably a typo in your JSON. Look for extra/missing commas, parentheses, square brackets, &cet.

  • You can double-check by launching Xcode's Simulator, launching the Wallet app, and dragging and dropping your .pkpass file from the Finder into your Simulator Wallet. If everything checks out, it'll ask if you want to add it.

Simulator add pass

  • If you're uploading it to S3, make sure you set the Content-Type to application/vnd.apple.pkpass. Normally, this is a dropdown, but you can also just type whatever you want in there and S3 will accept it.

S3 Content-Type

And now you have a fancy electronic business card that other iPhone users can scan and download. Have fun!

Discuss...

There's a new kid on the XCTest block, and its name is XCTAssertThrowsError.

I haven't been able to find much on its usage aside from its original discussion on the swift-evolution mailing list and a Stack Overflow question, so here's a little bit of a discussion on how I'm using it in a new project of mine.

Swift introduced some pretty neat error handling in 2.0, and Natasha the Robot provided a nice guide on how to throw an error in your code.

So, as a contrived example, let's say you have a class called AccountManager that manages a set of Account objects:

	enum ListError: ErrorType {
		case AccountAlreadyExistsInList
		case AccountDoesNotExistInList
	}
		
	class AccountManager {
		var accountList = Set<Account>()    // Account conforms to Hashable, Equatable. I promise.
		
		func add(_ account: Account) throws {
			if (accountList.contains(account)) {
				throw ListError.AccountAlreadyExistsInList
			}
			else {
				accountList.insert(account)
			}
		}

		func remove(_ account: Account) throws {
			if (accountList.contains(account)) {
				accountList.remove(account)
			}
			else {
				throw ListError.AccountDoesNotExistInList
			}
		}
	}

(Note that in Swift 3, ErrorType has been renamed to ErrorProtocol.)

A couple of things to know about the Set type:

  • No duplicates can be added, a Set is silent when you try to add an element that it already contains.
  • Unless you're checking its return type, Set is also silent when you try to remove an element that it doesn't contain1.

While this protects the integrity of the Set, it could be a bit frustrating for consumers of the AccountManager class, because there's no way to surface what's going on when we try to add a duplicate or remove a non-existent element. So, we throw!

Specifically, what we're doing in the AccountManager class is checking to see if the Account argument we're passing to the add(account:) and remove(account:) functions already exists in the accountList Set, and handling the result appropriately:

  1. If we're trying to remove an Account from the accountList and it exists, go ahead and do so. If it doesn't, throw ListError.AccountDoesNotExistInList.
  2. If we're trying to add an Account to the accountList and it exists, throw ListError.AccountAlreadyExistsInList. If it doesn't, go ahead and add it.

And of course, in our unit tests, we want to check that these errors are thrown properly. Enter XCTAssertThrowsError!

    // Test that adding a duplicate account to an accountList throws an error.
    func testAdd_AddingDuplicateAccount_ThrowsAccountAlreadyExistsInList() {
        let accounts = AccountManager()
        let firstAccount = Account(descriptiveName: "Account 1", accountNumber: "12345AZ")
        let secondAccount = Account(descriptiveName: "Account 1", accountNumber: "12345AZ")

        accounts.add(firstAccount)

        XCTAssertThrowsError(try accounts.add(secondAccount))
    }

    // Test that removing an account from an empty accountList throws an error.
    func testRemove_RemovingAccountFromEmptyList_ThrowsAccountDoesNotExistInList() {
        let accounts = AccountManager()
        let firstAccount = Account(descriptiveName: "Account 1", accountNumber: "12345AZ")
        
        XCTAssertThrowsError(try accounts.remove(firstAccount))
    }

Run the tests and they'll pass, because the tested expression throws an error. Cool!

This is a good start, but we're only testing that an error is thrown. That's not good enough, of course, because any error thrown will make this test pass, but we're looking for a specific error. Let's take a closer look at the declaration for XCTAssertThrowsError:

    func XCTAssertThrowsError<T>(_ expression: @autoclosure () throws -> T,
                                    _ message: @autoclosure () -> String = default,
                                         file: StaticString = #file,
                                         line: UInt = #line,
                                 errorHandler: (error: ErrorProtocol) -> Void = default)

I've split the signature up so that there's just one argument per line. The description for each argument is available in the documentation, so I won't repeat them here, but the important thing to note is that XCTAssertThrowsError is actually a generic on T. This means that in the expression argument, we can add a closure that checks to see if, in fact, an error of type T is being thrown.

So let's add those checks to our two tests:

    // Test that adding a duplicate account to an accountList throws an AccountAlreadyExistsInList error.
        func testAdd_AddingDuplicateAccount_ThrowsAccountAlreadyExistsInList() {
        let accounts = AccountManager()
        let firstAccount = Account(descriptiveName: "Account 1", accountNumber: "12345AZ")
        let secondAccount = Account(descriptiveName: "Account 1", accountNumber: "12345AZ")
        
        accounts.add(firstAccount)
        
        XCTAssertThrowsError(try accounts.add(secondAccount)) { (error) -> Void in
        	XCTAssertEqual(error as? ListError, ListError.AccountAlreadyExistsInList)
        }
    }
    
    // Test that removing an account from an empty accountList throws an AccountDoesNotExistInList error.
    func testRemove_RemovingAccountFromEmptyList_ThrowsAccountDoesNotExistInList() {
        let accounts = AccountManager()
        let firstAccount = Account(descriptiveName: "Account 1", accountNumber: "12345AZ")
    
        XCTAssertThrowsError(try accounts.remove(firstAccount)) { (error) -> Void in
        	XCTAssertEqual(error as? ListError, ListError.AccountDoesNotExistInList)
        }
    }

Now we're sure that we're testing that the right error is being thrown in our tests: in the closure, we call another assertion, XCTAssertEqual, to check that the error being thrown is the type of ListError that we expect.

What does this mean? We no longer have to create weirdo functions that return a tuple <U, V>, where U is the result and V is an error that you can check for.

You can add other arguments to your assertion, like a message or the specific file and line number if the test fails, but for now this should be enough to get you started checking your error throwing.

  1. Of course, you should be checking the return type, and you’d see that you got back nil, but like I said: this is a contrived example.

Discuss...

Enter your email to subscribe to updates.