Swiftpack.co - cuba/PiuPiu as Swift Package

Swiftpack.co is a collection of thousands of indexed Swift packages. Search packages.
See all packages published by cuba.
cuba/PiuPiu 1.11.1
A swift framework for easily making network calls and serializing them to objects
⭐️ 4
🕓 2 years ago
.package(url: "https://github.com/cuba/PiuPiu.git", from: "1.11.1")

Swift 5 iOS 9 MacOS 10.15 SPM Carthage GitHub Build

PiuPiu Logo

PiuPiu

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!

Updates

1.11.1

  • Add async/await using fetchResult()

1.11.0

  • Add a URLRequestAdapter and URLResponseAdapter

1.10.0

  • Using DispatchGroup for parallel joins
  • Removed deprecated methods on ResponseFuture
    • 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)>
  • Removed methods for joining calls of the same type on a sequence (array) of futures
    • func addingParallelResult(from callback: () -> ResponseFuture<T.Element>) -> ResponseFuture<[T.Element]>
    • func addingSeriesResult(from callback: @escaping (T) throws -> ResponseFuture<T.Element>?) -> ResponseFuture<[T.Element]>
  • Added an initializer for joining many parallel calls on a sequence
  • Rename ResponseFuture embedded type from T to Success (i.e. ResponseFuture<Success>)
  • Rename Response embedded type from T to Body (i.e. Response<Body>)
  • Rename HTTPResponse embedded type from T to Body (i.e. HTTPResponse<Body>)
  • Rename ResponseInterface associated type from T to Body
  • Localize HTTPError
  • Add makeHTTPResponse and decoded functions to ResponseFuture with type Response<Data?>
  • Add decoded function to ResponseFuture with type HTTPResponse<Data?>
  • Add safeDecoded function to ResponseFuture with type HTTPResponse<Data?>
  • Move makeHTTPResponse from ResponseInterface to Response

1.9.0

  • Removed GroupedFailure. First error triggered will fail the future. If you need access to the results use safeParallelJoin instead.
  • Added some more convenience "join" functions on ResponseFuture: addingParallelNullableResult, addingSeriesNullableResult, safeParallelNullableJoin, safeSeriesNullableJoin, parallelNullableJoin, and seriesNullableJoin
  • Deprecated some ResponseFuture functions in favour of ones that take an explicit type
  • Remove useless throwables that never threw anything
  • Remove MockDispatcherError and use ResponseError.noResponse if no callback is set on MockURLRequestDispatcher

1.8.0

  • Dropped SafeResponse in favour or Result
  • Require [CodingKey] to be passed in to the EncodingTransform and DecodingTransform methods
  • Replaced func transform(value: Self.ValueSource) with func toJSON(_ value: Self.ValueSource, codingPath: [CodingKey])
  • Replaced func transform(json: Self.JSONSource) with func from(json: Self.JSONSource, codingPath: [CodingKey])
  • Added map, parallelJoin, seriesJoin, thenResult, safeResult, safeParallelJoin and safeSeriesJoin callbacks to ResponseFuture
  • Added result callback
  • Added addingParallelResult and addingSeriesResult methods to ResponseFutures that encompass a Sequence
  • Deprecated the SafeResponse enum and fulfill, join (series and parallel), thenError, nonFailing methods on ResponseFuture

1.7.0

  • Added support for swift package manager

1.6.0

  • Removed progress callback. This is now replaced with the updated callback which returns a task.
  • Add helper methods for computing progress

1.5.0

  • Download requests returns Response with temporary URL instead of Data
  • Added localizedDescription to StatusCode which returns Apple's translated error message
  • Re-shuffled "Response" objects
    • Response#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
    • You need to manually convert a Response to an HTTPResponse first (see the example below)
  • Errors have been re-organized.
    • 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 added
  • Error handling has been simplified
    • JSONSerializer errors no longer are wrapped by another error
    • Decodable errors are no longer wrapped

