PewPew
Formerly known as NetworkKit, the project was renamed to support CocoaPods.
PewPew adds the concept of Futures
(aka: Promises
) to iOS. It is intended to make netwoking calls cleaner and simpler and provides the developer with more customizability then any other networking framework.
Q: Why should I use this framework? A: Because, you like clean code.
Q: Why the stupid name? A: Because "pew pew" is the sound of lazers. And lazers are from the future.
Q: What sort of bear is best? A: False! A black bear!
- Updates
- Features
- Installation
- Usage
- Future
- Encoding
- Decoding
- Memory Managment
- Custom Encoding
- Custom Decoding
- Mock Dispatcher
- Dependencies
- Credits
- License
Updates
1.1.0
Removed default translations.
To migrate to this you should include your own translations by extending ResponseError
, RequestError
and SerializationError
conforming to LocalizedError
and (optionally) CustomNSError
To re-introduce the previous behaviour, you should include the files found here and the localizations here
1.0.1
Fixed crash when translating
Features
- A wrapper around network requests
- Uses
Futures
(ie.Promises
) to allow scalablity and dryness - Convenience methods for deserializing Decodable and JSON
- Easy integration
- Handles common http errors
- Returns production safe error messages
- Strongly typed and safely unwrapped responses
- Easily extensible. Can easily work with frameworks such as
Alamofire
,ObjectMapper
andMapCodableKit
- Clean!
Installation
Carthage
Carthage is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks.
You can install Carthage with Homebrew using the following command:
$ brew update
$ brew install carthage
To integrate PewPew into your Xcode project using Carthage, specify it in your Cartfile
:
github "cuba/PewPew" ~> 1.0
Run carthage update
to build the framework and drag the built PewPew.framework
into your Xcode project.
Usage
PewPew
into your file
1. Import import PewPew
ServerProvider
2. Implement a The server provider gives the server url. The reason a simple URL is not used is so that you can dynamically change the url. Say for example you have an environment picker. You would have to recreate the dispatcher every time you change the environment. The simplest way to create a ServerProvider is to just implement the protocol on your ViewController.
extension ViewController: ServerProvider {
var baseURL: URL {
return URL(string: "https://example.com")!
}
}
But you may chose to use a seperate object to implement the server provider or create a singleton object so you can share it througout your application. Because the reference to the server provider on the NetworkDispatcher
is weak, you don't have to worry about any circular references.
3. Making a request.
Now that we have our ServerProvider
established, we can start making api calls.
let dispatcher = NetworkDispatcher(serverProvider: self)
let request = BasicRequest(method: .get, path: "/posts")
dispatcher.future(from: request).response({ response in
// Handles all responses including negative responses such as 4xx and 5xx
// The error object is available if we get an
// undesirable status code such as a 4xx or 5xx
if error = response.error {
// Throwing an error in any callback will trigger the `error` callback.
// This allows us to pool all failures in one place.
throw error
}
let post = try response.decode(Post.self)
// Do something with our deserialized object
// ...
}).error({ error in
// Handles any errors during the request process,
// including anything thrown in any of the callback (except this one).
}).completion({
// The completion callback is guaranteed to be called once
// for every time the `start()` or `send()` method is triggered on the future.
//
}).start()
NOTE: Nothing will happen if you don't call start()
.
NOTE: Strange things might happen if you call start()
more than once. Don't do it.
4. Separating concerns and transforming the future
Pun not indended (honestly)
Now lets move the part of the future that decodes our object to another method. This way, our business logic is not mixed up with our serialization logic. One of the great thing about using futures is that we can return them!
Lets create a method similar to this:
private func getPosts() -> ResponseFuture<[Post]> {
let dispatcher = NetworkDispatcher(serverProvider: self)
let request = BasicRequest(method: .get, path: "/posts")
// We create a future and tell it to transform the response using the
// `then` callback.
return dispatcher.future(from: request).then({ response -> [Post] in
// This callback transforms our response to another type
// We can still handle errors the same way as we did before.
if let error = response.error {
// The error is available when a non-2xx response comes in
// Such as a 4xx or 5xx
// You may also parse a custom error object here.
throw error
}
// Return the decoded object. If an error is thrown while decoding,
// It will be caught in the `error` callback.
return try response.decode([Post].self)
})
}
NOTE: We intentionally did not call start()
in this case.
Then we can simply call it like this:
getPosts().response({ posts in
// Handle the success which will give your posts.
responseExpectation.fulfill()
}).error({ error in
// Triggers whenever an error is thrown.
// This includes deserialization errors, unwraping failures, and anything else that is thrown
// in a any other throwable callback.
}).completion({
// Always triggered at the very end to inform you this future has been satisfied.
}).send()
Future
You've already seen that a ResponseFuture
allows you to chain your callbacks, transform the response object and pass it around. But besides the simple examples above, there is so much more you can do to make your code amazingly clean!
dispatcher.future(from: request).then({ response -> Post in
// Handles any responses and transforms them to another type
// This includes negative responses such as 4xx and 5xx
// The error object is available if we get an
// undesirable status code such as a 4xx or 5xx
if let error = response.error {
// Throwing an error in any callback will trigger the `error` callback.
// This allows us to pool all the errors in one place.
throw error
}
return try response.decode(Post.self)
}).replace({ post -> ResponseFuture<EnrichedPost> in
// Perform some operation that itself uses a future
// such as something heavy like markdown parsing.
// Any callback can be transformed to a future.
return self.enrich(post: post)
}).join({ enrichedPost -> ResponseFuture<User> in
// Joins a future with another one returning both results
return self.fetchUser(forId: post.userId)
}).response({ enrichedPost, user in
// The final response callback includes all the transformations and
// Joins we had previously performed.
}).error({ error in
// Handles any errors throw in any callbacks
}).completion({
// At the end of all the callbacks, this is triggered once. Error or no error.
}).send()
Callbacks
response
callback
The response
callback is triggered when the request is recieved and no errors are thrown in any chained callbacks (such as then
or join
).
At the end of the callback sequences, this gives you exactly what your transforms "promised" to return.
dispatcher.future(from: request).response({ response in
// Triggered when a response is recieved and all callbacks succeed.
})
NOTE: This method should ONLY be called ONCE.
error
callback
Think of this as a catch
on a do
block. From the moment you trigger send()
, the error callback is triggered whenever something is thrown during the callback sequence. This includes errors thrown in any other callback.
dispatcher.future(from: request).error({ error in
// Any errors thrown in any other callback will be triggered here.
// Think of this as the `catch` on a `do` block.
})
NOTE: This method should ONLY be called ONCE.
completion
callback
The completion callback is always triggered at the end after all ResponseFuture
callbacks once every time send()
or start()
is triggered.
dispatcher.future(from: request).completion({
// The completion callback guaranteed to be called once
// for every time the `send` or `start` method is triggered on the callback.
})
NOTE: This method should ONLY be called ONCE.
then
callback
This callback transforms the response
type to another type.
dispatcher.future(from: request).then({ response -> Post in
// The `then` callback transforms a successful response to another object
// You can return any object here and this will be reflected on the `success` callback.
return try response.decode(Post.self)
}).response({ post in
// Handles any success responses.
// In this case the object returned in the `then` method.
})
replace
callback
This callback transforms the future to another type using another callback. This allows us to make asyncronous calls inside our callbacks.
dispatcher.future(from: request).then({ response -> Post in
return try response.decode(Post.self)
}).replace({ post -> ResponseFuture<EnrichedPost> in
// Perform some operation operation that itself requires a future
// such as something heavy like markdown parsing.
return self.enrich(post: post)
}).response({ enrichedPost in
// The final response callback has the enriched post.
})
join
callback
This callback transforms the future to another type containing its original results plus the results of the returned callback. This allows us to make asyncronous calls in series.
dispatcher.future(from: request).then({ response -> Post in
return try response.decode(Post.self)
}).join({ post -> ResponseFuture<User> in
// Joins a future with another one returning both results
return self.fetchUser(forId: post.userId)
}).response({ post, user in
// The final response callback includes both results.
})
send
or start
This will start the ResponseFuture
. In other words, the action
callback will be triggered and the requests will be sent to the server.
NOTE: If this method is not called, nothing will happen (no request will be made).
NOTE: This method should ONLY be called AFTER declaring all of your callbacks (success
, failure
, error
, then
etc...)
NOTE: This method should ONLY be called ONCE.
Creating your own ResponseFuture
You can create your own ResponseFuture for a variety of reasons. If you do, you will have all the benefits you have seen so far.
Here is an example of a response future that does decoding in another thread.
return ResponseFuture<[Post]>(action: { future in
// This is an example of how a future is executed and
// fulfilled.
DispatchQueue.global(qos: .userInitiated).async {
// lets make an expensive operation on a background thread.
// The below is just an example of how you can parse on a seperate thread.
do {
// Do an expensive operation here ....
let posts = try response.decode([Post].self)
DispatchQueue.main.async {
// We should syncronyze the result back to the main thread.
future.succeed(with: posts)
}
} catch {
// We can handle any errors as well.
DispatchQueue.main.async {
// We should syncronize the error to the main thread.
future.fail(with: error)
}
}
}
})
NOTE You should ALWAYS syncronize the results on the main thread before succeeding or failing your future.
Encoding
PewPew has some convenience methods for you to encode objects into JSON and add them to the BasicRequest
object.
String
Encode JSON var request = BasicRequest(method: .post, path: "/users")
request.setJSONBody(string: jsonString, encoding: .utf8)
Encode JSON Object
let jsonObject: [String: Any?] = [
"id": "123",
"name": "Kevin Malone"
]
var request = BasicRequest(method: .post, path: "/users")
try request.setJSONBody(jsonObject: jsonObject)
Encodable
Encode var request = BasicRequest(method: .post, path: "/posts")
try request.setJSONBody(encodable: myCodable)
Data
object)
Custom Encoding (By setting the var request = BasicRequest(method: .post, path: "/users")
request.httpBody = myData
Wrap Encoding In a ResponseFuture
It might be beneficial to wrap the Request creation in a ResponseFuture. This will allow you to:
- Delay the request creation at a later time when submitting the request.
- Combine any errors thrown while creating the request in the error callback.
dispatcher.future(from: {
var request = BasicRequest(method: .post, path: "/posts")
try request.setJSONBody(myCodable)
return request
}).error({ error in
// Any error thrown while creating the request will trigger this callback.
}).send()
Decoding
Data
Unwrapping This will unwrap the data object for you or throw a ResponseError if it not there. This is convenent so that you don't have to deal with those pesky optionals.
dispatcher.future(from: request).response({ response in
let data = try response.unwrapData()
// do something with data.
print(data)
}).error({ error in
// Triggered when the data object is not there.
}).send()
String
Decode dispatcher.future(from: request).response({ response in
let string = try response.decodeString(encoding: .utf8)
// do something with string.
print(string)
}).error({ error in
// Triggered when decoding fails.
}).send()
Decodable
Decode dispatcher.future(from: request).response({ response in
let posts = try response.decode([Post].self)
// do something with the decodable object.
print(posts)
}).error({ error in
// Triggered when decoding fails.
}).send()
Memory Managment
The ResponseFuture
may have 3 types of strong references:
- The system may have a strong reference to the
ResponseFuture
aftersend()
is called. This reference is temporary and will be dealocated once the system returns a response. This will never create a circular reference but as the promise is held on by the system, it will not be released until AFTER a response is recieved or an error is triggered. - Any callback that references
self
has a strong reference toself
unless[weak self]
is explicitly specified. - The developer's own strong reference to the
ResponseFuture
.
Strong callbacks
When ONLY 1
and 2
applies to your case, no circular reference is created. However the object reference as self
is held on stongly (temporarily) until the request returns or an error is thrown. You may wish to use [weak self]
in this case but it is not necessary.
dispatcher.future(from: request).then({ response -> [Post] in
// [weak self] not needed as `self` is not called but it doesn't hurt
return try response.decode([Post].self)
}).response({ posts in
self.show(posts)
}).send()
WARNING If you use [weak self]
do not forcefully unwrap self
and never forcefully unwrap anything on self
either. Thats just asking for crashes.
!! DO NOT DO THIS. !! Never do this. Not even if you're a programming genius. It's just asking for problems.
dispatcher.future(from: request).success({ response in
// We are foce unwrapping a text field! DO NOT DO THIS!
let textField = self.textField!
// If we dealocated textField by the time the
// response comes back, a crash will occur
textField.text = "Success"
}).send()
You will have crashes if you force unwrap anything in your callbacks (i.e. usign a !
). We suggest you ALWAYS avoid force unwrapping anything in your callbacks.
Always unwrap your objects before using them. This includes any IBOutlet
s that the system generates. Use a guard, Use an assert. Use anything but a !
.
ResponseFuture
Strong reference to a You may be holding a reference to your ResponseFuture
. This is fine as long as you make the callbacks weak in order to avoid circular references.
self.postResponseFuture = dispatcher.future(from: request).then({ response in
// [weak self] not needed as `self` is not called
let posts = try response.decode([Post].self)
return SuccessResponse<[Post]>(data: posts, response: response)
}).response({ [weak self] response in
// [weak self] needed as `self` is called
self?.show(response.data)
}).completion({ [weak self] in
// [weak self] needed as `self` is called
self?.postResponseFuture = nil
})
// Perform other logic, add delay, do whatever you would do that forced you
// to store a reference to this ResponseFuture in the first place
self.postResponseFuture?.send()
WARNING If you hold strongly to your future but don't make self
weak using [weak self]
you are guaranteed to have a cirucular reference. The following is a bad example that should not be followed:
!! DO NOT DO THIS !!
self.strongResponseFuture = dispatcher.future(from: request).response({ response in
// Both the `ResponseFuture` and `self` are held on by each other.
// `self` will never be dealocated and neither will the future!
self.show(response.data)
}).send()
if self
has a strong reference to your ResponseFuture
and the ResponseFuture
has a strong reference to self
through any callback, you have created a circular reference. Neither will be dealocated.
ResponseFuture
Weak reference to a You may chose to have a weak reference to your ResponseFuture. This is fine, as long as you do it after calling send()
as your object will be dealocated before you get a chance to do this.
self.weakResponseFuture = dispatcher.future(from: request).completion({
// Always triggered
}).send()
// This ResponseFuture may or may not be nil at this point.
// This depends on if the system is holding on to the
// ResponseFuture as it is awaiting a response.
// but the callbacks will always be triggered.
NOTE The following is an example of where we our request will never happen because we lose the referrence to the ResponseFuture before send()
is triggered:
DO NOT DO THIS:
self.weakResponseFuture = dispatcher.future(from: request).completion({
// [weak self]
expectation.fulfill()
})
// WHOOPS!!!!
// Our object is already nil because we have not established a strong reference to it.
// The `send()` method will do nothing and no callback will be triggered.
self.weakResponseFuture?.send()
Custom Encoding
You can extend BasicRequest to add encoding for any type of object.
ObjectMapper
ObjectMapper
is not included in the framework. This is in order to make the framework much lighter for those that don't want to use it. But if you want, you can easily add encoding support for ObjectMapper
. Here is an example how you can add BaseMappable
(Mappable
and ImmutableMappable
) encoding support for objects and arrays:
extension BasicRequest {
/// Add JSON body to the request from a `BaseMappable` object.
///
/// - Parameters:
/// - mappable: The `BaseMappable` object to serialize into JSON.
/// - context: The context of the mapping object
/// - shouldIncludeNilValues: Wether or not we should serialize nil values into the json object
mutating func setJSONBody<T: BaseMappable>(mappable: T, context: MapContext? = nil, shouldIncludeNilValues: Bool = false) {
let mapper = Mapper<T>(context: context, shouldIncludeNilValues: shouldIncludeNilValues)
guard let jsonString = mapper.toJSONString(mappable) else {
return
}
self.setJSONBody(string: jsonString)
}
/// Add JSON body to the request from a `BaseMappable` array.
///
/// - Parameters:
/// - mappable: The `BaseMappable` array to serialize into JSON.
/// - context: The context of the mapping object
/// - shouldIncludeNilValues: Wether or not we should serialize nil values into the json object
mutating func setJSONBody<T: BaseMappable>(mappable: [T], context: MapContext? = nil, shouldIncludeNilValues: Bool = false) {
let mapper = Mapper<T>(context: context, shouldIncludeNilValues: shouldIncludeNilValues)
guard let jsonString = mapper.toJSONString(mappable) else {
return
}
self.setJSONBody(string: jsonString)
}
}
MapCodableKit
MapCodableKit is a lightweight json parsing framework.
Similarly MapCodableKit
support is no longer available on this framework. But like ObjectMapper
You can easily add back support for MapEncodable
.
extension BasicRequest {
/// Add body to the request from a `MapEncodable` object.
///
/// - Parameters:
/// - mapEncodable: The `MapEncodable` object to serialize into JSON.
/// - options: Writing options for serializing the `MapEncodable` object.
/// - Throws: Any serialization errors thrown by `MapCodableKit`.
mutating public func setJSONBody<T: MapEncodable>(mapEncodable: T, options: JSONSerialization.WritingOptions = []) throws {
ensureJSONContentType()
self.httpBody = try mapEncodable.jsonData(options: options)
}
}
Custom Decoding
Similar to encoding, you can also add Decoding support for whatever decoder you are using, including ObjectMapper
by extending the ResponseInterface
ObjectMapper
extension ResponseInterface where T == Data? {
/// Attempt to Decode the response data into an BaseMappable object.
///
/// - Returns: The decoded object
func decodeMappable<D: BaseMappable>(_ type: D.Type, context: MapContext? = nil) throws -> D {
let jsonString = try self.decodeString()
let mapper = Mapper<D>(context: context)
guard let result = mapper.map(JSONString: jsonString) else {
throw SerializationError.failedToDecodeResponseData(cause: nil)
}
return result
}
/// Attempt to decode the response data into a BaseMappable array.
///
/// - Returns: The decoded array
func decodeMappable<D: BaseMappable>(_ type: [D].Type, context: MapContext? = nil) throws -> [D] {
let jsonString = try self.decodeString()
let mapper = Mapper<D>(context: context)
guard let result = mapper.mapArray(JSONString: jsonString) else {
throw SerializationError.failedToDecodeResponseData(cause: nil)
}
return result
}
}
MapCodableKit
MapCodableKit is a lightweight json parsing framework.
extension ResponseInterface where T == Data? {
/// Attempt to deserialize the response data into a MapDecodable object.
///
/// - Returns: The decoded object
func decodeMapDecodable<D: MapDecodable>(_ type: D.Type) throws -> D {
let data = try self.unwrapData()
do {
// Attempt to deserialize the object.
return try D(jsonData: data)
} catch {
// Wrap this error so that we're controlling the error type and return a safe message to the user.
throw SerializationError.failedToDecodeResponseData(cause: error)
}
}
/// Attempt to decode the response data into a MapDecodable array.
///
/// - Returns: The decoded array
func decodeMapDecodable<D: MapDecodable>(_ type: [D].Type) throws -> [D] {
let data = try self.unwrapData()
do {
// Attempt to deserialize the object.
return try D.parseArray(jsonData: data)
} catch {
// Wrap this error so that we're controlling the error type and return a safe message to the user.
throw SerializationError.failedToDecodeResponseData(cause: error)
}
}
}
Mock Dispatcher
Testing network calls is always a pain. That's why we included the MockDispatcher
. It allows you to simulate network responses without actually making network calls.
let url = URL(string: "https://jsonplaceholder.typicode.com")!
let dispatcher = MockDispatcher(baseUrl: url, mockStatusCode: .ok)
let request = BasicRequest(method: .get, path: "/posts")
try dispatcher.setMockData(codable)
/// The url specified is not actually called.
dispatcher.future(from: request).send()
Future Features
- Parallel calls
- Sequential calls:
- Custom translations
- More futuresque request creation
- A more generic dispatcher. The response object is way too specific.
- Better multi-threading support
Dependencies
PewPew includes...nothing. This is a light-weight library.
Credits
PewPew is owned and maintained by Jacob Sikorski.
License
PewPew is released under the MIT license. See LICENSE for details