This library provides a simple abstraction around URLSession and HTTP. There are a few main goals:
- Wrap up all the HTTP boilerplate (method, headers, status codes, etc.) to allow your app to deal with them in a type-safe way.
- Provide a thin wrapper around URLSession:
- Make error handling more pleasant.
- Make it easy to define the details of your request and the model type you want to get back.
- Keep things simple.
- There are currently around 800 SLOC, with about a quarter of that being boilerplate HTTP definitions.
- Of course, complexity will increase over time as new features are added, but we're not trying to cover every possible networking use case here.
- HTTP - Contains standard HTTP definitions and types. If you feel something is missing from here, please submit a pull request!
- Request - A protocol that defines the details of a request, including the desired result type. This is basically a thin wrapper around
URLRequest
, utilizing the definitions inHTTP
. - NetworkService - Uses a
NetworkSession
(URLSession
by default) to executeURLRequests
. Deals with rawHTTP
andData
. - BackendService - Uses a
NetworkService
to executeRequests
. Transforms the rawData
returned from theNetworkService
into the response model type defined by theRequest
. This is the main worker object your app will deal with directly.
You have two options to create requests - create your own struct or class that conforms to the Request
protocol or by utilize the built-in AnyRequest<T>
type-erased struct. Creating your own structs or classes is a bit more explicit, but can help encourage encapsulation and testability if your requests are complex. The AnyRequest<T>
struct is generally fine to use for most cases.
The CreatePostRequest
in the example below represents a simple request to create a new post in something like a social network feed:
struct CreatePostRequest: Request {
// Define the model we want to get back
typealias ResponseType = Post
typealias ErrorType = AnyError
// Define Request property values
var method: HTTP.Method = .post
var url = URL(string: "http://jsonplaceholder.typicode.com/posts")!
var headers: [HTTP.HeaderKey: HTTP.HeaderValue]? = [.contentType: .applicationJSON]
var body: Data? {
let encoder = JSONEncoder()
return try? encoder.encode(newPost)
}
// Define any custom properties needed
private let newPost: NewPost
// Initializer
init(newPost: NewPost) {
self.newPost = newPost
}
}
let createPostRequest = AnyRequest<Post>(method: .post,
url: URL(string: "http://jsonplaceholder.typicode.com/posts")!,
headers: [.contentType: .applicationJSON],
body: postBody)
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
}
To avoid having to define default Request
property values for every request in your app, it can be useful to extend Request
with the defaults you want every request to have:
extension Request {
var cachePolicy: URLRequest.CachePolicy {
return .reloadIgnoringLocalCacheData
}
var timeout: TimeInterval {
return 60.0
}
}
Alternatively, you can also modify the values of RequestDefaults
directly:
RequestDefaults.defaultTimeout = 60 // Default timeout is 30 seconds
RequestDefaults.defaultCachePolicy = .reloadIgnoringLocalCacheData // Default cache policy is '.useProtocolCachePolicy'
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 funtions 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...
}
Let's say our 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...
}
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:
backendService.execute(request: createPostRequest) { [weak self] result in
debugPrint("Create post result: \(result)")
switch result {
case .success(let post):
// Insert the new post into the UI...
case .failure(let error):
// Alert the user to the error...
}
}
To run the example project, you'll first need to use Carthage to install Hyperspace's dependencies (Result and SwiftLint).
After installing Carthage, clone the repo:
git clone https://github.com/BottleRocketStudios/iOS-Hyperspace.git
Next, use Carthage to install the dependencies:
carthage update
From here, you can open up Hyperspace.xcworkspace
and run the examples:
Models.swift
,Requests.swift
- Sample models and network requests shared by the various examples.
- Hyperspace-iOSExample
ViewController.swift
- View a simplified example of how you might use this in your iOS app.
- Hyperspace-tvOSExample
ViewController.swift
- View a simplified example of how you might use this in your tvOS app (this is essentially the same as the iOS example).
- Hyperspace-watchOSExample Extension
InterfaceController.swift
- View a simplified example of how you might use this in your watchOS app.
- Playground/Hyperspace.playground
- View and run a single file that defines models, network requests, and executes the requests similar to the example targets above.
- Playground/Hyperspace_AnyRequest.playground
- The same example as above, but using the
AnyRequest<T>
struct.
- The same example as above, but using the
- Playground/Hyperspace_DELETE.playground
- An example of how to deal with requests that don't return a result. This is usually common for DELETE requests.
- iOS 8.0+
- tvOS 9.0+
- watchOS 2.0+
- Swift 5.0
Hyperspace is available through CocoaPods. To install it, simply add the following line to your Podfile:
pod 'Hyperspace'
Hyperspace is available under the Apache 2.0 license. See the LICENSE.txt file for more info.