Watch App Development Blog - Week 6

February 20, 2015 - alamofire bug error-handling swift telstra

 I’m blogging my progress in developing an Apple Watch App. Read the previous instalments here.

Telstra - grrr!

I had a frustrating week trying to track down an odd issue - I wouldn’t get any train times load when I was walking around (i.e. near the station, where I want to test), but there was nothing showing up in the logs. The app would work perfectly at home when plugged into Xcode. This put a bit of a dampener on the planned Pebble app field testing. Eventually I managed to log an error message (NSURLErrorDomain -1003 - hostname not found). It turns out the tutum.io subdomain assigned to my docker endpoint isn’t resolving on any of Telstra’s DNS servers. I’m running Google DNS at home, which resolves the hostname fine, and so doesn't exhibit the issue, but on 4G I’m at the mercy of the Telstra DNS. After an hour on the phone with Telstra, this has been ‘escalated to Level 3 support’ and I’ll get a response 'within 7 days’. I’ve switched back to an IP address so I can continue my testing.

What did we learn here?

While I’m obviously shocked that Telstra’s internet services could be anything less than spectacular, the key takeaway is that leaving logging out of your code DOESN’T SAVE TIME. Swift makes it trivially easy to return a typesafe, idiot-proof error result that forces you to think critically about how you’re managed error conditions in your code. Consider this snippet (the culprit):

func get<T: JSONConvertible>(path: String, f: [T] ->()) {
    Alamofire.request(.GET, hostname + path)
        .responseJSON { (_, _, json, _) in
        if let json = json as? [NSDictionary] {
            f(json.map({ T(dictionary: $0) }))
        } else {
            f([])
        }
    }
}

func getAllStations(f: [Station] -> ()) {
    get("/train", f)
}
  1. I’m ignoring the error object that Alamofire is helpfully returning (the last parameter in responseJSON)
  2. I’m returning an empty array if literally anything goes wrong. Networking error, HTTP error (like a 500 from the server), malformed JSON payload, the lot = empty array.

In this case the empty station array was overwriting the locally cached stations, and my 'nearestStation' method was never producing a result. Because it’s just refreshing a local cache of fairly static data, I can safely ignore errors if I already have data. This could be done by testing the array count in the callback, but we should be able to do better.

Let’s change our ApiClient get method to the following:

enum Result<T> {
    case Value(Box<T>)
    case Error(NSError)
}

func get<T: JSONConvertible>(path: String, f: Result<[T]> ->()) {
    Alamofire.request(.GET, hostname + path)
        .responseJSON { (_, _, json, error) in
        if let json = json as? [NSDictionary] {
            f(Result.Value(Box(json.map({ T(dictionary: $0) }))))
        } else if let error = error {
            f(Result.Error(error))
        } else {
            f(Result.Error(NSError(domain: ApiClientErrorDomain,
                code: 1,
                userInfo: [NSLocalizedDescriptionKey: "An unknown error occurred."])))
        }
    }
}

We’re defining a Result type that returns either the requested value, or an NSError. Ignore the Box, this is only a figment of your imagination, and is totally not a hack to work around the Swift compiler’s problems with ‘non-fixed multi-payload enum layouts’. Note I’m also creating an ‘unknown error’ to handle the case where I don’t get back an error object from Alamofire. Just in case.

I use the updated API as follows:

    getAllStations { r in
        switch r {
        case let .Value(s):
            setStations(s.unbox)
        case let .Error(e):
            println("Error refreshing stations from the server: \(e.localizedDescription)")
        }
    }

So we have glorious logging in case of error and a rather annoying ‘unbox’ call. Importantly though, due to the signature of the API, it’s now much harder to lazily ignore error handling.