🌏 A zero-dependency networking solution for building modern and secure iOS, watchOS, macOS and tvOS applications.

Overview

A zero-dependency networking solution for building modern and secure iOS, watchOS, macOS and tvOS applications.

🚀 TermiNetwork was tested in a production environment with a heavy load of asynchronous requests and tens of thousands of unique clients per day.




This is a low resolution diagram of TermiNetwork that shows how its componets are connected to each other.


Features

➡️  Multi-environment setup
➡️  Model deserialization with Codables
➡️  Choose the response type you want: Codable, UIImage, Data or String
➡️  UIKit/SwiftUI helpers for downloading remote images
➡️  Organize your Requests with Routers
➡️  Reachability
➡️  Transformers: convert REST models to DOMAIN models
➡️  Error Handling
➡️  Interceptors
➡️  Mock Responses
➡️  Certificate Pinning
➡️  Flexible Configuration
➡️  Middleware
➡️  File/Data Upload/Download
➡️  Pretty printed debug information

Table of contents

Installation

You can install TermiNetwork with one of the following ways...

CocoaPods

Add the following line to your Podfile and run pod install in your terminal:

pod 'TermiNetwork', '~> 2.1.1'

Carthage

Add the following line to your Carthage and run carthage update in your terminal:

github "billp/TermiNetwork" ~> 2.1.1

Swift Package Manager

Go to File > Swift Packages > Add Package Dependency and add the following URL :

https://github.com/billp/TermiNetwork

Demo Application

To see all the features of TermiNetwork in action, download the source code and run the TermiNetworkExamples scheme.

Usage

Simple usage (Request)

Let's say you have the following Codable model:

struct Todo: Codable {
   let id: Int
   let title: String
}

The following example creates a request that adds a new Todo:

let params = ["title": "Go shopping."]
let headers = ["x-auth": "abcdef1234"]

Request(method: .post,
	url: "https://myweb.com/api/todos",
	headers: headers,
	params: params)
    .success(responseType: Todo.self) { todo in
	print(todo)
    }
    .failure { error in
	print(error.localizedDescription)
    }

Parameters Explanation

method

One of the following supported HTTP methods:

.get, .head, .post, .put, .delete, .connect, .options, .trace or .patch
responseType

One of the following supported response types

Codable.self (implementations), UIImage.self, Data.self or String.self
onSuccess

A callback that returns an object of the given type. (specified in responseType parameter)

onFailure

A callback that returns a Error and the response Data (if any).

Advanced usage of Request with Configuration and custom Queue

The following example uses a custom Queue with maxConcurrentOperationCount and a configuration object. To see the full list of available configuration properties, take a look at Configuration properties in documentation.

let myQueue = Queue(failureMode: .continue)
myQueue.maxConcurrentOperationCount = 2

let configuration = Configuration(
    cachePolicy: .useProtocolCachePolicy,
    timeoutInterval: 30,
    requestBodyType: .JSON
)

let params = ["title": "Go shopping."]
let headers = ["x-auth": "abcdef1234"]

Request(method: .post,
        url: "https://myweb.com/todos",
        headers: headers,
        params: params,
        configuration: configuration)
    .queue(queue)
    .success(responseType: String.self) { response in
        print(response)
    }
    .failure { error in
        print(error.localizedDescription)
    }

The request above uses a custom queue myQueue with a failure mode of .continue (default), which means that the queue continues its execution if a request fails.

Complete setup with Environments and Routers

The complete and recommended setup of TermiNetwork consists of defining Environments and Routers.

Environment setup

Create a swift enum that implements the EnvironmentProtocol and define your environments.

Example
enum MyAppEnvironments: EnvironmentProtocol {
    case development
    case qa

    func configure() -> Environment {
        switch self {
        case .development:
            return Environment(scheme: .https,
                               host: "localhost",
                               suffix: .path(["v1"]),
                               port: 3000)
        case .qa:
            return Environment(scheme: .http,
                               host: "myqaserver.com",
                               suffix: .path(["v1"]))
        }
    }
}

Optionally you can pass a configuration object to make all Routers and Requests to inherit the given configuration settings.

To set your global environment use Environment.set method

Environment.set(MyAppEnvironments.development)

Router setup

Create a swift enum that implements the RouteProtocol and define your environments.

The following example creates a TodosRoute with all the required API routes (cases).

Example
enum TodosRoute: RouteProtocol {
    // Define your routes
    case list
    case show(id: Int)
    case add(title: String)
    case remove(id: Int)
    case setCompleted(id: Int, completed: Bool)

