Skip to Content
Course content

60: Practice Exercise: Building a Generic Result-Based Networking Layer

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Up until now, we've talked about generics and the Result type in isolation. They're powerful tools, but they really shine when you combine them to solve a recurring architectural problem. The most common one? Networking. Most apps spend a huge amount of time fetching JSON and converting it into models. If you write a separate fetch function for every single model, you're just wasting your time.

Let's build a generic network manager that can handle any Decodable type. I want a system where I can say, "Go fetch this URL and give me back a User," or "Go fetch this other URL and give me a list of Posts," all using the same piece of logic.

Defining the Failure States

Before we touch the networking code, we need to define what can actually go wrong. Using a generic Error is lazy and makes it a nightmare for the UI layer to show a helpful message. I prefer a dedicated enum.

enum NetworkError: Error {
    case badURL
    case requestFailed(Error)
    case invalidResponse
    case decodingError(Error)
}

The Generic Fetch Logic

Here is the core of the operation. I'm using a generic type T that must conform to Decodable. This tells Swift, "I don't know exactly what we're downloading yet, but I promise it's something that can be initialized from JSON."

func fetchResource<T: Decodable>(from urlString: String, completion: @escaping (Result<T, NetworkError>) -> Void) {
    guard let url = URL(string: urlString) else {
        completion(.failure(.badURL))
        return
    }

    URLSession.shared.dataTask(with: url) { data, response, error in
        if let error = error {
            completion(.failure(.requestFailed(error)))
            return
        }

        guard let data = data else {
            completion(.failure(.invalidResponse))
            return
        }

        do {
            let decodedData = try JSONDecoder().decode(T.self, from: data)
            completion(.success(decodedData))
        } catch {
            completion(.failure(.decodingError(error)))
        }
    }.resume()
}

Correcting the "Silent Failure" Mistake

I'll be honest: I've written the code above a hundred times and shipped it to production, only to realize later it had a major flaw. Look closely at the URLSession block. I'm checking if there's an error and if data exists, but I'm completely ignoring the response object.

In the real world, if a server returns a 404 (Not Found) or a 500 (Internal Server Error), URLSession does not return an error. It returns a valid response with a status code and often some HTML body explaining the error. My code above would take that 404 HTML page, try to decode it as JSON, and fail with a decodingError. That's misleading. The decoding didn't fail; the request did.

Let's fix that by casting the response to HTTPURLResponse and checking the status code:

// ... inside dataTask ...
guard let httpResponse = response as? HTTPURLResponse, 
      (200...299).contains(httpResponse.statusCode) else {
    completion(.failure(.invalidResponse))
    return
}

guard let data = data else {
    completion(.failure(.invalidResponse))
    return
}
// ... proceed to decode ...

Wiring it up to a Model

Now that we have a robust, generic engine, using it is incredibly clean. We just define our model and tell the function what we expect back.

struct User: Decodable {
    let id: Int
    let name: String
    let email: String
}

// Notice how we explicitly tell Swift we expect a 'User' here
fetchResource(from: "https://jsonplaceholder.typicode.com/users/1") { (result: Result<User, NetworkError&gt>) in
    switch result {
    case .success(let user):
        print("Fetched user: \(user.name)")
    case .failure(let error):
        print("Error occurred: \(error)")
    }
}

The beauty here is that if I want to fetch a Post or a Comment, I don't have to write any new networking logic. I just create the struct and call the same fetchResource function. We've moved the complexity into one place and left the call site clean.




📋 Practical Task

Exercise: Implementing a Generic List Fetcher for a Movie API

You are tasked with extending the networking layer to handle arrays of data. Create a small program that does the following:

  • Define a Movie struct that conforms to Decodable (include at least title and year).
  • Reuse the fetchResource function from the lesson.
  • Call fetchResource using the URL "https://jsonplaceholder.typicode.com/posts" (since we don't have a real movie API handy, pretend the "posts" endpoint returns a list of movies).
  • Crucial: Ensure the Result type you expect in the completion handler is an array of movies ([Movie]) rather than a single movie.
  • Print the title of the first movie in the returned list upon success, or the specific NetworkError upon failure.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.