BottleRocketStudios / iOS-Hyperspace

An extremely lightweight wrapper around URLSession to make working with APIs a breeze.
Apache License 2.0
47 stars 17 forks source link
networking urlsession

Hyperspace

CI Status Version Carthage compatible SwiftPM compatible Platform codecov License

Hyperspace provides a simple abstraction around URLSession and HTTP. There are a few main goals:

Contents

Usage

1. Create Requests

You have multiple options when creating requests. These include creating static functions to reduce the boilerplate when creating a Request object or simply creating them locally. In addition, you can still create your own custom struct that wraps and vends a Request object if your network requests are complex.

Option 1 - Extending Request

The example below illustrates how to create an extension on Request which can drastically reduce the boilerplate when creating a request to create a new post in something like a social network feed. It takes advantage of the many defaults into Request (all of which are customizable) to keep the definition brief:

extension Request where Response == Post {

    static func createPost(_ post: NewPost) -> Request<Post> {
        return Request(method: .post, url: URL(string: "https://jsonplaceholder.typicode.com/posts")!, headers: [.contentType: .applicationJSON], body: try? HTTP.Body.json(post))
    }
}

Option 2 - Define Each Request Locally

let createPostRequest: Request<Post> = Request(method: .post, url: URL(string: "https://jsonplaceholder.typicode.com/posts")!, headers: [.contentType: .applicationJSON], body: try? HTTP.Body.json(post))

Option 3 - Create a CreatePostRequest that wraps a Request

struct CreatePostRequest {
    let newPost: NewPost

    var request: Request<Post> {
        return Request(method: .post, url: URL(string: "https://jsonplaceholder.typicode.com/posts")!, headers: [.contentType: .applicationJSON], body: try? HTTP.Body.json(post))
    }
}

For the above examples, the Post response type and NewPost body are defined as follows:

struct Post: Decodable {
    let id: Int
    let userId: Int
    let title: String
    let body: String
}
struct NewPost: Encodable {
    let userId: Int
    let title: String
    let body: String
}

2. Create Request defaults (optional)

To avoid having to define default Request property values for every request in your app, it can be useful to rely on the RequestDefaults provided by Hyperspace. These can even be customized:

RequestDefaults.defaultCachePolicy = .reloadIgnoringLocalCacheData // Default cache policy is '.useProtocolCachePolicy'
RequestDefaults.defaultDecoder = MyCustomDecoder() // Default decoder is JSONDecoder()

3. Create a BackendService to execute your requests

We recommend adhering to the Interface Segregation principle by creating separate "controller" objects for each section of the API you're communicating with. Each controller should expose a set of related functions and use a BackendService to execute requests. However, for this simple example, we'll just use BackendService directly as a private property on the view controller:

class ViewController: UIViewController {
    private let backendService = BackendService()

    // Rest of your view controller code...
}

4. Instantiate your Request

Let's say a view controller is supposed to create the post whenever the user taps the "send" button. Here's what that might look like:

@IBAction private func sendButtonTapped(_ sender: UIButton) {
    let title = ... // Get the title from a text view in the UI...
    let message = ... // Get the message from a text view/field in the UI...
    let post = NewPost(userId: 1, title: title, body: message)

    let createPostRequest = CreatePostRequest(newPost: post)

    // Execute the network request...
}

5. Execute the Request using the BackendService

For the above example, here's how you would execute the request and parse the response. While all data transformation happens on the background queue that the underlying URLSession is using, all BackendService completion callbacks happen on the main queue so there's no need to worry about threading before you update UI. Notice that the type of the success response's associated value below is a Post struct as defined in the CreatePostRequest above:

do {
    let post = NewPost(userId: 1, title: title, body: "")
    let createPostRequest = Request<Post>.createPost(post)
    let createdPost = try await backendService.execute(request: createPostRequest)
    // Insert the new post into the UI...

} catch {
    // Alert the user to the error...
}

Example

Clone the repo:

git clone https://github.com/BottleRocketStudios/iOS-Hyperspace.git

From here, you can open up Hyperspace.xcworkspace and run the examples:

Shared Code

Example Targets

Playgrounds

Requirements

Installation

Cocoapods

Hyperspace is available through CocoaPods. To install it, simply add the following line to your Podfile:

pod 'Hyperspace'

Carthage

Add the following to your Cartfile:

github "BottleRocketStudios/iOS-Hyperspace"

Run carthage update and follow the steps as described in Carthage's README.

Swift Package Manager

dependencies: [
    .package(url: "https://github.com/BottleRocketStudios/iOS-Hyperspace.git", from: "5.0.0")
]

Author

Bottle Rocket Studios

License

Hyperspace is available under the Apache 2.0 license. See the LICENSE.txt file for more info.

Contributing

See the CONTRIBUTING document. Thank you, contributors!