1.4.0

  • Change Request protocol to return a URLRequest
  • Replace Dispatcher and NetworkDispatcher with RequestSerializer.
  • Callbacks will only be triggered once. Once a callback is triggered, its reference is released (nullified).
    • This is to prevent memory leaks.
  • Added DataDispatcher, UploadDispatcher, and DownloadDispatcher protocols which use a basic URLRequest.
    • Added URLRequestDispatcher class which implements all 3 protocols.
    • Added weak callbacks on dispatchers including the MockURLRequestDispatcher. You must now have a reference to your dispatcher.
    • Requests are cancelled when the dispatcher is de-allocated.
  • Added cancellation callback to ResponseFuture.
    • This may be manually triggered using cancel or is or is manually triggered when a nil is returned in any join (series only), then or replace or action (init) callback.
    • This callback does not cancel the actual requests but simply stops any further execution of the ResponseFuture after its final cancellation and completion callback.
  • Added a parallel join callback that does not pass a response object. This callback is non-escaping.
  • Slightly better multi-threading support.
    • by default, then is triggered on a background thread.
    • success, response, error, completion, and cancellation callbacks are always synchronized on the main thread.
  • Add progress updates via the progress callback.
  • Add better request mocking tools via the MockURLRequestDispatcher.

1.3.0

  • Rename PewPew to PiuPiu
    • To handle this migration, replace all import PewPew with import PiuPiu
  • Fix build for Carthage
  • Delete unnecessary files

1.2.0

  • Make URLRequestProvider return an optional URL. This will safely handle invalid URLs instead of forcing the developer to use a !.
  • Add JSON array serialization method to BasicRequest

1.1.0

Removed default translations.

1.0.1

Fixed crash when translating caused by renaming the project.

Features

  • ☑ A wrapper around network requests
  • ☑ Uses Futures (ie. Promises) to allow scalability and dryness
  • ☑ Convenience methods for deserializing Decodable and JSON
  • ☑ Easy integration
  • ☑ Handles common http errors
  • ☑ Strongly typed and safely unwrapped responses
  • ☑ Clean!

Installation

SPM

PiuPiu supports SPM

Usage

1. Import PiuPiu into your file

import PiuPiu

2. Instantiate a Dispatcher

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.

3. Making a request

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().

Advanced Usage

URLRequestAdapter

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:

  • Injecting authorization headers
  • Modifying url's to point to some local (mock data)
  • Logging the request

For a better idea of how to use this protocol, take a look at the tests found under the example project.

URLResponseAdapter

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:

  • Converting mock responses (pointing to local files) to fake HTTPResponses (very useful for mock data testing)
  • Logging the response

For a better idea of how to use this protocol, take a look at the tests found under the example project.

Async/await

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.

Separating concerns

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().

Re-usability

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.

Method chaining (not recommended)

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).

Extensions (recommended)

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.

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!

Callbacks and functions

success callback

The 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 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.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 callback

The 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 callback

This 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 callback

This 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 callbacks

This 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 callback

This 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 callback

The 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.

Creating your own 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.

Encoding

PiuPiu has some convenience methods for you to encode objects into JSON and add them to the BasicRequest object.

Encode JSON String

request.setJSONBody(string: jsonString, encoding: .utf8)

Encode JSON Object

let jsonObject: [String: Any?] = [
    "id": "123",
    "name": "Kevin Malone"
]

try request.setJSONBody(jsonObject: jsonObject)

Encode Encodable

try request.setJSONBody(encodable: myCodable)

Wrap Encoding In a ResponseFuture

It might be beneficial to wrap the Request creation in a ResponseFuture. This will allow you to:

  1. Delay the request creation at a later time when submitting the request.
  2. Combine any errors thrown while creating the request in the error callback.
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()

Decoding

Unwrapping 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()

Decode 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()

Decode 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

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.

Custom transforms

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:

  • Convert old DTO objects to newer objects.
  • Different Encoding or Decoding strategies on the same object
  • Filtering arrays

An 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)
    }
}

Included Transforms

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

Memory Management

The ResponseFuture may have 3 types of strong references:

  1. The system may have a strong reference to the 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.
  2. Any callback that references self has a strong reference to self unless [weak self] is explicitly specified.
  3. The developer's own strong reference to the ResponseFuture.

Strong callbacks

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 IBOutlets that the system generates. Use a guard, Use an assert. Use anything but a !.

Mock Dispatcher

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

Future Features

  • ☑ Parallel calls
  • ☑ Sequential calls
  • ☑ A more generic dispatcher. The response object is way too specific
  • ☑ Better multi-threading support
  • ☑ Request cancellation

Dependencies

PiuPiu includes...nothing. This is a light-weight library.

Credits

PiuPiu is owned and maintained by Jacob Sikorski.

License

PiuPiu is released under the MIT license. See LICENSE for details

GitHub

link
Stars: 4
Last commit: 1 year ago
Advertisement: IndiePitcher.com - Cold Email Software for Startups

Release Notes

2 years ago
  • Add async/await

Swiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API | Analytics