Angelo Stavrow [dot] Blog

Missives and musings on a variety of topics.

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

In moving this blog to a static site generator, one concern was whether I'd still be able to work on and/or publish a post from my phone. I tend to do a fair bit of mobile drafting while I'm in the subway or waiting in line, and will sometimes publish content when I'm away from my computer. YOLO.

Requirements

It's a given—since that's how the site has been setup—that you need a GitLab.com account, and Hugo set up with GitLab CI to build the site and deploy it to GitLab Pages.

On my iPhone, I have Working Copy, which I use for git operations on the repository. It's an excellent app, and when you pair it with Editorial's powerful workflow and text editing capabilities, you've got a great toolkit for repository management as well as writing.

I like using issues as jumping off points for new posts (including this one, so I also use Git Trident to manage this in GitLab from my phone. Matt's working hard on making this the best app for handling GitHub and GitLab issues on your iOS device, and I'm especially looking forward to offline access.

Workflow

This post was a test for how to add content to the site from my phone, and here's how I'm going to do it from now on:

  1. Usually (but not always), an issue tagged post idea is the basis for a new post. When I'm ready to write, I assign the issue to myself and set a due date for it.
  2. I then create a new branch as n_post_post-subject_yyyy-mm-dd in Working Copy, where n is the issue number and yyyy-mm-dd is the post date. If no issue exists, I drop the n_ prefix in the branch name. I try to avoid setting a title, as this can change over the course of the post. “Post subject” is already pretty well defined, since I know what I want to write about.
  3. Next, I create a new text file in Working Copy under /content/post, and open it in Editorial via the share sheet to start writing. I have a TextExpander snippet to add the TOML front matter; it's also set as a draft, to ensure it doesn't get published accidentally.
  4. Then comes the hard part: write the post. Commit as necessary with Working Copy's workflow for Editorial. I commit and push for a “cloud save”, or if I think I might want to continue elsewhere (i.e., on my desktop, or in a web editor1.
  5. When it's all said and done, I set 'draft = false in the front matter, commit and push with message Closes #n, where n is the issue number.
  6. When I'm ready to publish, I launch Safari and go to GitLab.com to open a new merge request. Merging the draft branch into master will trigger GitLab CI; once the build passes, the post will be published. I also delete the original branch.
  7. That's it! The post is published. Enjoy a tasty, refreshing beverage!

I wrote the majority of this post on my iPhone, although I'll admit that it does feel a bit silly to tap away on a 4.7” screen when I've got a 24” monitor and full-size keyboard to work with. Still, for drafting and editing, it's great, and in a pinch (or, say, on an iPad with an external keyboard), you can keep your blog running from your iOS device without issue.

  1. Web editor, you say? Why yes! You can edit a file in GitLab, then commit the changes. Neat, huh?

Discuss...

As I mentioned last week, today is July 1st, which marks a convenient half-way point for the year. On New Year's Day, I posted a short list of goals that I was hoping to make progress towards. Here's how it all breaks down.

1. Post something here every Friday.

So far, so good. I haven't missed a single week, which I'm pretty pleased with. I'll grant that it hasn't always (ever?) been a particularly interesting read, but I've been trying hard to keep showing up. Discipline isn't easy, but it helps if you try to avoid breaking the streak.

2. Post to a journal at least once every day.

I was doing pretty well with this through to the end of March, and gave up by April 22nd. It started well, but frankly, I wasn't comfortable in writing my deepest thoughts into a service that can't guarantee its privacy—not that I think the fine folks at Bloom would ever consider it, but if key employees have access to the decrypted data, that means necessarily that there's some risk of a data breach.

And I could feel it when I was writing, too—I was self-censoring just in case. And if you can't just write whatever you want in your journal, of all places, then it's not really a journal.

So, I've deleted whatever I'd posted there, and I'm re-thinking this goal. There's value to writing regularly, sure, and there's value to getting your thoughts down, but between this blog, the Break Before Make tumblr blog, and Twitter, I think I've got more than enough opportunity to write.

3. Make real progress towards my Mac app.

Myeah.

Well, I settled on a name and registered a domain. I keep starting new projects and I've got even more in my Someday/Maybe list. I really need to focus more on this.

4. Contribute to open-source projects.

I feel like I haven't done too much with this yet. I've written for open-source projects, but beyond that, this also feels like something that's maybe taking focus away from the Mac app.

5. Get in better shape.

After taking care of some knee issues with my physiotherapist, she gave me the green light to start getting active again. So, as of the end of March, I've started by jogging. My goal is to get in three 15-minute runs per week—I'm not trying to run a marathon, just improve my cardiovascular health.

It's been, well, shaky.

I've lapsed into no-run periods a few times, either because of an injury, or because I've been too lazy to get out and do it. But generally, there's a very strong downward trend in my pace (from a 7:41 to a 6:17), so I'm happy. Moreover, it's just been getting easier to run for extended periods of time.

It's high time I add some kind of strength training into the mix, though!

Other salient projects

As linked above, other projects have come up that I started work on.

For one, you may notice the new look of this site. As of today, it's being built by Hugo and is running on GitLab Pages. That's been a fun little project, with the side benefit of saving me a few bucks a month, too. I'm considering cross-posting over to Medium, but we'll see. There's some theme work to be done, but otherwise, hello Hugo!

I've also open-sourced my first iOS app, in the hopes that having it in public will embarrass me into updating it. But again, I really don't want this to distract me from the Mac app I want to write.

What's next

Honestly?

I don't know.

But the intention is, for the next six months, to:

  • keep up with writing here, on (at least) a weekly basis;
  • keep up with improving my general fitness and health; and
  • really get cracking on this Mac app I keep hinting at.

Will I get sidetracked? Undoubtedly. Will I venture into new little projects that may or may not be abandoned? Yeah, it's possible. We live in flux, after all, and it's way more fun to explore the forest than it is to walk the path.

Definitely more tk.

Discuss...

Reminder: as of next week, the RSS feed for this site is moving! If you subscribe, please be sure to point your favourite reader to:

http://makebeforebreak.com/index.xml

Next week is July the first. It's Canada Day here, which means it's a national holiday, but in Montreal, it's also an unofficial moving day.

So it's a propos that this site will be moving over to its new digs on that same day.

But July first also marks the halfway point of the year, a year which I started with some set of goals. So it's also a good time to take stock of progress made against those goals, and make any necessary course-corrections.

I don't intend on revisiting that here today, but I can tell you that I know that I haven't made the kind of progress I was hoping for. In fact, the only thing I've “succeeded” at, strictly speaking, is posting something here every Friday. It hasn't always been easy, but so far the streak has remained unbroken.

So, technically, I'm looking at an 80% failure rate. That doesn't sound encouraging at all.

I could feel bad about this.

But I don't.

Ash Furrow gave a great talk called Loosely Held Strong Convictions last year. It reminded me that, despite our belief to the contrary, we exist in a state of flux.

It's strange how we unconsciously fight this. I mean, we know that things—including ourselves—are constantly changing. And yet, we often try to plot rigid courses through life, despite the fact that today's road may well be tomorrow's wall.

It's fine to change your mind. In fact, it's encouraged. It means you're adaptable. You're able to go beyond living your life as a to-do list.

The hard part in all of this, what I fight with the most, is whether or not I'm changing my mind because a path has become irrelevant, or just unpleasant. I may not be doing super well at getting to the gym (like, at all), but I am trying to run for 15 minutes or so, three times a week.

Except if it's raining.

Or if I really need to get into the office soon.

Or maybe if I woke up with a bit of a gurgle in my tummy.

Discipline is hard. Which means it's way more valuable than motivation. It's easy to do the pleasant thing when you're motivated; it's useful to do the unpleasant thing because you're disciplined.

So, yeah. By all means, make changes to your opinions and your plans. But do it because it's a good idea, not because it makes your life easier.

Discuss...

Enter your email to subscribe to updates.