A great example use case of using Swift’s associatedtype.

I like how this pattern:

  • binds the request and response types at compile time
  • lets the implementation of submit be ignorant of the response type; it just needs to know that it’s the associatedtype of the request
  • lets the caller of submit not have to explicitly state the response type but still access its data
enum APIRequestMethod {
    case get
    case post
}

protocol APIRequest {
    associatedtype Response: Decodable
    var path: String { get }
    var method: APIRequestMethod { get }
    var parameters: [String: Any] { get }
}

struct APILoginRequest: APIRequest {
    let path: String = "/api/login"
    let method: APIRequestMethod
    let parameters: [String : Any]

    struct Response: Decodable {
        let username: String
        let userToken: String
    }
}

enum APIClientResponse<T> {
    case success(T)
    case error(Error)
}

enum APIClientError: Error {
    case invalidData
}

class APIClient {
    func submit<T>(_ request: T, complete: (APIClientResponse<T.Response>) -> Void) where T: APIRequest {
        let decoder = JSONDecoder()

        let fakeResponseData = "{\"username\": \"fake_dude\", \"userToken\": \"fake_token\"}".data(using: .utf8)
        guard let data = fakeResponseData else {
            complete(.error(APIClientError.invalidData))
            return
        }

        do {
            let response: T.Response = try decoder.decode(T.Response.self, from: data)
            complete(.success(response))
        } catch {
            complete(.error(error))
        }
    }
}


let client = APIClient()
let request = APILoginRequest(method: .get, parameters: [:])
client.submit(request) { (response) in
    switch response {
    case .success(let response):
        print("login success: \(response.username) [\(response.userToken)]")
    case .error(let error):
        print("error: \(error.localizedDescription)")
    }
}