Survival Swift: Simple Networking Example

Posted by Grego on November 23, 2021

Foreword

I decided to build a small reference of useful Swift snippets I’m calling Survival Swift. Common stuff we do in many projects.

Rather than Googling I figured I’ll save some time by hosting the answers myself with some home grown examples of my own. I’ll eventually build these into a separate page but for now they will make nice micro posts.

I’ll keep the copy short and let the code do the talking since most will probably skip to that part anyway.

Network Fetch in Swift

Here’s a small example of the most basic—and generic (<T>)—way to handle a simple network get request in Swift/Foundation.

The API

We’ll use a sample REST API I found online: https://dog.ceo/api/breeds/image/random – it returns random dog photos :)

How to call it

First I’ll show how to use the sample code, then the full code for the networking.

Using the generic method directly

Using the generic method directly requires us to type annotate our closure parameters.

// Using explicit types in closure since the network client is using generic types
// this is the only way the compiler can infer the Decodable type since calling
// fetch<DogPhoto>(urlString:) is strictly prohibited
NetworkClient.fetch(urlString: "https://dog.ceo/api/breeds/image/random") { (dogPhoto: DogPhoto?, error: Error?) -> Void in
  print(">> Dog Photo: \(dogPhoto)")
}

If you want to avoid that then make a separate client…

Using in a custom network client

Or we can call our generic function from our own custom function and get the type inference to do that for us:

enum DogPhotoClient {
  static let dogUrl = "https://dog.ceo/api/breeds/image/random"

  /// Requests random dog photos from dog photo service
  static func fetchDogPhoto(completion: @escaping (DogPhoto?, Error?) -> Void) {
    NetworkClient.fetch(
      urlString: Self.dogUrl,
      completion: completion)
  }
}

Since our completion parameter specifies the types we can just pass the completion closure directly to our underlying fetch call and get free type inference.

The Codable DogPhoto response struct

Here we use the additional optional CodingKeys sub enum to rename some of the api’s JSON keys to a more suitable semantic name.

Note: if we only want to use this for decoding we could conform to the Decodable protocol and likewise the Encodable protocol if we only need to encode. Read more on Codable at Apple.com.

A sample response looks like this:

{
  "message": "https://images.dog.ceo/breeds/wolfhound-irish/n02090721_2116.jpg",
  "status": "success"
}

Let’s model a struct after the response.

/// Represents a DogPhoto api response
struct DogPhoto: Codable {
  let status: String
  let photoUrl: String

  /// Custom encoding/decoding keys (optional).
  /// Used to rename the api's response keys to our own internal names.
  enum CodingKeys: String, CodingKey {
    /// maps the `message` key in the json to the `photoUrl`
    /// field of our response struct
    case photoUrl = "message"

    /// since CodingKeys must be exhaustive, we provide status with no override
    case status
  }
}

The Network client

This code does the heavy lifting

enum NetworkClient {
  /// Reusable URLSession for making requests
  private static let session: URLSession = {
    URLSession(configuration: .default)
  }()

  typealias Response<T: Decodable> = (T?, Error?) -> Void
  /// fetches any kind of `Decodable` object from given url string
  static func fetch<T: Decodable>(urlString: String, completion: @escaping Response<T>) {
    guard let url = URL(string: urlString) else {
      completion(nil, Err.badUrl(urlString))
      return
    }

    session.dataTask(with: url) { data, response, error in
      guard let data = data else {
        completion(nil, error ?? Err.noData)
        return
      }

      guard let decoded: T = decode(data: data) else {
        completion(nil, Err.decodeFailed)
        return
      }

      completion(decoded, nil)
    }
    // must call resume on the task to fire off the network request
    .resume()
  }

  /// decodes response data into a `Decodable` object
  private static func decode<T: Decodable>(data: Data) -> T? {
    do {
      return try JSONDecoder().decode(T.self, from: data)
    }
    catch {
      // the real error handling is done by our caller (fetch)
      print(error)
    }

    return nil
  }

  /// Various errors to return from our service
  enum Err: Error {
    /// not a valid URL
    case badUrl(_: String)

    /// no data was returned
    case noData

    ///
    case decodeFailed
  }
}