/SimpleApiClient-ios

A configurable api client based on Alamofire4 and RxSwift4 for iOS

Primary LanguageSwiftMIT LicenseMIT

SimpleApiClient

Swift CI Status Version License Platform

A configurable api client based on Alamofire4 and RxSwift4 for iOS

Requirements

  • iOS 8.0+
  • Swift 4

Table of Contents

Installation

SimpleApiClient is available through CocoaPods. To install it, simply add the following line to your Podfile:

pod 'SimpleApiClient'

Step 1

Configurate the api client

let config = SimpleApiClient.Config(
  baseUrl: "https://api.github.com",
  defaultParameters: ["foo": "bar"],
  defaultHeaders: ["foo": "bar"],
  timeout: 120, // default is 60s
  certificatePins: [
    CertificatePin(hostname: "https://api.github.com", certificateUrl: Bundle.main.url(forResource: "serverCert", withExtension: "cer")!)
  ],
  errorMessageKeyPath: "message",
  jsonDecoder: JSONDecoder(),  // default is JSONDecoder()
  isMockResponseEnabled: true, // default is false
  logHandler: { request, response in
    ...
  },
  errorHandler: { error in
    // you can centralize the handling of general error here
    switch error {
    case .authenticationError(let code, let message):
      ...
    case .clientError(let code, let message):
      ...
    case .serverError(let code, let message):
      ...
    case .networkError(let source):
      ...
    case .sslError(let source):
      ...
    case .uncategorizedError(let source):
      ...
    }
  }
)

let githubClient = SimpleApiClient(config: config)

Step 2

Create the API

import SimpleApiClient

struct GetRepoApi: SimpleApi {
  let user: String
  let repo: String
  
  var path: String {
    return "/repos/\(user)/\(repo)"
  }
  
  var method: HTTPMethod {
    return .get
  }
  
  // optional
  var parameters: Parameters {
    return [:]
  }
  
  // optional
  var headers: HTTPHeaders {
    return [:]
  }
}

extension SimpleApiClient {
  func getRepo(user: String, repo: String) -> Observable<Repo> {
    return request(api: GetRepoApi(user: user, repo: repo))
  }
}

Step 3

Use observe() to enqueue the call, do your stuff in corresponding parameter block. All blocks run on main thread by default and are optional.

githubClient.getRepo(user: "foo", repo: "bar")
  .observe(
    onStart: { print("show loading") },
    onEnd: { print("hide loading") },
    onSuccess: { print("sucess: \($0)") },
    onError: { print("error: \($0)" }
  )

Model

The library uses JSONDecoder to deserialize JSON to model object, so the model should conform to Decodable or Codable

struct User: Decodable {
  let name: String
  let profileUrl: URL
  // if the serialized name is different from the property name
  private enum CodingKeys: String, CodingKey {
    case name = "login"
    case profileUrl = "avatar_url"
  }
}

Sometimes the api response includes metadata that we don't need, but in order to map the response we create a wrapper class and make the function return that wrapper class. This approach leaks the implementation of service to calling code.

Assuming the response json looks like the following:

{
  total_count: 33909,
  incomplete_results: false,
  foo: {
    bar: {
      items: [
        {
          login: "foo",
          ...
        }
        ...
      ]
    }
  }
}

And you only need the items part, implement Unwrappable to indicate which part of response you want.

struct GetUsersApi: SimpleApi, Unwrappable {
  ... 
  
  var responseKeyPath: String {
    return "foo.bar.items"
  }
}

// then your response will be a list of User
extension SimpleApiClient {
  func getUsers(query: String) -> Observable<[User]> {
    return request(api: GetUsersApi(query: query))
  }
}

To upload file(s), make the API implements Uploadable to provide Multiparts

struct UploadImageApi: SimpleApi, Uploadable {
  ...
  
  var multiParts: [MultiPart] {
    let multiPart = MultiPart(data: UIImageJPEGRepresentation(image, 1)!, name: "imagefile", filename: "image.jpg", mimeType: "image/jpeg")
    return [multiPart]
  }
}

extension SimpleApiClient {
  func uploadImage(image: UIImage) -> Observable<Image> {
    return request(api: UploadImageApi(image))
  }
}

Serial

githubClient.foo()
  .then { foo in githubClient.bar(foo.name) }
  .observe(...)

Serial then Parallel

githubClient.foo()
  .then { foo in githubClient.bar(foo.name) }
  .thenAll { bar in 
    (githubClient.baz(bar.name), githubClient.qux(bar.name)) // return a tuple
  }
  .observe(...)

Parallel

SimpleApiClient.all(
  githubApi.foo(),
  githubApi.bar()
)
.observe(...)

Parallel then Serial

SimpleApiClient.all(
  githubApi.foo(),
  githubApi.bar()
).then { array -> // the return type is Array<Any>, you should cast them, e.g. let foo = array[0] as! Foo
  githubApi.baz()
}.observe(...)
githubClient.getUsers("foo")
  .retry(delay: 5, maxRetryCount: 3) // retry up to 3 times, each time delays 5 seconds
  .retry(exponentialDelay: 5, maxRetryCount: 3) // retry up to 3 times, each time delays 5^n seconds, where n = {1,2,3}
  .observe(...)

Auto Call Cancellation

The call will be cancelled when the object is deallocated.

githubClient.getUsers("foo")
  .cancel(when: self.rx.deallocated)
  .observe(...)

Cancel call manually

let call = githubClient.getUsers("foo").observe(...)

call.cancel()

To enable response mocking, set SimpleApiClient.Config.isMockResponseEnabled to true and make the API implements Mockable to provide MockResponse.

Mock sample json data

To make the api return a successful response with provided json

struct GetUsersApi: SimpleApi, Mockable {
  ...
  
  var mockResponse: MockResponse {
    let file = Bundle.main.url(forResource: "get_users", withExtension: "json")!
    return MockResponse(jsonFile: file)
  }
}

Mock status

To make the api return a client side error with provided json

struct GetUsersApi: SimpleApi, Mockable {
  ...
  
  var mockResponse: MockResponse {
    let file = Bundle.main.url(forResource: "get_users_error", withExtension: "json")!
    return MockResponse(jsonFile: file, status: .clientError)
  }
}

the parameter jsonFile of MockResponse is optional, you can set the status only, then you receive empty string.

Possible Status values:

public enum Status {
  case success
  case authenticationError
  case clientError
  case serverError
  case networkError
  case sslError
}

To mock a response with success status only, you should return Observable<Nothing>.

struct DeleteRepoApi: SimpleApi, Mockable {
  ...
  
  var mockResponse: MockResponse {
    return MockResponse(status: .success)
  }
}

extension SimpleApiClient {
  func deleteRepo(id: String) -> Observable<Nothing> {
    return request(api: DeleteRepoApi(id: id))
  }
}

Author

jaychang0917, jaychang0917@gmail.com

License

SimpleApiClient is available under the MIT license. See the LICENSE file for more info.