项目作者: mitchtreece

项目描述 :
Creepy networking library for Swift
高级语言: Swift
项目地址: git://github.com/mitchtreece/Spider.git
创建时间: 2017-01-10T02:39:23Z
项目社区:https://github.com/mitchtreece/Spider

开源协议:MIT License

下载


Spider



Version
Xcode
Swift
iOS
macOS

Spider

Spider is an easy-to-use networking library built for speed & readability. Modern syntax & response handling makes working
with web services so simple - it’s almost spooky.

Installation

SPM

The easiest way to get started is by installing via Xcode.
Just add Spider as a Swift package & choose the modules you want.

If you’re adding Spider as a dependency of your own Swift package,
just add a package entry to your dependencies.

  1. .package(
  2. name: "Spider",
  3. url: "https://github.com/mitchtreece/Spider",
  4. .upToNextMajor(from: .init(2, 0, 0))
  5. )

Spider is broken down into several modules making it quick & easy to pick and choose exactly what you need.

  • Spider: Core classes, extensions, & dependencies
  • SpiderUI: UIKit & SwiftUI classes, extension, & dependencies
  • SpiderPromise: PromiseKit classes, extensions, & dependencies
  • SpiderPromiseUI: UIKit & SwiftUI PromiseKit classes, extensions, & dependencies

CocoaPods

As of Spider 2.2.0, CocoaPods support has been dropped in favor of SPM. If you’re depending on a Spider version prior to 2.2.0, you can still integrate using CocoaPods.

  1. pod 'Spider-Web', '~> 2.0'

Usage

Spider can be used in many different ways. Most times, the shared Spider instance is all you need.

  1. Spider.web
  2. .get("https://path/to/endpoint")
  3. .data { _ in
  4. print("We got a response!")
  5. }

This makes a GET request with a given path, then returns a Response object.

Base URLs

Because we typically make more than one request to a given API, using base URLs just makes sense. This is also useful
when we need to switch between versions of API’s (i.e. dev, pre-prod, prod, etc…).

  1. Spider.web.baseUrl = "https://api.spider.com/v1"
  2. Spider.web
  3. .get("/users")
  4. .data { _ in
  5. print("We got a response!")
  6. }
  7. Spider.web
  8. .get("/locations")
  9. .data { _ in
  10. print("We got another response!")
  11. }

Notice how we can now make requests to specific endpoints with the same shared base url. The above requests would hit
the endpoints:

  1. https://base.url/v1/users
  2. https://base.url/v1/locations

If a base url is not specified, Spider will assume the path of your request is a fully qualified url (as seen in the
first example).

Request Parameters

All variations of Request instantiation have a means for you to pass in request parameters. For example:

  1. let params = ["user_id": "123456789"]
  2. Spider.web
  3. .post(
  4. "https://path/to/endpoint",
  5. parameters: params
  6. )
  7. .data { _ in
  8. print("We got a response!")
  9. }

This will take your parameters and pass them along in the request’s body. For GET requests, parameters will be
encoded into the path as query parameters.

Spider Instances

So far, we have been working with the shared instance of Spider. This is usually all you need. Just in case you need
more control, Spider also supports a more typical instantiation flow.

  1. let tarantula = Spider()
  2. tarantula
  3. .get("https://path/to/endpoint")
  4. .data { _ in
  5. print("Tarantula got a response!")
  6. }

Instead of using the shared Spider instance, we created our own instance named tarantuala and made a request with it.
Scary! Naturally, Spider instances created like this also support base URLs:

  1. let blackWidow = Spider(baseUrl: "https://base.url/v1")
  2. blackWidow
  3. .get("/users")
  4. .data { _ in
  5. print("Black Widow got a response!")
  6. }

Advanced & Multipart Requests

Spider also supports more fine-tuned request options. You can configure and perform a Request manually:

  1. let request = Request(
  2. method: .get,
  3. path: "https://path/to/endpoint",
  4. parameters: nil
  5. )
  6. request.header.accept = [
  7. .image_jpeg,
  8. .custom("custom_accept_type")
  9. ]
  10. request.header.set(
  11. value: "12345",
  12. forHeaderField: "user_id"
  13. )
  14. Spider.web
  15. .perform(request)
  16. .data { _ in
  17. print("We got a response!")
  18. }

Multipart requests can also be constructed & executed in a similar fashion:

  1. let file = MultipartFile(
  2. data: image.pngData()!,
  3. key: "image",
  4. name: "image.png",
  5. type: .image_png
  6. )
  7. let request = MultipartRequest(
  8. method: .put,
  9. path: "https://path/to/upload",
  10. parameters: nil,
  11. files: [file]
  12. )
  13. Spider.web
  14. .perform(request)
  15. .data { _ in
  16. print("We got a response!")
  17. }

MultipartRequest is a Request subclass that is initialized with an array of MultipartFile objects. Everything else works the exact same as a normal request.

Authorization

Currently, Spider supports the following authorization types:

  • Basic (user:pass base64 encoded)
  • Bearer token

