Spider: Creepy Networking Library for Swift
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
CocoaPods
Spider is integrated with CocoaPods!
- Add the following to your
Podfile
:
use_frameworks!
pod 'Spider-Web'
- In your project directory, run
pod install
- Import the
Spider
module wherever you need it - Profit
Manually
You can also manually add the source files to your project.
- Clone this git repo
- Add all the Swift files in the
Spider/
subdirectory to your project - Profit
The Basics
Spider can be used in many different ways. Most times, the shared Spider instance is all you need.
Spider.web
.get("https://path/to/endpoint")
.data { _ in
print("We got a response!")
}
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...).
Spider.web.baseUrl = "https://api.spider.com/v1"
Spider.web
.get("/users")
.data { _ in
print("We got a response!")
}
Spider.web
.get("/locations")
.data { _ in
print("We got another response!")
}
Notice how we can now make requests to specific endpoints with the same shared base url. The above requests would hit the endpoints:
https://base.url/v1/users
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:
let params = ["user_id": "123456789"]
Spider.web
.post(
"https://path/to/endpoint",
parameters: params
)
.data { _ in
print("We got a response!")
}
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.
let tarantula = Spider()
tarantula
.get("https://path/to/endpoint")
.data { _ in
print("Tarantula got a response!")
}
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:
let blackWidow = Spider(baseUrl: "https://base.url/v1")
blackWidow
.get("/users")
.data { _ in
print("Black Widow got a response!")
}
Advanced & Multipart Requests
Spider also supports more fine-tuned request options. You can configure and perform a Request
manually:
let request = Request(
method: .get,
path: "https://path/to/endpoint",
parameters: nil
)
request.header.accept = [
.image_jpeg,
.custom("custom_accept_type")
]
request.header.set(
value: "12345",
forHeaderField: "user_id"
)
Spider.web
.perform(request)
.data { _ in
print("We got a response!")
}
Multipart requests can also be constructed & executed in a similar fashion:
let file = MultipartFile(
data: image.pngData()!,
key: "image",
name: "image.png",
type: .image_png
)
let request = MultipartRequest(
method: .put,
path: "https://path/to/upload",
parameters: nil,
files: [file]
)
Spider.web
.perform(request)
.data { _ in
print("We got a response!")
}
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:
let authSpider = Spider(
baseUrl: "https://base.url/v1",
authorization: TokenRequestAuth(value: "0123456789")
)
authSpider
.get("/topSecretData")
.data { _ in
print("Big hairy spider got a response!")
}
However, authorization can also be provided on a per-request basis if it better fits your situation:
let token = TokenRequestAuth(value: "0123456789")
let spider = Spider(baseUrl: "https://base.url/v1")
spider
.get(
"/topSecretData",
authorization: token
)
.data { _ in
print("Spider got a response!")
}
Advanced requests can also provide authorization:
let request = Request(
method: .get,
path: "https://path/to/endpoint",
authorization: TokenAuth(value: "0123456789")
)
request.header.accept = [
.image_jpeg,
.custom("custom_accept_type")
]
request.header.set(
value: "12345",
forHeaderField: "user_id"
)
Spider.web
.perform(request)
.data { _ in
print("We got a response!")
}
By default, authorization is added to the "Authorization" header field. This can be changed by passing in a custom field when creating the authorization:
let basic = BasicRequestAuth(
username: "root",
password: "pa55w0rd",
field: "Credentials"
)
let authSpider = Spider(
baseUrl: "https://base.url/v1",
authorization: basic
)
authSpider
.get("/topSecretData")
.data { _ in
print("Spider got a response!")
}
The authorization prefix can also be customized if needed. For example, BasicRequestAuth
generates the following for the credentials "root:pa55w0rd"
Basic cm9vdDpwYTU1dzByZA==
In this case, the "Basic" prefix before the encoded credentials is the authorization type. This can be customized as follows:
let basic = BasicRequestAuth(
username: "root",
password: "pa55w0rd"
)
basic.prefix = "Login"
let spider = Spider(
baseUrl: "https://base.url/v1",
authorization: basic
)
spider
.get("/topSecretData")
.data { _ in
print("Got a response!")
}
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:
Spider.web
.get("https://some/data/endpoint")
.dataResponse { res in
switch res.result {
case .success(let data): // Handle response data
case .failure(let error): // Handle response error
}
}
Response
also has helper value
& error
properties if you prefer that over the result syntax:
Spider.web
.get("https://some/data/endpoint")
.dataResponse { res in
if let error = res.error {
// Handle the error
return
}
guard let data = res.value else {
// Missing data
return
}
// Do something with the response data
}
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:
let worker = Spider.web
.get("https://some/data/endpoint")
worker.dataResponse { res in
if let error = res.error {
// Handle the error
return
}
guard let data = res.value else {
// Missing data
return
}
// Do something with the response data
}
If you'd rather work directly with response values instead of responses themselves, each worker function has a raw value alternative:
let worker = Spider.web
.get("https://some/data/endpoint")
worker.data { (data, error) in
if let error = error {
// Handle the error
return
}
guard let data = data else {
// Missing data
return
}
// Do something with the data
}
In addition to Data
, RequestWorker
also supports the following serialization functions:
func stringResponse(encoding: String.Encoding, completion: ...)
func string(encoding: String.Encoding, completion: ...)
func jsonResponse(completion: ...)
func json(completion: ...)
func jsonArrayResponse(completion: ...)
func jsonArray(completion: ...)
func imageResponse(completion:)
func image(completion: ...)
func decodeResponse<T: Decodable>(type: T.Type, completion: ...)
func decode<T: Decodable>(type: T.Type, completion: ...)
Custom serialization functions can be added via RequestWorker
extensions.
Middleware
Responses can also be ran through middleware to validate or transform the returned data if needed.
class ExampleMiddleware: Middleware {
override func next(_ response: Response<Data>) throws -> Response<Data> {
let stringResponse = response.compactMap { $0.stringResponse() }
switch stringResponse.result {
case .success(let string):
guard !string.isEmpty else {
throw NSError(
domain: "com.example.Spider-Example",
code: -1,
userInfo: [NSLocalizedDescriptionKey: "ExampleMiddleware failed"]
)
}
case .failure(let error): throw error
}
return response
}
}
Spider.web.middlewares = [ExampleMiddleware()]
Spider.web
.get("https://path/to/endpoint")
.data { _ in
print("We got a response!")
}
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:
let request = Request(
method: .get,
path: "https://path/to/endpoint"
)
request.middlewares = [ExampleMiddleware()]
Spider.web
.perform(request)
.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!
SpiderImageDownloader.getImage("http://url.to/image.png") { (image, isFromCache, error) in
guard let image = image, error == nil else {
// Handle error
}
// Do something with the image
}
The above getImage()
function returns a discardable task that can be used to cancel the download if needed:
let task = SpiderImageDownloader.getImage("http://url.to/image.png") { (image, isCachedImage, error) in
...
}
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:
let imageCache = SpiderImageCache.shared
let image: UIImage = ...
let key = "my_image_key"
// Add an image to the cache
imageCache.cache(image, forKey: key) { ... }
// Fetch an image from the cache
if let image = imageCache.image(forKey: key) { ... }
// Remove an image from the cache
imageCache.removeImage(forKey: key) { ... }
You can also clean the cache:
// Clean the disk cache
imageCache.clean(.disk)
// Clean the memory cache
imageCache.clean(.memory)
// Clean all caches
imageCache.cleanAll()
UI Integrations
Spider also has some nifty UI integrations, like image view loading!
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.
let photo = await Spider.web
.get("https://jsonplaceholder.typicode.com/photos/1")
.decode(Photo.self)
guard let photo = photo else { return }
let image = await Spider.web
.get(photo.url)
.image()
if let image = image {
// Do something with the image!
}
Promises
Spider has built-in support for PromiseKit. Promises help keep your codebase clean & readable by eliminating pesky nested callbacks.
Spider.web
.get("https://jsonplaceholder.typicode.com/photos/1")
.decode(Photo.self)
.then { photo -> Promise<Image> in
return Spider.web
.get(photo.url)
.image()
}
.done { image in
// Do something with the image!
}
.catch { error in
// Handle error
}
Async / Await
As of Swift 5.5, async/await has been built into the standard library! If you're targeting iOS 15 or macOS 12 you can use Spider's async worker variants.
do {
let photo = try await Spider.web
.get("https://jsonplaceholder.typicode.com/photos/1")
.decode(Photo.self)
let image = try await Spider.web
.get(photo.url)
.image()
// Do something with the image!
}
catch {
// Handle error
}
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!