PiuPiu adds the concept of Futures
(aka: Promises
) to iOS. It is intended to make networking calls cleaner and simpler and provides the developer with more customizability than any other networking framework.
Q: Why should I use this framework? A: Because, you like clean code.
Q: Why the stupid name? A: Because "piu piu" is the sound of lasers. And lasers are from the future.
Q: What sort of bear is best? A: False! A black bear!
fetchResult()
URLRequestAdapter
and URLResponseAdapter
DispatchGroup
for parallel joinsResponseFuture
public func replace<U>(_ successCallback: @escaping (T) throws -> ResponseFuture<U>?) -> ResponseFuture<U>
public func join<U>(_ callback: () -> ResponseFuture<U>) -> ResponseFuture<(T, U)>
public func nonFailing() -> ResponseFuture<SafeResponse<T>>
public func thenError<U>(_ callback: @escaping (SafeResponse<T>) throws -> U) -> ResponseFuture<U>
public func join<U>(_ callback: @escaping (T) throws -> ResponseFuture<U>?) -> ResponseFuture<(T, U)>
func addingParallelResult(from callback: () -> ResponseFuture<T.Element>) -> ResponseFuture<[T.Element]>
func addingSeriesResult(from callback: @escaping (T) throws -> ResponseFuture<T.Element>?) -> ResponseFuture<[T.Element]>
ResponseFuture
embedded type from T
to Success
(i.e. ResponseFuture<Success>
)Response
embedded type from T
to Body
(i.e. Response<Body>
)HTTPResponse
embedded type from T
to Body
(i.e. HTTPResponse<Body>
)ResponseInterface
associated type from T
to Body
makeHTTPResponse
and decoded
functions to ResponseFuture
with type Response<Data?>
decoded
function to ResponseFuture
with type HTTPResponse<Data?>
safeDecoded
function to ResponseFuture
with type HTTPResponse<Data?>
makeHTTPResponse
from ResponseInterface
to Response
GroupedFailure
. First error triggered will fail the future. If you need access to the results use safeParallelJoin
instead.ResponseFuture
: addingParallelNullableResult
, addingSeriesNullableResult
, safeParallelNullableJoin
, safeSeriesNullableJoin
, parallelNullableJoin
, and seriesNullableJoin
ResponseFuture
functions in favour of ones that take an explicit typeMockDispatcherError
and use ResponseError.noResponse
if no callback is set on MockURLRequestDispatcher
SafeResponse
in favour or Result
[CodingKey]
to be passed in to the EncodingTransform
and DecodingTransform
methodsfunc transform(value: Self.ValueSource)
with func toJSON(_ value: Self.ValueSource, codingPath: [CodingKey])
func transform(json: Self.JSONSource)
with func from(json: Self.JSONSource, codingPath: [CodingKey])
map
, parallelJoin
, seriesJoin
, thenResult
, safeResult
, safeParallelJoin
and safeSeriesJoin
callbacks to ResponseFuture
result
callbackaddingParallelResult
and addingSeriesResult
methods to ResponseFutures
that encompass a Sequence
SafeResponse
enum and fulfill
, join
(series and parallel), thenError
, nonFailing
methods on ResponseFuture
progress
callback. This is now replaced with the updated
callback which returns a task
.Response
with temporary URL
instead of Data
localizedDescription
to StatusCode
which returns Apple's translated error messageResponse#error
has been removed and replaced with HTTPResponse#httpError
SuccessResponse
has been renamed to HTTPResponse
ResponseInterface
returns URLResponse
instead of HTTPURLResponse
Response
returns URLResponse
instead of HTTPURLResponse
Response
to an HTTPResponse
first (see the example below)ResponseError
cases have been reduced to 4 and renamed to HTTPError
HTTPError
cases contain more generic HTTP errors instead of specific HTTP errors based on status code.SerializationError
has been moved to ResponseError
and 2 new cases have been addedJSONSerializer
errors no longer are wrapped by another errorDecodable
errors are no longer wrappedRequest
protocol to return a URLRequest
Dispatcher
and NetworkDispatcher
with RequestSerializer
.DataDispatcher
, UploadDispatcher
, and DownloadDispatcher
protocols which use a basic URLRequest
.
URLRequestDispatcher
class which implements all 3 protocols.MockURLRequestDispatcher
. You must now have a reference to your dispatcher.cancellation
callback to ResponseFuture
.
cancel
or is or is manually triggered when a nil is returned in any join
(series only), then
or replace
or action
(init
) callback.ResponseFuture
after its final cancellation
and completion
callback.join
callback that does not pass a response object. This callback is non-escaping.then
is triggered on a background thread.success
, response
, error
, completion
, and cancellation
callbacks are always synchronized on the main thread.progress
callback.MockURLRequestDispatcher
.PewPew
to PiuPiu
import PewPew
with import PiuPiu
URLRequestProvider
return an optional URL. This will safely handle invalid URLs instead of forcing the developer to use a !.Removed default translations.
Fixed crash when translating caused by renaming the project.
Futures
(ie. Promises
) to allow scalability and drynessPiuPiu supports SPM
PiuPiu
into your fileimport PiuPiu
All requests are made through a dispatcher. There are 3 protocols for dispatchers:
DataDispatcher
: Performs standard http requests and returns a ResponseFuture
that contains a Response<Data?>
object. Can also be used for uploading data.DownloadDispatcher
: For downloading data. It returns a ResponseFuture
that contains only a Data
object.UploadDispatcher
: For uploading data. Can usually be replaced with a DataDispatcher
, but offers a few upload specific niceties like better progress updates.For convenience, a URLRequestDispatcher
is provided implementing all 3 protocols.
For tests you can also use MockURLRequestDispatcher
but you will have to provide your own mocks.
class ViewController: UIViewController {
private let dispatcher = URLRequestDispatcher()
// ...
}
You should have a strong reference to this object as it is held on weakly by your callbacks.
Here we have a complete request example, including error handling and decoding.
let url = URL(string: "https://jsonplaceholder.typicode.com/posts/1")!
let request = URLRequest(url: url, method: .get)
dispatcher.dataFuture(from: request)
.success { response in
// Here we handle our response as long as nothing was thrown along the way
// This method is always invoked on the main queue.
// Here we check if we have an HTTP response.
// Anything we throw in this method will be handled on the `error` callback.
// If PiuPiu cannot create an http response, the method will throw an error.
// Unhandled, it will just end up in our `error` callback.
let httpResponse = try response.makeHTTPResponse()
// We also ensure that our HTTP response is valid (i.e. a 1xx, 2xx or 3xx response)
if let error = httpResponse.httpError {
// HTTP errors are not thrown automatically so you get a chance to handle them
// If we want it to be handled in our `error` callback, we simply just throw it
throw error
} else {
// PiuPiu has a convenience method to decode `Decodable` objects
self.post = try response.decode(Post.self)
// now we can present our post
// ...
}
}
.error { error in
// Here we handle any errors that were thrown along the way
// This method is always invoked on the main queue.
// This includes all errors thrown by PiuPiu during the request
// creation/dispatching process as well as any network failures.
// It also includes anything we threw in our previous callbacks
// such as any decoding issues, http errors, etc.
print(error)
}
.completion {
// The completion callback is guaranteed to be called once
// for every time the `start` method is triggered on the future
// regardless of success or error.
// It will always be the last callback to be triggered.
}
.send()
NOTE: Nothing will happen if you don't call start()
or send()
.
The URLRequestAdapter
protocol allows you to change the request or perform some other task before dispatching it.
This is useful for a number of cases including:
For a better idea of how to use this protocol, take a look at the tests found under the example project.
Similar to the URLRequestAdapter
, the URLResponseAdapter
allows you to modify the response or perform some other task before returning the response.
This is useful for a number of cases including:
HTTPResponses
(very useful for mock data testing)For a better idea of how to use this protocol, take a look at the tests found under the example project.
You can use async/await to replace the success
/error
/result
callbacks.
The following is an example of how to use it:
let postURL = URL(string: "https://jsonplaceholder.typicode.com/posts/1")!
let postRequest = URLRequest(url: postURL, method: .get)
Task {
do {
let postResponse = try await dispatcher.dataFuture(from: postRequest)
.decoded(Post.self)
.fetchResult()
let userURL = URL(string: "https://jsonplaceholder.typicode.com/users/\(postResponse.data.userId)")!
let userRequest = URLRequest(url: userURL, method: .get)
let userResponse = try await dispatcher.dataFuture(from: userRequest)
.decoded(User.self)
.fetchResult()
show(post: postResponse.data, user: userResponse.data)
} catch {
show(error)
}
}
NOTE: You should not use success
/error
/result
callbacks. You may still use joins, however there is no need to use series joins with async/await anymore.
Often times we want to separate our responses into different parts so we can handle them differently. We also want to decode on a background thread so that it doesn't make our UI choppy. We can do this using the then
callback.
Each then
callback transforms the response and returns a new one.
dispatcher
.dataFuture {
let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!
return URLRequest(url: url, method: .get)
}
.then { response -> Post in
// Attempt to get a http response
let httpResponse = try response.makeHTTPResponse()
// Check if we have any http error
if let error = httpResponse.httpError {
// Throwing an error in any callback will trigger the `error` callback.
// This allows us to pool all failures in that callback if we want to
throw error
}
// If we have no error, we just return the decoded object
// If anything is thrown, it will be caught in the `error` callback.
return try response.decode(Post.self)
}
.updated { task in
// Sends task updates so you can perform things like progress updates
}
.success { post in
// Handles any success responses.
// In this case the object returned in the `then` method.
}
.error { error in
// Handles any errors during the request process,
// including all request creation errors and anything
// thrown in the `then` or `success` callbacks.
}
.completion {
// The completion callback guaranteed to be called once
// for every time the `start` method is triggered on the callback.
completionExpectation.fulfill()
}
.send()
NOTE: Nothing will happen if you don't call start()
or send()
.
In the above example, we show a number of then
callbacks to transform our response. In the first then
callback we deal with common HTTP errors. In the other, we deal with with decoding a specific Post
object. However none of the code is yet re-usable.
One way to do that is through chaining.
/// This method returns an HTTP response containing a decoded `Post` object
func getPost(id: Int) -> ResponseFuture<HTTPResponse<Post>> {
let url = URL(string: "https://jsonplaceholder.typicode.com/posts/1")!
let request = URLRequest(url: url, method: .get)
return getHTTPResponse(from: request)
.then(on: DispatchQueue.global(qos: .background)) { httpResponse -> HTTPResponse<Post> in
// Here we decode the http response into an object using `Decodable`
// We use the `background` thread because decoding can be somewhat intensive.
// WARNING: Do not use `self` here as
// this `callback` is being invoked on a `background` queue
// PiuPiu has a convenience method to decode `Decodable` objects
return try httpResponse.decoded(Post.self)
}
}
/// This method handles common HTTP errors and returns an HTTP response.
private func getHTTPResponse(from request: URLRequest) -> ResponseFuture<HTTPResponse<Data?>> {
return dispatcher.dataFuture(from: request)
.then { response -> HTTPResponse<Data?> in
// In this callback we handle common HTTP errors
// Here we check if we have an HTTP response.
// Anything we throw in this method will be handled on the `error` callback.
// If PiuPiu cannot create an http response, method will throw an error.
// Unhandled, it will just end up in our `error` callback.
let httpResponse = try response.makeHTTPResponse()
// We also ensure that our HTTP response is valid (i.e. a 1xx, 2xx or 3xx response)
// because there is no point deserializing anything unless we have a valid response
if let error = httpResponse.httpError {
// HTTP errors are not thrown automatically so you get a chance to handle them
// If we want it to be handled in our `error` callback, we simply just throw it
throw error
}
// Everything is good, so we just return our HTTP response.
return httpResponse
}
}
To use this we can just simply call getPost
like so:
getPost(id: 1)
.success { response in
// This method is always invoked on the main queue.
// At this point, we know all of our errors are handled
// and our object is deserialized
let post = response.data
// Do something with our deserialized object ...
print(post)
}
.error { error in
// This method is always invoked on the main queue.
}
.completion {
// The completion callback is guaranteed to be called once
// for every time the `start` method is triggered on the future.
}
.send()
This concept should not be too new for us since we're probably done similar type of chaining using regular callbacks. Our method getPost
creates the request and parses a Post
object. Internally it calls getHTTPResponse
which dispatches our request and handles HTTP
errors.
But we don't need to nest callbacks anymore. It makes our code flatter and easier to follow (once we get used to it).
Another way to handle common logic is to add extensions to futures.
We can perform our getHTTPResponse
logic inside an extension such as this:
extension ResponseFuture where Success == Response<Data?> {
/// This method handles common HTTP errors and returns an HTTP response.
var validHTTPResponse: ResponseFuture<HTTPResponse<Data?>> {
return then(HTTPResponse<Data?>.self) { response -> HTTPResponse<Data?> in
// In this callback we handle common HTTP errors
// Here we check if we have an HTTP response.
// Anything we throw in this method will be handled on the `error` callback.
// If PiuPiu cannot create an http response, method will throw an error.
// Unhandled, it will just end up in our `error` callback.
let httpResponse = try response.makeHTTPResponse()
// We also ensure that our HTTP response is valid (i.e. a 1xx, 2xx or 3xx response)
// because there is no point deserializing anything unless we have a valid response
if let error = httpResponse.httpError {
// HTTP errors are not thrown automatically so you get a chance to handle them
// If we want it to be handled in our `error` callback, we simply just throw it
throw error
}
// Everything is good, so we just return our HTTP response.
return httpResponse
}
}
}
Here we took a slightly different approach. We changed our method to support any Decodable
object. In other words, this method allows us to convert any ResponseFuture
containing an HTTPResponse<Data?>
object to one that contains any Decodable
object.
In addition, we do the decoding on a background thread so it doesn't stall our UI while doing so.
And now, not only can we use these two extensions on our getPost
call, but we can also use it on other calls such as this getUser
call such as this one:
/// This method returns an HTTP response containing a decoded `Post` object
func getPost(id: Int) -> ResponseFuture<HTTPResponse<Post>> {
let url = URL(string: "https://jsonplaceholder.typicode.com/posts/1")!
let request = URLRequest(url: url, method: .get)
return dispatcher.dataFuture(from: request)
.validHTTPResponse
.decoded(Post.self)
}
/// This method returns an HTTP response containing a decoded `User` object
func getUser(id: Int) -> ResponseFuture<HTTPResponse<User>> {
let url = URL(string: "https://jsonplaceholder.typicode.com/users/1")!
let request = URLRequest(url: url, method: .get)
return dispatcher.dataFuture(from: request)
.validHTTPResponse
.decoded(User.self)
}
NOTE: decoded
function is already provided by PiuPiu
.
But these are just some examples. There is an infinite number of combinations you can create. This is why futures are far superior to using simple callbacks.
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!
success
callbackThe success
callback is triggered when the request is received 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.dataFuture(from: request)
.success { response in
// Triggered when a response is received and all callbacks succeed.
}
.send()
NOTE: This method should ONLY be called ONCE.
error
callbackThink 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.dataFuture(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.
}
.send()
NOTE: This method should ONLY be called ONCE.
completion
callbackThe completion callback is always triggered at the end after all ResponseFuture
callbacks once every time send()
or start()
is triggered.
dispatcher.dataFuture(from: request)
.completion {
// The completion callback guaranteed to be called once
// for every time the `send` or `start` method is triggered on the callback.
}
.send()
NOTE: This method should ONLY be called ONCE.
then
or map
callbackThis callback transforms the response
type to another type. This operation is done on a background queue so heavy operations won't lock your main queue.
dispatcher.dataFuture(from: request)
.then([Post].self, on: .main) { response in
// This callback transforms the future from one form to another
// (i.e. it changes the return object)
// Any errors thrown will be handled by the `error` callback
return try response.decode([Post].self)
}
.send()
Note: Although not necessary, you should eagerly specify the return type in the method to make it easier on your compiler.
Note: You may also specify the thread to use. Use main
if you're going to call self that will update any UI. You can also use any background threads for heavier operations.
WARNING: You should avoid calling self in this callback if you're not specifying the main
thread.
replace
callbackThis callback transforms the future to another type using another callback. This allows us to make asynchronous calls inside our callbacks.
dispatcher.dataFuture(from: request)
.replace(EnrichedPost.self) { [weak self] response in
// Perform some operation operation that itself requires a future
// such as something heavy like markdown parsing.
let post = try response.decode(Post.self)
// In this case we're parsing markdown and enriching the post.
return self?.enrich(post: post)
}
.success { enrichedPost in
// The final response callback has the enriched post.
}
.send()
Here is what the EnrichedPost
and enrich
method looks like
typealias EnrichedPost = (post: Post, markdown: NSAttributedString?)
private func enrich(post: Post) -> ResponseFuture<EnrichedPost> {
return ResponseFuture<EnrichedPost>() { future in
DispatchQueue.global(qos: .background).async {
do {
let enrichedPost = try Parser.parse(markdown: post.body)
future.succeed(with: (post, enrichedPost))
} catch {
future.fail(with: error)
}
}
}
}
decoded
Since decoding data is necessary, a convenience decoded
method is added which will use the Decodable
protocol.
You may even provide your own custom JSONDecoder
.
This method is only available on futures that contain Response<Data?>
and HTTPResponse<Data?>
dispatcher.dataFuture(from: request)
.makeHTTPResponse()
.decoded([Post].self)
.success { (posts: HTTPResponse<[Post]>) in
// Here is what happened in order:
// * `dispatcher.dataFuture(from: request)` method gave us a `Result<Data?>` future
// * `makeHTTPResponse()` method transfomed the future to `HTTPResponse<Data?>`
// * `decoded([Post].self)` method transformed the future to `HTTPResponse<[Post]>`
}
.send()
NOTE: You can return nil to stop the request process. Useful when you want a weak self.
safeResult()
The safe result is not useful in itself but very useful when you are joining requests. It will not cause all the requests to fail if this one future fails.
It will return a Result
object in the success block and the error block will not be triggered for everything before the safeResult()
Error callback may still be triggered if an error occurs afterwards, for example if you add a parallelJoin
after safeResult
that parallel join may cause the final future to fail.
The following is an example of using safeResult()
dispatcher.dataFuture(from: request)
.decoded([Post].self)
.safeResult()
.success { (posts: Result<Response<[Post]>, Error>) in
// Here is what happened in order:
// * `dispatcher.dataFuture(from: request)` method gave us a `Result<Data?>` future
// * `decoded([Post].self)` method transformed the future to `HTTPResponse<[Post]>`
// * `safeResult()` method transfomed the future to `Result<HTTPResponse<Data?>, Error>`
// Unlike the parallel call example above, the error callback will never be triggered as we do safeResult right before the success
}
.send()
parallelJoin
or seriesJoin
callbacksThis callback transforms the future to another type containing its original results plus the results of the returned callback.
This callback comes with 2 flavours: parallel and series. You may also use parallelJoin
and seriesJoin
which do the same thing
parallelJoin
callbackThis callback does not wait for the original request to complete, and executes right away. It is useful when you don't need to wait for some other data to comeback before making a request.
dispatcher.dataFuture(from: request)
.makeHTTPResponse()
.decoded([Post].self)
.parallelJoin(HTTPResponse<[User]>.self) {
// Joins a future with another one returning both results.
// Since this callback is non-escaping, you don't have to use [weak self]
let url = URL(string: "https://jsonplaceholder.typicode.com/users")!
let request = URLRequest(url: url, method: .get)
return self.dispatcher.dataFuture(from: request)
.makeHTTPResponse()
.decoded([User].self)
}
.success { (posts: HTTPResponse<[Post]>, users: HTTPResponse<[User]>) in
// The final response callback includes both results.
// Here is what happened in order:
// * `dispatcher.dataFuture(from: request)` method gave us a `Result<Data?>` future
// * `makeHTTPResponse()` method transfomed the future to `HTTPResponse<Data?>`
// * `decoded([Post].self)` method transformed the future to `HTTPResponse<[Post]>`
// * `parallelJoin` gave us a second result with its own changes which transformed the future to (HTTPResponse<[Post]>, HTTPResponse<[User]>)
// If any futures fail, this callbakc will not be called. To prevent that, we need to use a `safeResult()`
}
.send()
Notice that we don't need the user id to do the second call. This is why we can do it in parallel.
Notice that we decode [Post]
before joining. this is not necessary but our convienent decoded
method only exists on Futures of type HTTPResponse<Data?>
or Response<Data?>
. If we joined first, we would not have Response<Data?>
but a touple of (Response<Data?>, HTTPResponse<[User]>)
and then we would be able to use our convenient method. We would have to do something more custom.
Order of operations really really matters. It helps to deal with thing one at a time.
Notice too that unlike the seriesJoin
above we are also calling makeHTTPResponse()
giving us a HTTPResponse
rather than a Response
. There is no need to do this in the example but in reality you can't really decode a json response unless your response is an HTTP response. Plus the http response gives us access to status codes, headers if we need them.
This is why we added a convenient validateHTTPResponse()
method in an example above which does more than give us a HTTPResponse
but also checks if we have a valid status code and throws the appropriate error.
NOTE: This callback will execute right away (it is non-escaping). [weak self]
is therefore not necessary.
seriesJoin
callbackThe series join waits for the first response and passes it to the callback so you can make requests that depend on that response but is obviously much slower than making parallel calls.
dispatcher.dataFuture(from: request)
.decoded(Post.self)
.seriesJoin(Response<User>.self) { [weak self] result in
guard let self = self else {
// We used [weak self] because our dispatcher is referenced on self.
// Returning nil will cancel execution of this promise
// and triger the `cancellation` and `completion` callbacks.
// Do this check to prevent memory leaks.
return nil
}
// Joins a future with another one returning both results.
// Since this callback is non-escaping, you don't have to use [weak self]
let url = URL(string: "https://jsonplaceholder.typicode.com/users/\(result.data.userId)")!
let request = URLRequest(url: url, method: .get)
return self.dispatcher.dataFuture(from: request)
.decoded(User.self)
}
.success { (post: Response<Post>, user: Response<User>) in
// The final response callback includes both responses
// Here is what happened in order:
// * `dispatcher.dataFuture(from: request)` method gave us a `Result<Data?>` future
// * `decoded([Post].self)` method transformed the future to `Result<[Post]>`
// * `seriesJoin` gave us a second result as a touple which transformed the future to (Response<Post>, Response<User>)
}
.send()
NOTE: You can return nil to stop the request process. Useful when you want a [weak self]
.
Notice in this example how we need the id of the user from the post before we can get the user. This is why we decoded the post before joining.
Failures for either the future before the join and the future in the join can cause the whole thing to fail. As a result, we use a safeResult() before the join and inside the join.
safeSeriesJoin
and safeParallelJoin
Because this is so convenient (and often necessary) to not have all your requests fail when joining, safeParallelJoin
and safeSeriesJoin
callbacks are also available. More about this below. These callback do the same thing as parallelJoin and seriesJoin but attach a safeResult() to them.
dispatcher.dataFuture(from: request)
.decoded([Post].self)
.safeParallelJoin(Response<[User]>.self) {
// Joins a future with another one returning both results.
// Since this callback is non-escaping, you don't have to use [weak self]
let url = URL(string: "https://jsonplaceholder.typicode.com/users")!
let request = URLRequest(url: url, method: .get)
return self.dispatcher.dataFuture(from: request)
.decoded([User].self)
}
.success { (posts: Response<[Post]>, users: Result<Response<[User]>, Error>) in
// The final response callback includes both results.
// Here is what happened in order:
// * `dispatcher.dataFuture(from: request)` method gave us a `Result<Data?>` future
// * `decoded([Post].self)` method transformed the future to `Response<[Post]>`
// * `safeParallelJoin` gave us a second result with its own changes which transformed the future to (Response<[Post]>, Result<Response<[User]>, Error>).
// Notice we are no longer calling `safeResponse()` and yet we still get a `Result<Response<[User]>, Error>` for the joined future. This is because safeResponse() is done for us via the `safeParallelJoin`
// Also notice that we don't call `safeResult()` before doing the join. This means that the first response is "unsafe" and will cause everythign to fail if it has an error. But this might be exactly what we want depending on our business rules.
}
.send()
result
This is a callback that groups both the success
and failure
callbacks into one callback using Result<Success, Failure>
. This is useful when you want to treat the success and failure under similar conditions.
dispatcher.dataFuture(from: request)
.result { result in
// This is a convenience callback that will trigger for both success and error.
// This is useful when you need to treat success and error in a similar fashion.
// Doing this will not prevent this future and all other joined futures from failing.
// For that you should use `safeResult()` or `safeParallelJoin` and `safeSeriesJoin`
}
.send()
NOTE: If this method is not called, nothing will happen (no request will be made).
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.
WARNING: This method should ONLY be called AFTER declaring all of your callbacks (success
, failure
, error
, then
, seriesJoin
, parallelJoin
etc...). The compiler won't even let you do anything after calling send()
because send doesn't return itself back.
WARNING: This function MUST be called.
ResponseFuture
You can create your own ResponseFuture for a variety of reasons. This can be used on another future's join
or replace
callback for some nice chaining.
Here is an example of a response future that does an expensive operation in another thread.
return ResponseFuture<UIImage>() { future in
// This is an example of how a future is executed and fulfilled.
DispatchQueue.global(qos: .background).async {
// let's make an expensive operation on a background thread.
// The success, updated and error callbacks will be synced on the main thread
// So no need to sync back to the main thread.
do {
// Do an expensive operation here ....
let resizedImage = try image.resize(ratio: 16/9)
future.succeed(with: resizedImage)
} catch {
future.fail(with: error)
}
}
}
NOTE You can also use the then
callback of an existing future which is performed on a background thread.
PiuPiu has some convenience methods for you to encode objects into JSON and add them to the BasicRequest
object.
String
request.setJSONBody(string: jsonString, encoding: .utf8)
let jsonObject: [String: Any?] = [
"id": "123",
"name": "Kevin Malone"
]
try request.setJSONBody(jsonObject: jsonObject)
Encodable
try request.setJSONBody(encodable: myCodable)
It might be beneficial to wrap the Request creation in a ResponseFuture. This will allow you to:
dispatcher.dataFuture() {
let url = URL(string: "https://jsonplaceholder.typicode.com/posts/1")!
var request = URLRequest(url: url, method: .post)
try request.setJSONBody(post)
return request
}.error { error in
// Any error thrown while creating the request will trigger this callback.
}.send()
Data
This will unwrap the data object for you or throw a ResponseError.unexpectedEmptyResponse
if it not there. This is convenient so that you don't have to deal with those pesky optionals.
dispatcher.dataFuture(from: request).success { 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
dispatcher.dataFuture(from: request).success { response in
let string = try response.decodeString(encoding: .utf8)
// do something with string.
print(string)
}.error { error in
// Triggered when decoding fails.
}.send()
Decodable
dispatcher.dataFuture(from: request).success { 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()
Transforms let you handle custom objects that are not Encodable
or Decodable
or if the default Encodable
or Decodable
logic on the object does not work for you.
For example, let's say we want to change how we encode a TimeZone
so that it encodes or decodes a timezone identifier (example: America/Montreal
). We can use the included TimeZoneTransform
object like this:
struct ExampleModel: Codable {
enum CodingKeys: String, CodingKey {
case timeZoneId
}
let timeZone: TimeZone
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.timeZone = try container.decode(using: TimeZoneTransform(), forKey: .timeZoneId)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(timeZone, forKey: .timeZoneId, using: TimeZoneTransform())
}
}
In the above example, we are passing the TimeZoneTransform()
to the decode
and encode
methods because it conforms to both the EncodingTransform
and DecodingTransform
protocols. We can use the EncodingTransform
and DecodingTransform
individually if we don't need to conform to both. If we want both, we can also use the Transform
protocol which encompasses both. They are synonymous with Encodable
, Decodable
and Codable
.
We can create our own custom transforms by implementing the EncodingTransform
or DecodingTrasform
protocols.
public protocol EncodingTransform {
associatedtype ValueSource
associatedtype JSONDestination: Encodable
func toJSON(Self.ValueSource, codingPath: [CodingKey]) throws -> Self.JSONDestination
}
public protocol DecodingTransform {
associatedtype JSONSource: Decodable
associatedtype ValueDestination
func from(json: Self.JSONSource, codingPath: [CodingKey]) throws -> Self.ValueDestination
}
EncodingTransform
is used when encoding and the DecodingTransform
is used when decoding. You could also implement both by conforming to the Transform
protocol.
There are many use cases for this but the following are a few examples:
Encoding
or Decoding
strategies on the same objectAn example of this implementation can be seen on the included DateTransform
:
public class DateTransform: Transform {
public let formatter: DateFormatter
public init(formatter: DateFormatter) {
self.formatter = formatter
}
public func from(json: String, codingPath: [CodingKey]) throws -> Date {
guard let date = formatter.date(from: json) else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: "Could not convert `\(json)` to `Date` using formatter `\(String(describing: formatter))`"))
}
return date
}
public func toJSON(Date, codingPath: [CodingKey]) throws -> String {
return formatter.string(from: value)
}
}
The following transforms are included:
DateTransform
Converts a String
to a Date
and vice versa using a custom formatter.
TimeZoneTransform
Converts a time zone identifier (example: America/Montreal
) to a TimeZone
and vice versa.
URLTransform
Converts a URL String
(example: https://example.com
) to a URL
object and vice versa.
IntFromStringTransform
Converts a String
to an Int64
in both directions.
StringFromIntTransform
Converts an Int64
(including Int
) to a String
in both directions.
EmptyStringTransform
Will convert an empty string (""
) to a nil.
NOTE: Using decodeIfPresent
will result in a double optional (i.e. ??
). You can solve this by coalescing to a nil
. For example:
self.value = try container.decodeIfPresent(using: EmptyStringTransform(), forKey: .value) ?? nil
The ResponseFuture
may have 3 types of strong references:
ResponseFuture
after send()
is called. This reference is temporary and will be deallocated once the system returns a response. This will never create a circular reference but as the future is held on by the system, it will not be released until AFTER a response is received or an error is triggered.self
has a strong reference to self
unless [weak self]
is explicitly specified.ResponseFuture
.When ONLY 1
and 2
applies to your case, a temporary circular reference is created until the future is resolved. You may wish to use [weak self]
in this case but it is not necessary.
dispatcher.dataFuture(from: request).then({ response -> [Post] in
// [weak self] not needed as `self` is not called
return try response.decode([Post].self)
}).success({ posts in
// [weak self] not needed but may be added. There is a temporary reference which will hold on to self while the request is being made.
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.dataFuture(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. using 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 !
.
Testing network calls is always a pain. That's why we included the MockURLRequestDispatcher
. It allows you to simulate network responses without actually making network calls.
Here is an example of its usage:
private let dispatcher = MockURLRequestDispatcher(delay: 0.5, callback: { request in
if let id = request.integerValue(atIndex: 1, matching: [.constant("posts"), .wildcard(type: .integer)]) {
let post = Post(id: id, userId: 123, title: "Some post", body: "Lorem ipsum ...")
return try Response.makeMockJSONResponse(with: request, encodable: post, statusCode: .ok)
} else if request.pathMatches(pattern: [.constant("posts")]) {
let post = Post(id: 123, userId: 123, title: "Some post", body: "Lorem ipsum ...")
return try Response.makeMockJSONResponse(with: request, encodable: [post], statusCode: .ok)
} else {
return try Response.makeMockResponse(with: request, statusCode: .notFound)
}
})
NOTE: You should have a strong reference to your dispatcher and a weak reference to self
in the callback
PiuPiu includes...nothing. This is a light-weight library.
PiuPiu is owned and maintained by Jacob Sikorski.
PiuPiu is released under the MIT license. See LICENSE for details
link |
Stars: 4 |
Last commit: 1 year ago |
Swiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API | Analytics