Authorization can be added on a per-request or instance-based basis. Typically we would want to provide our Spider
instance authorization that all requests would be sent with:

  1. let authSpider = Spider(
  2. baseUrl: "https://base.url/v1",
  3. authorization: TokenRequestAuth(value: "0123456789")
  4. )
  5. authSpider
  6. .get("/topSecretData")
  7. .data { _ in
  8. print("Big hairy spider got a response!")
  9. }

However, authorization can also be provided on a per-request basis if it better fits your situation:

  1. let token = TokenRequestAuth(value: "0123456789")
  2. let spider = Spider(baseUrl: "https://base.url/v1")
  3. spider
  4. .get(
  5. "/topSecretData",
  6. authorization: token
  7. )
  8. .data { _ in
  9. print("Spider got a response!")
  10. }

Advanced requests can also provide authorization:

  1. let request = Request(
  2. method: .get,
  3. path: "https://path/to/endpoint",
  4. authorization: TokenAuth(value: "0123456789")
  5. )
  6. request.header.accept = [
  7. .image_jpeg,
  8. .custom("custom_accept_type")
  9. ]
  10. request.header.set(
  11. value: "12345",
  12. forHeaderField: "user_id"
  13. )
  14. Spider.web
  15. .perform(request)
  16. .data { _ in
  17. print("We got a response!")
  18. }

By default, authorization is added to the “Authorization” header field. This can be changed by passing in a custom
field when creating the authorization:

  1. let basic = BasicRequestAuth(
  2. username: "root",
  3. password: "pa55w0rd",
  4. field: "Credentials"
  5. )
  6. let authSpider = Spider(
  7. baseUrl: "https://base.url/v1",
  8. authorization: basic
  9. )
  10. authSpider
  11. .get("/topSecretData")
  12. .data { _ in
  13. print("Spider got a response!")
  14. }

The authorization prefix can also be customized if needed. For example, BasicRequestAuth generates the following for the credentials “root:pa55w0rd”

  1. Basic cm9vdDpwYTU1dzByZA==

In this case, the “Basic” prefix before the encoded credentials is the authorization type. This can be customized
as follows:

  1. let basic = BasicRequestAuth(
  2. username: "root",
  3. password: "pa55w0rd"
  4. )
  5. basic.prefix = "Login"
  6. let spider = Spider(
  7. baseUrl: "https://base.url/v1",
  8. authorization: basic
  9. )
  10. spider
  11. .get("/topSecretData")
  12. .data { _ in
  13. print("Got a response!")
  14. }

Likewise, the TokenRequestAuth “Bearer” prefix can be modified in the same way.

Responses

Response objects are clean & easy to work with. A typical data response might look something like the following:

  1. Spider.web
  2. .get("https://some/data/endpoint")
  3. .dataResponse { res in
  4. switch res.result {
  5. case .success(let data): // Handle response data
  6. case .failure(let error): // Handle response error
  7. }
  8. }

Response also has helper value & error properties if you prefer that over the result syntax:

  1. Spider.web
  2. .get("https://some/data/endpoint")
  3. .dataResponse { res in
  4. if let error = res.error {
  5. // Handle the error
  6. return
  7. }
  8. guard let data = res.value else {
  9. // Missing data
  10. return
  11. }
  12. // Do something with the response data
  13. }

Workers & Serialization

When asked to perform a request, Spider creates & returns a RequestWorker instance. Workers are what actually manage the execution of requests, and serialization of responses. For instance, the above example could be broken down as follows:

  1. let worker = Spider.web
  2. .get("https://some/data/endpoint")
  3. worker.dataResponse { res in
  4. if let error = res.error {
  5. // Handle the error
  6. return
  7. }
  8. guard let data = res.value else {
  9. // Missing data
  10. return
  11. }
  12. // Do something with the response data
  13. }

If you’d rather work directly with response values instead of responses themselves, each worker function has a raw value alternative:

  1. Spider.web
  2. .get("https://some/data/endpoint")
  3. .data { (data, error) in
  4. if let error = error {
  5. // Handle the error
  6. return
  7. }
  8. guard let data = data else {
  9. // Missing data
  10. return
  11. }
  12. // Do something with the data
  13. }

In addition to Data, RequestWorker also supports the following serialization functions:

  1. func stringResponse(encoding: String.Encoding, completion: ...)
  2. func string(encoding: String.Encoding, completion: ...)
  3. func jsonResponse(completion: ...)
  4. func json(completion: ...)
  5. func jsonArrayResponse(completion: ...)
  6. func jsonArray(completion: ...)
  7. func imageResponse(completion:)
  8. func image(completion: ...)
  9. func decodeResponse<T: Decodable>(type: T.Type, completion: ...)
  10. func decode<T: Decodable>(type: T.Type, completion: ...)

Custom serialization functions can be added via RequestWorker extensions.

Passthroughs

