Do you use Alamofire? Are you used to create some network layer called APIManager or Alamofire+NameOfProject in every new project? Are you used to spend time deintegrating and taking what you need from previous implementations? Or even coding what you have previously done? What if there is a solution and you should not deal with this anymore?
Now, instead of creating a network layer, you create Interceptors, which are responsible for intercepting the requests at different moments of its life cycle (depending on the requirements). Go to Interceptors for more information.
Moreover, Leash also includes some processes that are common on the network layers, such as encoding, decoding, authentication and more. And, just to clarify, it also uses Alamofire.
Requirements
Xcode 10.0+
Swift 5.0+
Installation
Swift Package Manager
To integrate Leash into your project using Swift Package Manager, specify it in your Package.swift:
To integrate Leash into your project using CocoaPods, specify it in your Podfile:
pod 'Leash', '~> 3.2'
pod 'Leash/Interceptors', '~> 3.2'
pod 'Leash/RxSwift', '~> 3.2'
Carthage
To integrate Leash into your project using Carthage, specify it in your Cartfile:
github "LucianoPolit/Leash" ~> 3.2
Usage
Setup
First, we need to configure a Manager. You can see here all the options available. Here is an example:
let manager = Manager.Builder()
.scheme("http")
.host("localhost")
.port(8080)
.path("api")
.build()
Then, we need a Client to create and execute the requests:
let client = Client(
manager: manager
)
Now, assuming that we have already created an APIEndpoint with all the reachable endpoints, we can execute requests. For example:
client.execute(
APIEndpoint.readAllUsers
) { (response: Response<[User]>) in
// Do whatever you have to do with the response here.
}
Ok, that is good. But, what if we just want to call it like this?
usersClient.readAll { response in
// Do whatever you have to do with the response here.
}
Much simpler, huh? To keep your project as simple and clean as possible, follow the architecture of the example project.
Encoding
Now that you know how to execute the requests you may be asking how to configure the parameters (because Any is accepted). There are different options depending if the endpoint is query or body encodable:
Query types: QueryEncodable or [String: CustomStringConvertible].
Body types: Encodable or [String: Any].
Here is an example with all the possibilities:
enum APIEndpoint {
case first(QueryEncodable)
case second([String: CustomStringConvertible])
case third(Encodable)
case fourth([String: Any])
}
extension APIEndpoint: Endpoint {
var path: String {
return "/it/does/not/matter/"
}
var method: HTTPMethod {
switch self {
case .first: return .get
case .second: return .get
case .third: return .post
case .fourth: return .post
}
}
var parameters: Any? {
switch self {
case .first(let request): return request // This is `QueryEncodable`.
case .second(let request): return request // This is `[String: CustomStringConvertible]`.
case .third(let request): return request // This is `Encodable`.
case .fourth(let request): return request // This is `[String: Any]`.
}
}
}
Three different classes are being used to encode the parameters:
URLEncoding: to encode QueryEncodable and [String: CustomStringConvertible].
In case you want to encode the parameters in a different way, you have to override the method Client.urlRequest(for:).
Decoding
When you execute a request you have to specify the type of the response. Also, this type must conform to Decodable. After executing the request, in case of a successful response, you will have the response with a value of the specified type. So, a simple example could be:
client.execute(
APIEndpoint.readAllUsers
) { (response: Response<[User]>) in
// Here, in case of a successful response,
// the `response.value` is of the type `[User]`.
}
The only available option to serialize the response is through the JSONDecoder. In case you need to do it in a different way, you should implement your own response serializer. Yes, it uses the DataResponseSerializerProtocol provided by Alamofire!
For example, here is how a JSON response handler might be implemented:
An example of the result of these extensions could be:
client.execute(
APIEndpoint.readAllUsers
) { (response: Response<Any>) in
// Here, in case of a successful response,
// the `response.value` is of the type `Any`.
}
Now, you are able to create your own DataResponseSerializer and use all the features of Leash!
Authenticator
Do you need to authenticate your requests? That’s something really simple, lets do it!
class APIAuthenticator {
var accessToken: String?
}
extension APIAuthenticator: Authenticator {
static var header: String = "Authorization"
var authentication: String? {
guard let accessToken = accessToken else { return nil }
return "Bearer \(accessToken)"
}
}
Then, you just need to register the authenticator:
let authenticator = APIAuthenticator()
let manager = Manager.Builder()
{ ... }
.authenticator(authenticator)
.build()
Now, you have all your requests authenticated. But, are you wondering about token expiration? Here is the solution!
Interceptors
Now, it is the moment of the most powerful tool of this framework. The Interceptors gives you the capability to intercept the requests in different moments of its life cycle. There are five different moments to be explicit:
Execution: called before a request is executed.
Failure: called when there is a problem executing a request.
Success: called when there is no problem executing a request.
Completion: called before the completion handler.
Serialization: called after a serialization operation.
Three types of Interceptors are called in every request (Execution, Failure or Success, Completion). Also, there is one more type that is called depending if you are serializing the response or not (Serialization).
The Manager can store as many Interceptors as you need. And, at the moment of being called, it is done one per time, asynchronously, in a queue order (the same order in which they were added). In case that one of any type is completed requesting to finish the operation, no more Interceptors of that type are called.
One of the best advantages is that the Interceptors does not depend between them to work. So, any of them is a piece that can be taken out of your project at any moment (without having to deal with compilation issues). Moreover, you can take any of these pieces and reuse them in any other project without having to make changes at all!
Below I will show you how to interact with the different types with some examples. There are a lot more use cases, it is up to you and your requirements!
Just as a reminder, do not forget to add the Interceptors to the Manager, like this:
The purpose of this Interceptor is to intercept before a request is executed. Let me show you two examples:
First, the simplest one, we need to log every request that is executed:
class LoggerInterceptor: ExecutionInterceptor {
func intercept(
chain: InterceptorChain<Data>
) {
defer { chain.proceed() }
guard let request = try? chain.request.convertible.asURLRequest(),
let method = request.httpMethod,
let url = request.url?.absoluteString
else { return }
Logger.shared.logDebug("👉👉👉 \(method) \(url)")
}
}
Now, one more complex, but not more complicated to implement:
class CacheInterceptor: ExecutionInterceptor {
let controller = CacheController()
func intercept(
chain: InterceptorChain<Data>
) {
// On that case, the cache controller may need to finish
// the operation or not (depending on the policies).
// So, we can easily tell the chain wether the operation
// should be finished or not.
defer { chain.proceed() }
guard let cachedResponse =
try? controller.cachedResponse(for: chain.endpoint)
else { return }
chain.complete(
with: cachedResponse.data,
finish: cachedResponse.finish
)
}
}
Failure
Basically, the purpose of this Interceptor is to intercept when Alamofire retrieves an error. A simple example could be:
Use the Authenticator and an Interceptor! Let me show you how it should look like:
class AuthenticationValidator: CompletionInterceptor {
func intercept(
chain: InterceptorChain<Data>,
response: Response<Data>
) {
guard let error = response.error, case Error.unauthorized = error else {
chain.proceed()
return
}
RefreshTokenManager.shared.refreshTokenIfNeeded { authenticated in
guard authenticated else {
chain.complete(with: Error.unableToAuthenticate)
return
}
do {
try chain.retry()
} catch {
// In almost every case, no error is thrown.
// But, because we prefer the safest way, we use the do-catch.
chain.complete(with: Error.unableToRetry)
}
}
}
}
Serialization
This is the last Interceptor in being called. Also, it is optional. It depends if you are serializing your response or you only need the Data.
The serialization process is built on top of the process of getting the Data from the request. This is why it is called after all the previous Interceptors. Let me show you an example:
We started before with the CacheController, right? Well, now we need to update the cache in case that the response was serialized successfully:
class CacheInterceptor: SerializationInterceptor {
let controller = CacheController()
func intercept<T: DataResponseSerializerProtocol>(
chain: InterceptorChain<T.SerializedObject>,
response: Response<Data>,
result: Result<T.SerializedObject, Swift.Error>,
serializer: T
) {
defer { chain.proceed() }
guard let value = response.value,
(try? result.get()) != nil
else { return }
controller.updateCacheIfNeeded(
for: chain.endpoint,
value: value
)
}
}
RxSwift
Do you use RxSwift? There is an extension for that! Let me show you how to use it:
client.rx.execute(
APIEndpoint.readAllUsers,
type: [User].self
).subscribe { event in
// Do whatever you have to do with the response here.
}
Ok, that is good. But, what if we just want to call it like this?
usersClient.rx.readAll().subscribe { event in
// Do whatever you have to do with the response here.
}
Much simpler, again, huh? As mentioned before, to keep your project as simple and clean as possible, follow the architecture of the example project.
Index
Introduction
Do you use Alamofire? Are you used to create some network layer called
APIManager
orAlamofire+NameOfProject
in every new project? Are you used to spend time deintegrating and taking what you need from previous implementations? Or even coding what you have previously done? What if there is a solution and you should not deal with this anymore?Now, instead of creating a network layer, you create
Interceptors
, which are responsible for intercepting the requests at different moments of its life cycle (depending on the requirements). Go to Interceptors for more information.Moreover,
Leash
also includes some processes that are common on the network layers, such as encoding, decoding, authentication and more. And, just to clarify, it also usesAlamofire
.Requirements
Installation
Swift Package Manager
To integrate
Leash
into your project using Swift Package Manager, specify it in yourPackage.swift
:CocoaPods
To integrate
Leash
into your project using CocoaPods, specify it in yourPodfile
:Carthage
To integrate
Leash
into your project using Carthage, specify it in yourCartfile
:Usage
Setup
First, we need to configure a
Manager
. You can see here all the options available. Here is an example:Then, we need a
Client
to create and execute the requests:Now, assuming that we have already created an
APIEndpoint
with all the reachable endpoints, we can execute requests. For example:Ok, that is good. But, what if we just want to call it like this?
Much simpler, huh? To keep your project as simple and clean as possible, follow the architecture of the example project.
Encoding
Now that you know how to execute the requests you may be asking how to configure the parameters (because
Any
is accepted). There are different options depending if the endpoint is query or body encodable:QueryEncodable
or[String: CustomStringConvertible]
.Encodable
or[String: Any]
.Here is an example with all the possibilities:
Three different classes are being used to encode the parameters:
QueryEncodable
and[String: CustomStringConvertible]
.[String: Any]
.Encodable
.In case you want to encode the parameters in a different way, you have to override the method
Client.urlRequest(for:)
.Decoding
When you execute a request you have to specify the type of the response. Also, this type must conform to
Decodable
. After executing the request, in case of a successful response, you will have the response with a value of the specified type. So, a simple example could be:The only available option to serialize the response is through the JSONDecoder. In case you need to do it in a different way, you should implement your own response serializer. Yes, it uses the DataResponseSerializerProtocol provided by
Alamofire
!For example, here is how a
JSON
response handler might be implemented:To facilitate the process of executing requests, you should add a method like this to the
Client
:An example of the result of these extensions could be:
Now, you are able to create your own
DataResponseSerializer
and use all the features ofLeash
!Authenticator
Do you need to authenticate your requests? That’s something really simple, lets do it!
Then, you just need to register the authenticator:
Now, you have all your requests authenticated. But, are you wondering about token expiration? Here is the solution!
Interceptors
Now, it is the moment of the most powerful tool of this framework. The
Interceptors
gives you the capability to intercept the requests in different moments of its life cycle. There are five different moments to be explicit:Three types of
Interceptors
are called in every request (Execution
,Failure
orSuccess
,Completion
). Also, there is one more type that is called depending if you are serializing the response or not (Serialization
).The
Manager
can store as manyInterceptors
as you need. And, at the moment of being called, it is done one per time, asynchronously, in a queue order (the same order in which they were added). In case that one of any type is completed requesting to finish the operation, no moreInterceptors
of that type are called.One of the best advantages is that the
Interceptors
does not depend between them to work. So, any of them is a piece that can be taken out of your project at any moment (without having to deal with compilation issues). Moreover, you can take any of these pieces and reuse them in any other project without having to make changes at all!Below I will show you how to interact with the different types with some examples. There are a lot more use cases, it is up to you and your requirements!
Just as a reminder, do not forget to add the
Interceptors
to theManager
, like this:Execution
The purpose of this
Interceptor
is to intercept before a request is executed. Let me show you two examples:First, the simplest one, we need to log every request that is executed:
Now, one more complex, but not more complicated to implement:
Failure
Basically, the purpose of this
Interceptor
is to intercept whenAlamofire
retrieves an error. A simple example could be:Success
Basically, the purpose of this
Interceptor
is to intercept whenAlamofire
retrieves a response. Let me show you two examples:We know that, sometimes, the
API
could retrieve a custom error with more information:Maybe, there is no custom error, but we still need to validate the status code of the response:
Completion
The purpose of this
Interceptor
is to intercept before the completion handler is called. Let me show you two examples:Again, the simplest one, we need to log every response:
Now, one more complex, we have to update the
authentication
when expired. There are two options here:Alamofire
.Interceptor
! Let me show you how it should look like:Serialization
This is the last
Interceptor
in being called. Also, it is optional. It depends if you are serializing your response or you only need theData
.The serialization process is built on top of the process of getting the
Data
from the request. This is why it is called after all the previousInterceptors
. Let me show you an example:We started before with the
CacheController
, right? Well, now we need to update the cache in case that the response was serialized successfully:RxSwift
Do you use RxSwift? There is an extension for that! Let me show you how to use it:
Ok, that is good. But, what if we just want to call it like this?
Much simpler, again, huh? As mentioned before, to keep your project as simple and clean as possible, follow the architecture of the example project.
Communication
Author
Luciano Polit, lucianopolit@gmail.com
License
Leash
is available under the MIT license. See the LICENSE file for more info.