    // Set method, path, params, headers for each route
    func configure() -> RouteConfiguration {
        let configuration = Configuration(requestBodyType: .JSON,
                                          headers: ["x-auth": "abcdef1234"])

        switch self {
        case .list:
            return RouteConfiguration(method: .get,
                                      path: .path(["todos"]), // GET /todos
                                      configuration: configuration)
        case .show(let id):
            return RouteConfiguration(method: .get,
                                      path: .path(["todo", String(id)]), // GET /todos/[id]
                                      configuration: configuration)
        case .add(let title):
            return RouteConfiguration(method: .post,
                                      path: .path(["todos"]), // POST /todos
                                      params: ["title": title],
                                      configuration: configuration)
        case .remove(let id):
            return RouteConfiguration(method: .delete,
                                      path: .path(["todo", String(id)]), // DELETE /todo/[id]
                                      configuration: configuration)
        case .setCompleted(let id, let completed):
            return RouteConfiguration(method: .patch,
                                      path: .path(["todo", String(id)]), // PATCH /todo/[id]
                                      params: ["completed": completed],
                                      configuration: configuration)
        }
    }
}

You can optionally pass a configuration object to each case if you want provide different configuration for each route.

Make a request

To create the request you have to initialize a Router instance and specialize it with your defined Router, in our case TodosRoute:

Router<TodosRoute>().request(for: .add(title: "Go shopping!"))
    .success(responseType: Todo.self) { todo in
        // do something with todo
    }
    .failure { error in
        // do something with error
    }

Queue Hooks

Hooks are closures that run before and/or after a request execution in a queue. The following hooks are available:

Queue.shared.beforeAllRequestsCallback = {
    // e.g. show progress loader
}

Queue.shared.afterAllRequestsCallback = { completedWithError in
    // e.g. hide progress loader
}

Queue.shared.beforeEachRequestCallback = { request in
    // do something with request
}

Queue.shared.afterEachRequestCallback = { request, data, urlResponse, error
    // do something with request, data, urlResponse, error
}

For more information take a look at Queue in documentation.

Error Handling

TermiNetwork provides its own error types (TNError) for all the possible error cases. These errors are typically returned in onFailure callbacks of start methods.

To see all the available errors, please visit the TNError in documentation.

Example

