APEReactiveNetworking is simply a reactive oriented
, lightweight
networking library, made by Apegroup
We focused on building a network lib that was real-world, use-case oriented (looking at what our existing app projects actually used/needed from a networking lib) rather than implementing all sorts of functions any given project would possibly use.
It's lightweight because we deliberately did not implement features, available in other networking libs, that are seldom used, for example multipart request-body support. Why create waste?
It's reactive based because we built it on top of ReactiveSwift, which is an aswesome lib that we think will be the best Reactive lib for the Apple platforms.
APEReactiveNetworking is implemented purley in Swift 3
and powering the magic of Swift Generics. Its used in production by BLOX iOS app among others.
- Feather-weight
- 100% Swift 3
- Powering Swift Generics
- Reactive oriented, based on ReactiveSwift
- Automatic retry mechanism (exponential back-off strategy)
- Deterministic response time (successful, error or timeout), i.e. abort after 'X' seconds
- Automatically updates the network activity indicator
- Possibility to add custom request headers
- Access to all HTTP response headers
- Automatically adds device info, such as OS-version, as a few X-headers in all requests
- PUT, POST, DELETE, GET, PATCH operations
- Possibility to customize response code validation (default implementation accepts 200-299 codes)
- Example project available
- Code coverage at X %
- Support for starting the retry mechanism from a custom value, e.g. reading the Retry-After header and starting from that value (retries are currently performed using an exponential backoff strategy between each retry, starting from 1)
- Support for background download/upload by the OS
- Async image downloads for cell updating (extension of UIImage?)
- Add cookie support
- A custom URLSession with request timeout set to 10 s
- Add HTTPS + SSL certificate validation support
- Consider response caching (using HTTP headers: ETag, If-Modified-Since, Last-Modified)
- Extend the Example project
- Add more test cases
- Requirements
- Installation
- Usage
- Example
- Author
- Constribution
- License
-
iOS 9.0 or greater
-
Xcode 8 (Swift 3.0) or later
-
Dependencies:
You can use Carthage to install APEReactiveNetworking
by adding it to your Cartfile
:
github "apegroup/APEReactiveNetworking"
An endpoint is a type that conforms to the Endpoint
protocol and describes the endpoints that your client communicates with.
public protocol Endpoint {
var httpMethod: Http.Method { get }
var absoluteUrl: String { get }
var acceptedResponseCodes: [Http.StatusCode] { get }
}
Example of conforming to the Endpoint
protocol:
enum UserAPI: Endpoint {
//MARK: - Endpoints
case getUser(id: String)
case getUsers
case createUser
case updateUser(id: String)
case deleteUser(id: String)
//MARK: - Endpoint protocol conformance
var httpMethod : Http.Method {
switch self {
case .updateUser, .createUser: return .post
case .deleteUser: return .delete
default: return .get
}
}
var absoluteUrl: String {
let baseUrl = "https://api.apegroup.com"
let path: String
switch self {
case .getUser(let userId), .updateUser(let userId), .deleteUser(let userId):
path = "/users/\(userId)"
case .getUsers, .createUser:
path = "/users/"
}
return baseUrl + path
}
var acceptedResponseCodes : [Http.StatusCode] {
switch self {
case .deleteUser: return [.noContent]
default: return [.ok, .created]
}
}
}
Build your request by using a HttpRequestBuilder
.
public protocol HttpRequestBuilder {
init(endpoint: Endpoint)
func setHeader(_ header: (key: String, value: String)) -> HttpRequestBuilder
func setBody(data: Data, contentType: Http.ContentType) -> HttpRequestBuilder
func build() -> ApeURLRequest
}
ApeRequestBuilder
, a type conforming to the 'HttpRequestBuilder' protocol, is provided by the framework:
let endpoint = UserAPI.getUsers
let requestBuilder: HttpRequestBuilder = ApeRequestBuilder(endpoint: endpoint)
let request: ApeURLRequest = requestBuilder.build()
When using the built-in ApeRequestBuilder
the following http headers will be included in each request:
- "X-Client-OS" // The name of the operating system running on the device represented by the receiver, e.g. "iOS"
- "X-Client-OS-Version" // The current version of the operating system, e.g. "10.0"
- "X-Client-Device-Type" // The device model name, e.g. "iPhone 6s Plus"
- "X-Client-Device-VendorId" // An alphanumeric string that is the same for apps that come from the same vendor running on the same device, or "unknown" if unavailable, e.g. "9C32B72F-8532-4F73-BA76-18C9E522E539"
To authenticate using basic authentication with a username "ape" and password "group" you only need to do this:
let endpoint = UserAPI.getUsers
let requestBuilder = try ApeRequestBuilder(endpoint: endpoint).setAuthorizationHeader(username: "ape", password: "group")
To authenticate using a bearer token "ASDFASDFASDF12345" you only need to do this:
let endpoint = UserAPI.getUsers
let requestBuilder = ApeRequestBuilder(endpoint: endpoint).setAuthorizationHeader(token: "ASDFASDFASDF12345")
To authenticate using a custom authentication header, for example "Token token=ASDFASDFASDF12345" you would need to set the following header field: Authorization: Token token=ASDFASDFASDF12345
. Simply do this:
let endpoint = UserAPI.getUsers
let requestBuilder = ApeRequestBuilder(endpoint: endpoint).setAuthorizationHeader(headerValue: "Token token=ASDFASDFASDF12345")
There are two ways of setting your own http headers.
By setting all the headers (which will replace existing key-values):
let customHeaders: Http.RequestHeaders = ["CustomKey1": "CustomValue1", "CustomKey2" : "CustomValue2"]
let requestBuilder = ApeRequestBuilder(endpoint: endpoint).setHeaders(customHeaders)
Or by adding a single header entry (if the header already exists it will be replaced by the new value):
let requestBuilder = ApeRequestBuilder(endpoint: endpoint).setHeader(("CustomKey","CustomValue"))
let jsonBody: [String : Any] = ["key":"value"]
let requestBuilder = ApeRequestBuilder(endpoint: endpoint).setBody(json: jsonBody)
let requestBuilder = ApeRequestBuilder(endpoint: endpoint).setBody(text: "plain text body")
let image: UIImage = ...
let imageData = UIImagePNGRepresentation(image)!
let requestBuilder = ApeRequestBuilder(endpoint: endpoint).setBody(data: imageData, contentType: Http.ContentType.imagePng)
Requests are sent using the Network
API.
Using the default URLSessionConfiguration
let network = Network()
Using a custom URLSessionConfiguration
let customConfiguration = URLSessionConfiguration()
let customSession = URLSession(configuration: customConfiguration)
let network = Network(session: customSession)
To send a request, simply create a request operation (i.e. a SignalProducer), by calling the send method.
(Remember to import ReactiveSwift
and to start the signal producer)
import ReactiveSwift
let endpoint = UserAPI.getUsers
let requestBuilder = ApeRequestBuilder(endpoint: endpoint)
let request = requestBuilder.build()
let network = Network()
let signalProducer: SignalProducer<Http.ResponseHeaders, Network.OperationError> = network.send(request)
signalProducer.start()
Response handling is performed in a reactive manner by providing closures to the SignalProducer
import ReactiveSwift
let endpoint = UserAPI.getUsers
let requestBuilder = ApeRequestBuilder(endpoint: endpoint)
let request = requestBuilder.build()
let network = Network()
By default, only the response http headers are returned by the SignalProducer
let signalProducer: SignalProducer<Http.ResponseHeaders, Network.OperationError> = network.send(request)
signalProducer.on(
failed: { (error: Network.OperationError) in
print("Network operation failed with error: \(error)")
}, completed: {
print("Successfully sent a request!")
}).start()
If you expect response data to be returned, simply pass an additional parameter, parseDataBlock
, to the method Network.send()
. The parseDataBlock specifies how to convert the raw response data to your desired model type.
The SignalProducer will send a generic wrapper object parameterized with your model type 'T', NetworkDataResponse<T>
, which contains:
- the raw response data
- your parsed data model
- the http response headers
struct MyModel {
init(from: Data) {}
}
let signalProducer: SignalProducer<NetworkDataResponse<MyModel>, Network.OperationError> =
network.send(request, parseDataBlock: { (data: Data) -> MyModel? in
return MyModel(from: data)
})
signalProducer.on(value: { (response: NetworkDataResponse<MyModel>) in
let rawData: Data = response.rawData
let model: Model = response.parsedData
let responseHeaders: Http.ResponseHeaders = response.responseHeaders
print("Received response data successfully")
}}).start()
APEReactiveNetworking defines the following error type:
public enum OperationError : Error {
case parseFailure
case missingData
case invalidResponseType
case unexpectedResponseCode(httpCode: Http.StatusCode, data: Data?)
case requestFailure(error: Error)
case timedOut
}
Error handling is performed in the usual failed closure of the SignalProducer:
signalProducer.on(failed: { (operationError: Network.OperationError) in
switch operationError {
case .requestFailure(let error):
print("Request failed with error: \(error)")
case let .unexpectedResponseCode(code, data):
print("Received unexpected response code: \(code.rawValue)")
case .timedOut:
print("Operation timed out")
case .missingData:
print("Expected response data is missing")
case .parseFailure:
print("Failed to parse the response data to MyModel")
case .invalidResponseType:
print("Expected HTTPURLResponse")
}
}).start()
It is possible configure the network operation in the following ways:
- Setting a timeout limit before aborting the operation
- Specifying a Scheduler to which the returned SignalProducer will forward events to
The default abort-after time is 10 seconds, i.e. no mather how many retries or sleep between each retry, the signal will abort after the specified number of seconds and return a TimeOut
error. It is possible to override this value by providing the abortAfter
parameter in the Network.send()
method.
let maxSecondsAllowed: TimeInterval = 5
Network().send(request, abortAfter: maxSecondsAllowed).start()
The default scheduler to which the SignalProducer will forward events to is the UIScheduler. It is possible to override this value by providing the scheduler
parameter in the Network.send()
method.
let myScheduler: SchedulerProtocol = QueueScheduler()
Network().send(request, scheduler: myScheduler).start()
What about caching?
This network layer, deliberately, does not concern itself with caching, it relies on the cache settings of the URLSession
sent as a parameter in the Network
constructor (defaults to using the default URLSession). So you if you want custom cache behaviour, you can either create your own URLSession
with a custom URLSessionConfiguration
and pass it into the Network constructor or you can configure the shared URLCache
settings, eg in your AppDelegate.
let URLCache = URLCache(memoryCapacity: 4 * 1024 * 1024, diskCapacity: 20 * 1024 * 1024, diskPath: nil)
URLCache.setSharedURLCache(URLCache)
let cachesDirectory = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first
let cachePath = cachesDirectory?.appending("MyCache")
let cache = URLCache(memoryCapacity: 16384, diskCapacity: 268435456, diskPath: cachePath)
let defaultSessionConfiguration = URLSessionConfiguration.default
defaultSessionConfiguration.urlCache = cache
defaultSessionConfiguration.requestCachePolicy = .useProtocolCachePolicy
let urlSession = URLSession(configuration: defaultSessionConfiguration)
let network = Network(session: urlSession)
Here is a trivial example, for more elaborate example take a look at the included example project.
func updateUserProfile(userId: String, firstname: String) -> SignalProducer<Http.ResponseHeaders, Network.OperationError> {
let endpoint = UserAPI.updateUser(id: userId)
let jsonBody: [String : Any] = ["firstname": firstname]
let request = ApeRequestBuilder(endpoint: endpoint)
.setAuthorizationHeader(token: "<my secret auth token>")
.setBody(json: jsonBody)
.build()
return Network().send(request)
}
Apegroup AB, Stockholm, Sweden
All people are welcome to contribute. See CONTRIBUTING for details.
APEReactiveNetworking is released under the MIT license. See LICENSE for details.