Sometimes you might want to inspect or kick-off an action inside of your response chain.
For this, a passthrough closure can be added to a request worker:

  1. Spider.web
  2. .get("https://some/data/endpoint")
  3. .voidPassthrough {
  4. // This is executed before delivering
  5. // the response to the handler below.
  6. }
  7. .dataResponse { res in
  8. if let error = res.error {
  9. // Handle the error
  10. return
  11. }
  12. guard let data = res.value else {
  13. // Missing data
  14. return
  15. }
  16. // Do something with the response data
  17. }

Imagine you need to send an error event to your analytics provider when your API sends you back an error:

  1. Spider.web
  2. .get("https://some/data/endpoint")
  3. .jsonResponsePassthrough { res in
  4. if let error = getError(from: res) {
  5. self.analytics.track(error)
  6. }
  7. }
  8. .jsonResponse { res in
  9. guard let json = res.value else {
  10. return
  11. }
  12. // Do something with the response json
  13. }

Middlewares

Responses can also be ran through middleware to validate or transform the returned data if needed.

  1. class ExampleMiddleware: Middleware {
  2. override func next(_ response: Response<Data>) throws -> Response<Data> {
  3. let stringResponse = response
  4. .compactMap { $0.stringResponse() }
  5. switch stringResponse.result {
  6. case .success(let string):
  7. guard !string.isEmpty else {
  8. throw NSError(
  9. domain: "com.example.Spider-Example",
  10. code: -1,
  11. userInfo: [NSLocalizedDescriptionKey: "ExampleMiddleware failed"]
  12. )
  13. }
  14. case .failure(let error):
  15. throw error
  16. }
  17. return response
  18. }
  19. }
  1. Spider.web.middlewares = [ExampleMiddleware()]
  2. Spider.web
  3. .get("https://path/to/endpoint")
  4. .data { _ in
  5. print("We got a response!")
  6. }

Every request performed via the shared Spider instance would now also be ran through our ExampleMiddleware before being handed to the request’s completion closure. Middleware can also be set on a per-request basis:

  1. let request = Request(
  2. method: .get,
  3. path: "https://path/to/endpoint"
  4. )
  5. request.middlewares = [ExampleMiddleware()]
  6. Spider.web
  7. .perform(request)
  8. .data { ... }

Images

Image downloading & caching is supported via SpiderImageDownloader & SpiderImageCache. Spider uses the excellent Kingfisher library to manage image downloading & caching behind-the-scenes.

SpiderImageDownloader

Downloading images with SpiderImageDownloader is easy!

  1. SpiderImageDownloader.getImage("http://url.to/image.png") { (image, isFromCache, error) in
  2. guard let image = image, error == nil else {
  3. // Handle error
  4. }
  5. // Do something with the image
  6. }

The above getImage() function returns a discardable task that can be used to cancel the download if needed:

  1. let task = SpiderImageDownloader.getImage("http://url.to/image.png") { (image, isCachedImage, error) in
  2. ...
  3. }
  4. task.cancel()

By default, SpiderImageDownloader does not cache downloaded images. If you want images to be cached, simply set the cache flag to true when calling the getImage() function.

SpiderImageCache

Caching, fetching, & removing images from the cache:

  1. let imageCache = SpiderImageCache.shared
  2. let image: UIImage = ...
  3. let key = "my_image_key"
  4. // Add an image to the cache
  5. imageCache.cache(image, forKey: key) { ... }
  6. // Fetch an image from the cache
  7. if let image = imageCache.image(forKey: key) { ... }
  8. // Remove an image from the cache
  9. imageCache.removeImage(forKey: key) { ... }

You can also clean the cache:

  1. // Clean the disk cache
  2. imageCache.clean(.disk)
  3. // Clean the memory cache
  4. imageCache.clean(.memory)
  5. // Clean all caches
  6. imageCache.cleanAll()

UI Integrations

Spider also has some nifty UI integrations, like image view loading!

  1. imageView.web.setImage("http://url.to/image.png")

Currently, Spider has integrations for the following UI components:

  • UIImageView / NSImageView

Async / Await

As of Swift 5.5, async/await has been built into the standard library! If you’re targeting iOS 13 or macOS 12 you can use Spider’s async worker variants.

  1. let photo = await Spider.web
  2. .get("https://jsonplaceholder.typicode.com/photos/1")
  3. .decode(Photo.self)
  4. guard let photo = photo else { return }
  5. let image = await Spider.web
  6. .get(photo.url)
  7. .image()
  8. if let image = image {
  9. // Do something with the image!
  10. }

Promises

Spider has built-in support for PromiseKit. Promises help keep your codebase clean & readable by eliminating pesky nested callbacks.

  1. Spider.web
  2. .get("https://jsonplaceholder.typicode.com/photos/1")
  3. .decode(Photo.self)
  4. .then { photo -> Promise<Image> in
  5. return Spider.web
  6. .get(photo.url)
  7. .image()
  8. }
  9. .done { image in
  10. // Do something with the image!
  11. }
  12. .catch { error in
  13. // Handle error
  14. }

Debug Mode

Enabling Spider’s isDebugEnabled flag will print all debug information (including all outgoing requests) to the console.

Contributing

Pull-requests are more than welcome. Bug fix? Feature? Open a PR and we’ll get it merged in! 🎉