NetworkHandler

Table of Contents

Didn’t you already do this?

Since my last post on network calls, I started to get frustrated with the redundancy required. For example, I’d want to GET a model from an API, but the PUT request was 90% the same with only subtle variations.

Additionally, in our UI code, sometimes we’d want to tell our model controller to make a network call, know when it’s finished and if it has our requested data (or potentially an error), but we wouldn’t want to put the actual network code in our UI code. This would require a completion that could either have Data or an Error, but not both. It could, in fact, have neither depending on the circumstances. And since all three of those scenarios were equally plausible, we’d have to make the completion closure contain optional variables that would then need to be unwrapped.

Yuck. I want that stuff to happen over in data land, not in UI land.

Oh I see where you’re going… I think?

You might, if you see me making a framework to help alleviate those issues, because that’s what I did!

I actually started this project the same day as my previous post. With every new school project that required networking, I’d copy the files over and make some tweaks here and there to make sure it covered the edge cases and work in this new project. I’m finally at the point where I feel comfortable releasing it to the public.

Okay, so how much boilerplate can you really cut out?

I’m glad you asked. I created a before and after example to demonstrate this for you.

Used for both examples:

struct DemoModel: Codable, Equatable {
    let id: UUID
    var title: String
    var subtitle: String
    var imageURL: URL

    init(id: UUID = UUID(), title: String, subtitle: String, imageURL: URL) {
        self.id = id
        self.title = title
        self.subtitle = subtitle
        self.imageURL = imageURL
    }
}

let baseURL = URL(string: "https://networkhandlertestbase.firebaseio.com/DemoAndTests")!
let getURL = baseURL.appendingPathExtension("json")

Before (using URLSession.dataTask)

URLSession.shared.dataTask(with: getURL) { (data, response, error) in
    if let response = response as? HTTPURLResponse {
        if response.statusCode != 200 {
            // probably throw an error here
            print("Received a non 200 http response: \(response.statusCode) in \(#file) line: \(#line)")
            return
        }
    } else {
        // probably throw an error here
        print("Did not receive a proper response code in \(#file) line: \(#line)")
        return
    }

    if let error = error {
        print("There was an error fetching your data: \(error)")
        return
    }

    guard let data = data else {
        // again, probably throw an error here if the data doesn't exist
        return
    }

    do {
        let firebaseResults = try JSONDecoder().decode([String: DemoModel].self, from: data)
        let models = Array(firebaseResults.values).sorted { $0.title < $1.title }
        // do something with your successful result!
        print(models)
    } catch {
        let nullData = "null".data(using: .utf8)
        if data == nullData {
            // there was no actual error, Firebase just returns "null" if there is a request it can't provide data for.
            let models = [DemoModel]()
            // do something with your empty array result!
            print(models)
            return
        }
        // there was an error decoding your data
        [print]("Error loading demo models: \(error)")
    }
}.resume() //and I bet you always forget this! (I know do!)

After (using NetworkHandler)

// can only input URLRequests, but an extension is provided for ease of use
let request = getURL.request
NetworkHandler.default.transferMahCodableDatas(with: request) { (result: Result<[String: DemoModel], NetworkError>) in
    do {
        let firebaseResults = try result.get()
        let models = Array(firebaseResults.values).sorted { $0.title < $1.title }
        // do something with your successful result!
        print(models)
    } catch NetworkError.dataWasNull {
        // there was no actual error, Firebase just returns "null" if there is a request it can't provide data for.
        // do something with your empty array result!
        print(models)
    } catch {
        print("Error loading demo models: \(error)")
    }
}

Personally, I think the difference speaks for itself.

One more thing…

Okay, honestly, this isn’t enough gradeur to justify using that phrase, but I enjoy using it and I’M THE BOSS AROUND HERE.

Anyways, it includes features for mocking network calls built in. I think that’s pretty cool. It also has a few other things that come along for the ride, like NetworkCache (to make subsequent calls to the same URL super zippy), a UIAlertController extension for easy display of user facing errors, some enums to help fill out some stringly typed HTTP stuff, and more!

If you’re interested in checking it out, it’s open source over on GitHub. Additionally, if you want to use it in your own projects, you can simple add the following line to your Cartfile:
github "mredig/NetworkHandler"

Leave a Reply

Your email address will not be published. Required fields are marked *