A simple networking abstraction inspired by: http://kean.github.io/post/api-client
- Provides RequestBuilder easily create your requests
- Declarative definition of endpoints
- Define your endpoints mostly with relative paths to a base URL.
- Easily adaptable to use with common reactive frameworks (RxSwift, PromiseKit) via extensions
- Comes with Combine support.
- Chain multiple requests easily
- Mocks responses easily
Checkout the docs here
github "DanielCardonaRojas/APIClient" ~> 1.0.1
pod 'APIClient', :git => 'https://github.com/DanielCardonaRojas/APIClient', :tag => '1.0.1', :branch => 'master'
.package(url: "https://github.com/DanielCardonaRojas/APIClient", .upToNextMajor(from: "1.0.0"))
- Create a client object pointing to some base url
lazy var client: APIClient = {
let configuration = URLSessionConfiguration.default
let client = APIClient(baseURL: "https://jsonplaceholder.typicode.com", configuration: configuration)
return client
}()
- Define a declerative API
struct Todo: Codable {
let title: String
let completed: Bool
}
enum API {
enum Todos {
static func get() -> Endpoint<Todo> {
return Endpoint<Todo>(method: .get, path: "/todos/1")
}
}
}
- Consume the API (Comes with Callback and combine API), refer to the section below to integrate with PromiseKit or RxSwift
Callback API
client.request(endpoint, success: { item in
print("\(item)")
}, fail: { error in
print("Error \(error.localizedDescription)")
})
Combine API
// Combine API
let publisher: AnyPublisher<Todo, Error> = client.request(endpoint)
publisher.sink(receiveCompletion: { completion in
if case let .failure(error) = completion {
print("Error \(error.localizedDescription)")
}
}, receiveValue: { value in
print("\(value)")
}).store(in: &disposables)
Alternatively to using the regular combine API of the APIClient class, APIClientPublisher creates a custom publisher from a APIClient and allows to easily chain multiple endpoints creating a sequence dependent requests.
let endpoint: Endpoint<[Post]> = Endpoint(method: .get, path: "/posts")
APIClientPublisher(client: client, endpoint: endpoint).chain({
Endpoint<PostDetail>(method: .get, path: "/posts/\($0.first!.id)")
}).receive(on: RunLoop.main)
.sink(receiveCompletion: { _ in
}, receiveValue: { _ in
expectation.fulfill()
}).store(in: &disposables)
There is no built interceptors in this package but retrying requests and other related effects can be accomplished, using combine built in facilities.
Retrying requests can be accomplished using tryCatch
and has been documented by many authors,
give this a read for more details
// Copied from https://www.donnywals.com/retrying-a-network-request-with-a-delay-in-combine/
.tryCatch({ error -> AnyPublisher<(data: Data, response: URLResponse), Error> in
print("In the tryCatch")
switch error {
case DataTaskError.rateLimitted, DataTaskError.serverBusy:
return dataTaskPublisher
.delay(for: 3, scheduler: DispatchQueue.global())
.eraseToAnyPublisher()
default:
throw error
}
})
.retry(2)
It is easy to add fake responses that will bypass any http calls.
let client: APIClient = ...
MockDataClientHijacker.sharedInstance.registerSubstitute(User.fake(), requestThatMatches: .path(#"/posts/*"#))
client.hijacker = MockDataClientHijacker.sharedInstance
let endpoint = Endpoint<User>(method: .get, path: "/")
client.request(endpoint) // Will return fake User
Integrating PromiseKit can be done through the following extension:
import PromiseKit
extension APIClient {
func request<Response, T>(_ requestConvertible: T,
additionalHeaders headers: [String: String]? = nil,
additionalQuery queryParameters: [String: String]? = nil,
baseUrl: URL? = nil) -> Promise<T.Result>
where T: URLResponseCapable, T: URLRequestConvertible, T.Result == Response {
return Promise { seal in
self.request(requestConvertible, additionalHeaders: headers, additionalQuery: queryParameters, success: { response in
seal.fulfill(response)
}, fail: { error in
seal.reject(error)
})
}
}
}
Use this extension
import RxSwift
extension APIClient {
func request<Response, T>(_ requestConvertible: T,
additionalHeaders headers: [String: String]? = nil,
additionalQuery queryParameters: [String: String]? = nil,
baseUrl: URL? = nil) -> Observable<T.Result>
where T: URLResponseCapable, T: URLRequestConvertible, T.Result == Response {
return Observable.create({ observer in
let dataTask = self.request(requestConvertible, additionalHeaders: headers, additionalQuery: queryParameters, baseUrl: baseUrl, success: { response in
observer.onNext(response)
observer.onCompleted()
}, fail: {error in
observer.onError(error)
})
return Disposables.create {
dataTask?.cancel()
}
})
}
}