Router<TodosRoute>().request(for: .add(title: "Go shopping!"))
      .success(responseType: Todo.self) { todo in
         // do something with todo
      },
      .failure: { error in
          switch error {
          case .notSuccess(let statusCode):
               debugPrint("Status code " + String(statusCode))
               break
          case .networkError(let error):
               debugPrint("Network error: " + error.localizedDescription)
               break
          case .cancelled(let error):
               debugPrint("Request cancelled with error: " + error.localizedDescription)
               break
          default:
               debugPrint("Error: " + error.localizedDescription)
       }

Reachability

With Reachability you can monitor the network state of the device, like whether it is connected through wifi or cellular network.

Example

let reachability = Reachability()
try? reachability.monitorState { state in
    switch state {
    case .wifi:
        // Connected through wifi
    case .cellular:
        // Connected through cellular network
    case .unavailable:
        // No connection
    }
}

Transformers

Transformers enables you to convert your Rest models to Domain models by defining your custom transform functions. To do so, you have to create a class that inherits the Transformer class and specializing it by providing the FromType and ToType generics.

The following example transforms an array of RSCity (rest) to an array of City (domain) by overriding the transform function.

Example

final class CitiesTransformer: Transformer<[RSCity], [City]> {
    override func transform(_ object: [RSCity]) throws -> [City] {
        object.map { rsCity in
            City(id: UUID(),
                 cityID: rsCity.id,
                 name: rsCity.name,
                 description: rsCity.description,
                 countryName: rsCity.countryName,
                 thumb: rsCity.thumb,
                 image: rsCity.image)
        }
    }
}

Finally, pass the CitiesTransformer in the Request's start method:

Example

Router<CityRoute>()
    .request(for: .cities)
    .success(transformer: CitiesTransformer.self) { cities in
        self.cities = cities
    }
    .failure { error in
        switch error {
        case .cancelled:
            break
        default:
            self.errorMessage = error.localizedDescription
        }
    }

Mock responses

Mock responses is a powerful feature of TermiNetwork that enables you to provide a local resource file as Request's response. This is useful, for example, when the API service is not yet available and you need to implement the app's functionality without losing any time. (Prerequisite for this is to have an API contract)

Steps to enable mock responses

  1. Create a Bundle resource and put your files there. (File > New -> File... > Settings Bundle)
  2. Specify the Bundle path in Configuration

    Example

    let configuration = Configuration()
    if let path = Bundle.main.path(forResource: "MockData", ofType: "bundle") {
        configuration.mockDataBundle = Bundle(path: path)
    }
  3. Enable Mock responses in Configuration

    Example

    configuration.mockDataEnabled = true
  4. Define the mockFilePath path in your Routes

    Example

    enum CityRoute: RouteProtocol {
    case cities
    
    func configure() -> RouteConfiguration {
        switch self {
        case .cities:
            return RouteConfiguration(method: .get,
                                        path: .path(["cities"]),
                                        mockFilePath: .path(["Cities", "cities.json"]))
            }
        }
    }
    The example above loads the Cities/cities.json from MockData.bundle and returns its data as Request's response.

For a complete example, open the demo application and take a look at City Explorer - Offline Mode.

Interceptors

Interceptors offers you a way to change or augment the usual processing cycle of a Request. For instance, you can refresh an expired access token (unauthorized status code 401) and then retry the original request. To do so, you just have to implement the InterceptorProtocol.

The following Interceptor implementation tries to refresh the access token with a retry limit (5).

Example

final class UnauthorizedInterceptor: InterceptorProtocol {
    let retryDelay: TimeInterval = 0.1
    let retryLimit = 5

    func requestFinished(responseData data: Data?,
                         error: TNError?,
                         request: Request,
                         proceed: @escaping (InterceptionAction) -> Void) {
        switch error {
        case .notSuccess(let statusCode):
            if statusCode == 401, request.retryCount < retryLimit {
                // Login and get a new token.
                Request(method: .post,
                        url: "https://www.myserviceapi.com/login",
                        params: ["username": "johndoe",
                                 "password": "p@44w0rd"])
                    .success(responseType: LoginResponse.self) { response in
                        let authorizationValue = String(format: "Bearer %@", response.token)

                        // Update the global header in configuration which is inherited by all requests.
                        Environment.current.configuration?.headers["Authorization"] = authorizationValue

                        // Update current request's header.
                        request.headers["Authorization"] = authorizationValue

                        // Finally retry the original request.
                        proceed(.retry(delay: retryDelay))
                    }
            } else {
	 	// Continue if the retry limit is reached
	    	proceed(.continue)
            }
        default:
            proceed(.continue)
        }
    }
}

Finally, you have to pass the UnauthorizedInterceptor to the interceptors property in Configuration:

Example

let configuration = Configuration()
configuration.interceptors = [UnauthorizedInterceptor.self]

SwiftUI/UIKit Image Helpers

TermiNetwork provides two different helpers for setting remote images.

SwiftUI Image Helper

Examples

  1. Example with URL

    var body: some View {
        TermiNetwork.Image(withURL: "https://example.com/path/to/image.png",
    	               defaultImage: UIImage(named: "DefaultThumbImage"))
    }
  2. Example with Request

    var body: some View {
        TermiNetwork.Image(withRequest: Router<CityRoute>().request(for: .image(city: city)),
                           defaultImage: UIImage(named: "DefaultThumbImage"))
    }

UIImageView, NSImageView, WKInterfaceImage Extensions

  1. Example with URL

    let imageView = UIImageView() // or NSImageView (macOS), or WKInterfaceImage (watchOS)
    imageView.tn_setRemoteImage(url: sampleImageURL,
                                defaultImage: UIImage(named: "DefaultThumbImage"),
                                preprocessImage: { image in
        // Optionally pre-process image and return the new image.
        return image
    }, onFinish: { image, error in
        // Optionally handle response
    })
  2. Example with Request

    let imageView = UIImageView() // or NSImageView (macOS), or WKInterfaceImage (watchOS)
    imageView.tn_setRemoteImage(request: Router<CityRoute>().request(for: .thumb(withID: "3125")),
                                defaultImage: UIImage(named: "DefaultThumbImage"),
                                preprocessImage: { image in
        // Optionally pre-process image and return the new image.
        return image
    }, onFinish: { image, error in
        // Optionally handle response
    })

Middleware

Middleware enables you to modify headers, params and response before they reach the success/failure callbacks. You can create your own middleware by implementing the RequestMiddlewareProtocol and passing it to a Configuration object.

Take a look at ./Examples/Communication/Middleware/CryptoMiddleware.swift for an example that adds an additional encryption layer to the application.

Debug Logging

You can enable the debug logging by setting the verbose property to true in your Configuration.

let configuration = Configuration()
configuration.verbose = true

... and you will see a beautiful pretty-printed debug output in debug window

Tests

To run the tests open the Xcode Project > TermiNetwork scheme, select Product -> Test or simply press ⌘U on keyboard.

Contributors

Alex Athanasiadis, [email protected]

License

TermiNetwork is available under the MIT license. See the LICENSE file for more info.

You might also like...
A native, lightweight and secure time-based (TOTP) & counter-based (HOTP) password client built for iOS
A native, lightweight and secure time-based (TOTP) & counter-based (HOTP) password client built for iOS

A native, lightweight and secure time-based (TOTP) & counter-based (HOTP) password client built for iOS Built by Tijme Gommers – Buy me a coffee via P

SSL/TLS Add-in for BlueSocket using Secure Transport and OpenSSL

BlueSSLService SSL/TLS Add-in framework for BlueSocket in Swift using the Swift Package Manager. Works on supported Apple platforms (using Secure Tran

TheraForge's Client REST API framework to connect to TheraForge's secure CloudBox Backend-as-a-Service (BaaS)

OTFCloudClientAPI TheraForge's Client REST API Framework to Connect to TheraForg

OctopusKit is a simplicity but graceful solution for invoke RESTful web service APIs
OctopusKit is a simplicity but graceful solution for invoke RESTful web service APIs

OctopusKit OctopusKit is a simplicity but graceful solution for invoke RESTful web service APIs,it can help coder develop app based MVC pattern, it ca

Lightweight library for web server applications in Swift on macOS and Linux powered by coroutines.
Lightweight library for web server applications in Swift on macOS and Linux powered by coroutines.

Why Zewo? • Support • Community • Contributing Zewo Zewo is a lightweight library for web applications in Swift. What sets Zewo apart? Zewo is not a w

Asynchronous socket networking library for Mac and iOS

CocoaAsyncSocket CocoaAsyncSocket provides easy-to-use and powerful asynchronous socket libraries for macOS, iOS, and tvOS. The classes are described

foursquare iOS networking library

FSNetworking foursquare's iOS networking library FSN is a small library for HTTP networking on iOS. It comprises a single class, FSNConnection, and se

Extensible HTTP Networking for iOS

Bridge Simple Typed JSON HTTP Networking in Swift 4.0 GET GETDict("http://httpbin.org/ip").execute(success: { (response) in let ip: Dict = respo

An elegant yet powerful iOS networking layer inspired by ActiveRecord.
An elegant yet powerful iOS networking layer inspired by ActiveRecord.

Written in Swift 5 AlamoRecord is a powerful yet simple framework that eliminates the often complex networking layer that exists between your networki

Comments
  • Change the way of getting the response

    Change the way of getting the response

    Description

    Change the request execution method calls to a simpler approach.

    Steps

    1. Split the start() method call to two deifferent methods:
      • response(as:[TYPE], completionHandler([TYPE])
      • error(as:[TYPE], completionHandler([TYPE], [ERROR])
    2. Remove the queue parameter from start() methods and add it as separate method queue([QUEUE])
    3. The transformer calls will be overloaded with the new methods as defined in 1
    4. All the new methods are optional
    5. To start the request without any additional method call for handling responses, implement the exec() function

    Example

    Router<CityRoute>()
        .request(for: .cities)
        .queue(myQueue)
        .success(transformer: CitiesTransformer.self,
                 responseHandler: { model in
        })
        .failure(responseType: MyErrorModel.self,
                 responseHandler: { errorModel, error in
        })
    
    opened by billp 1
Releases(3.2.0)
  • 3.2.0(Dec 16, 2022)

    3.2.0 (2022-12-16)

    Full Changelog

    Implemented enhancements:

    • Write test cases covering task cancelation #44
    • Wrap all async functions with withTaskCancellationHandler that cancels the request if needed #43
    • Support task cancellation on async functions #42

    Closed issues:

    • Unescape escaped slashes of response from logger #49
    • Fix duplicated debug print on codable deserialisation error #46
    • No need to handle middleware if there is already an error #40
    • Change access level of Transformer's internal protocol type #37
    Source code(tar.gz)
    Source code(zip)
  • 3.1.1(Dec 4, 2022)

  • 3.1.0(Dec 1, 2022)

  • 3.0.0(Dec 29, 2021)

    Changelog

    v3.0.0 (Support Xcode 13.x)

    Full Changelog

    Implemented enhancements:

    • Implement HPKP (HTTP Public Key Pinning) #20
    • Add network reachability #18
    • Increase test coverage #17

    Fixed bugs:

    • Update heroku ssl certificate #24

    Closed issues:

    • Remove integration with Travis #28
    • Support Xcode 13.x #26
    • Disable Reachability for watchOS #22
    • Remove deprecated functions and classes #19

    Merged pull requests:

    • Increase test coverage (#17) #31 (billp)
    • Remove integration with Travis (#28) #29 (billp)
    • Issue 26 support Xcode 13.x #27 (billp)
    • Update heroku ssl certificate (#24) #25 (billp)
    Source code(tar.gz)
    Source code(zip)
  • 2.1.1(Feb 2, 2021)

  • 2.0.1(Jan 14, 2021)

    2.0.1 (2021-01-14)

    Full Changelog

    Closed issues:

    • Update TestPinning test cases to use the new response callbacks #14
    • Fix testQueueFailureModeCancelAll test case #13
    • Remove warning about operation queue when its finished but never started #12

    Merged pull requests:

    • Remove warning about operation queue when its finished but never started (fixes #12) #16 (billp)
    • Update TestPinning test cases to use the new response callbacks (fixes #14) #15 (billp)
    Source code(tar.gz)
    Source code(zip)
  • 2.0.0(Jan 14, 2021)

  • 1.0.5(Dec 29, 2020)

  • 1.0.3(Dec 28, 2020)

    Added

    • Transformers
    • Mock responses
    • Certificate pinning
    • Flexible configuration
    • Middleware
    • File/Data Upload/Download
    • Interceptors

    Fixed

    • Multi-environment setup (Redefined)
    • Model deserialization with Codables (Redefined)
    • Routers (Redefined)
    • Pretty printed debug information (Redefined)
    Source code(tar.gz)
    Source code(zip)
Owner
Bill Panagiotopoulos
 iOS Developer by day, Ruby developer by night.
Bill Panagiotopoulos
A networking library for iOS, macOS, watchOS and tvOS

Thunder Request Thunder Request is a Framework used to simplify making http and https web requests. Installation Setting up your app to use ThunderBas

3 SIDED CUBE 16 Nov 19, 2022
An open-source Swift framework for building event-driven, zero-config Multipeer Connectivity apps

PeerKit An open-source Swift framework for building event-driven, zero-config Multipeer Connectivity apps Usage // Automatically detect and attach to

JP Simard 861 Dec 23, 2022
Lightweight Networking and Parsing framework made for iOS, Mac, WatchOS and tvOS.

NetworkKit A lightweight iOS, Mac and Watch OS framework that makes networking and parsing super simple. Uses the open-sourced JSONHelper with functio

Alex Telek 30 Nov 19, 2022
QwikHttp is a robust, yet lightweight and simple to use HTTP networking library for iOS, tvOS and watchOS

QwikHttp is a robust, yet lightweight and simple to use HTTP networking library. It allows you to customize every aspect of your http requests within a single line of code, using a Builder style syntax to keep your code super clean.

Logan Sease 2 Mar 20, 2022
A type-safe, high-level networking solution for Swift apps

What Type-safe network calls made easy Netswift offers an easy way to perform network calls in a structured and type-safe way. Why Networking in Swift

Dorian Grolaux 23 Apr 27, 2022
Bonjour networking for discovery and connection between iOS, macOS and tvOS devices.

Merhaba Bonjour networking for discovery and connection between iOS, macOS and tvOS devices. Features Creating Service Start & Stop Service Stop Brows

Abdullah Selek 67 Dec 5, 2022
Modern networking support to monitor network connectivity

ConnectivityKit Adapting to changes in network connectivity can allow for suspending or resuming network activity. When entering an elevator or going

Brennan Stehling 1 Nov 7, 2022
A resource based, protocol oriented networking library designed for pure-SwiftUI applications.

Monarch ?? - WIP A resource based, protocol oriented networking library designed for pure-SwiftUI applications. Features: Async/Await Resource Based P

Emilio Pelaez Romero 4 Oct 17, 2022
ZeroMQ Swift Bindings for iOS, macOS, tvOS and watchOS

SwiftyZeroMQ - ZeroMQ Swift Bindings for iOS, macOS, tvOS and watchOS This library provides easy-to-use iOS, macOS, tvOS and watchOS Swift bindings fo

Ahmad M. Zawawi 60 Sep 15, 2022
SwiftSoup: Pure Swift HTML Parser, with best of DOM, CSS, and jquery (Supports Linux, iOS, Mac, tvOS, watchOS)

SwiftSoup is a pure Swift library, cross-platform (macOS, iOS, tvOS, watchOS and Linux!), for working with real-world HTML. It provides a very conveni

Nabil Chatbi 3.7k Dec 28, 2022