Swiftpack.co - Package - BottleRocketStudios/iOS-Hyperspace

Hyperspace

CI Status Version Carthage compatible License Platform codecov codebeat badge

Purpose

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.

Key Concepts

  • 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 in HTTP.
  • NetworkService - Uses a NetworkSession (URLSession by default) to execute URLRequests. Deals with raw HTTP and Data.
  • BackendService - Uses a NetworkService to execute Requests. Transforms the raw Data returned from the NetworkService 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 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.

Option 1 - Adopting the Request protocol

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

Option 2 - Using the AnyRequest<T> struct

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
}

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 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'

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

4. Instantiate your Request

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

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:

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

Example

To run the example project, you'll first need to use Carthage to install Hyperspace's dependency (BrightFutures.

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:

Shared Code

  • Models.swift, Requests.swift
    • Sample models and network requests shared by the various examples.

Example Targets

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

Playgrounds

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

Requirements

  • iOS 8.0+
  • tvOS 9.0+
  • watchOS 2.0+
  • Swift 5.0

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.

NOTE: Don't forget to add both Hyperspace.framework and the BrightFutures.framework dependency to your project.

Swift Package Manager

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

Author

Bottle Rocket Studios

License

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

Github

link
Stars: 33
Help us keep the lights on

Dependencies

Used By

Total: 0

Releases

3.2.1 - Jul 19, 2019

Enhancements

  • None
Bug Fixes

3.2.0 - Jul 11, 2019

Enhancements

Bug Fixes
  • None

3.1.0 - May 7, 2019

Enhancements

Bug Fixes
  • None.

3.0.0 - Jan 3, 2019

Enhancements

  • Fixed CHANGELOG for version 2.0.0/2.1.0. Tyler Milner #73

  • Remove the type definitions deprecated in 2.0.0 Will McGinty #72

  • [BREAKING] Added failing type information to DecodingFailureInitializable allowing the API to make decisions based off of the type that failed to decode and deprecate dynamically keyed decoding. Will McGinty #71

  • [BREAKING] Renamed Request protocol's transformData(_:serviceSuccess:) method to transformSuccess(_:). The redundant data parameter was removed since the NetworkServiceSuccess makes it available as a property. Also simplified method signatures by introducing RequestTransformBlock typealias. Tyler Milner #69 #70

  • Fixed minor typo in CHANGELOG where the PR URL text didn't match the underlying PR number. Tyler Milner #68

Bug Fixes
  • None.

2.1.0 - Sep 20, 2018

Enhancements
  • Two new error-facing protocols were added. NetworkServiceFailureInitializable represents a Swift.Error that can be initialized from a NetworkServiceFailure object. DecodingFailureInitializable represents a Swift.Error that can be initialized from a DecodingError as a result of decoding Data. These conformances have been added as extensions to AnyError (meaning AnyRequest usage is unaffected). As a result of these new protocols, the BackendServiceError type has been removed. Types conforming to Request now have an associated ErrorType which must conform to NetworkServiceFailureInitializable. If a request generates any sort of failure response, the custom error type will be initialized from it instead of returning a generic BackendServiceError. In addition, if Request.ErrorType conforms to DecodingFailureInitializable, the custom error type will be instantiated and returned. Will McGinty #38

  • Added a new initalizer to AnyRequest which accepts a String value designating the key of JSON at which to begin decoding. Will McGinty #41

  • Separated the generation/encoding of the URL query from the Request object into an extension URL. Will McGinty #40

  • Add functionality to NetworkReqest to allow for replacing and adding to the HTTP headers. Will McGinty #43

  • Simplify usage of DecodableContainer types with JSONDecoder Will McGinty #44

  • Add a subsystem which can perform transparent error handling using RequestRecoveryStrategy. Will McGinty #45

  • Simplify usage of dataTransfomer extensions with custom error types Will McGinty #47

  • Add HTTP.HeaderValue for JSON API specification. Earl Gaspard #46

  • Converted HTTP.Statusnested types (HTTP.Status.Success, HTTP.Status.ClientError, etc.) from enums to RawRepresentable structs. This keeps the library more open for extension by allowing clients to more easily specify and use custom HTTP status codes. Tyler Milner #49 #50

  • Implemented synthesized Equatable and Hashable conformance that was introduced in Swift 4.1. Tyler Milner #51

  • Renamed NetworkRequest and AnyNetworkRequest to Request and AnyRequest. Will McGinty #51

  • Add headers property to HTTP.Response. The method signature of Request’s transformData(_:) method has changed. If you implement a custom transformData(_:) method, you will need to replace it with transformData(_:serviceSuccess:). Earl Gaspard #64

Bug Fixes
  • None.