Swift, UnitTemperature, And Humidex

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.


How about that heat?

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.

🤔


The Give-A-💩 Manifesto

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.


Wallet-based business cards

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!


Using XCTAssertThrowsError in your Swift tests

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