Hyperspace provides a simple abstraction around URLSession and HTTP. There are a few main goals:
Keep things simple.
Keep the overall library size to a minimum. Of course, there will be some boilerplate involved (such as the HTTP definitions), but our main goal is to keep the library highly functional and maintainable without over-engineering.
Tailor the library to the networking use cases that we encounter the most often. We will continue to add features based on the common needs across all of the apps that we build.
Contents
HTTP - Contains standard HTTP definitions and types. If you feel something is missing from here, please submit a pull request.
Request - A struct that defines the details of a network request, including the desired result and error types. This is basically a thin wrapper around URLRequest, utilizing the definitions in HTTP.
TransportService - Uses a TransportSession (URLSession by default) to execute URLRequests. Deals with raw HTTP and Data.
BackendService - Uses a TransportService to execute Requests. Transforms the raw Data returned from the TransportService into the response model type defined by the Request. This is the main worker object your app will deal with directly.
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:
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...
}
Hyperspace
Hyperspace provides a simple abstraction around URLSession and HTTP. There are a few main goals:
HTTP
definitions), but our main goal is to keep the library highly functional and maintainable without over-engineering.Contents
URLRequest
, utilizing the definitions inHTTP
.TransportSession
(URLSession
by default) to executeURLRequests
. Deals with rawHTTP
andData
.TransportService
to executeRequests
. Transforms the rawData
returned from theTransportService
into the response model type defined by theRequest
. This is the main worker object your app will deal with directly.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 aRequest
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 intoRequest
(all of which are customizable) to keep the definition brief:Option 2 - Define Each
Request
LocallyOption 3 - Create a
CreatePostRequest
that wraps aRequest
For the above examples, the
Post
response type andNewPost
body are defined as follows: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 theRequestDefaults
provided by Hyperspace. These can even be customized: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 useBackendService
directly as aprivate
property on the view controller: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:
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 aPost
struct as defined in theCreatePostRequest
above:Example
Clone the repo:
From here, you can open up
Hyperspace.xcworkspace
and run the examples:Shared Code
Models.swift
,Requests.swift
Example Targets
ViewController.swift
ViewController.swift
InterfaceController.swift
Playgrounds
Requirements
Installation
Cocoapods
Hyperspace is available through CocoaPods. To install it, simply add the following line to your Podfile:
Carthage
Add the following to your Cartfile:
Run
carthage update
and follow the steps as described in Carthage’s README.Swift Package Manager
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!