Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up

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

License

NotificationsYou must be signed in to change notification settings

billp/TermiNetwork

Repository files navigation

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

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

Features

  • Multi-environment setup
  • Model deserialization withCodables
  • Async await support
  • Decodes response to the given type:Codable,Transformer,UIImage,Data orString
  • UIKit/SwiftUI extensions for downloading remote images
  • Request grouping with Repositories
  • Detects network status with Reachability
  • Transformers: convert models from one type to another easily
  • Error Handling
  • Interceptors
  • Request mocking
  • Certificate Pinning
  • Flexible configuration
  • Middleware support
  • File/Data Upload/Download support
  • Pretty printed debug information

Table of contents

Installation

You can installTermiNetwork with one of the following ways...

CocoaPods

Add the following line to yourPodfile and runpod install in your terminal:

pod'TermiNetwork','~> 4.0'

Carthage

Add the following line to yourCarthage and runcarthage update in your terminal:

github"billp/TermiNetwork" ~>4.0

Swift Package Manager

Go toFile >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 theTermiNetworkExamples scheme.

Usage

Simple usage (Request)

Let's say you have the following Codable model:

structTodo:Codable{letid:Intlettitle:String}

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

letparams=["title":"Go shopping."]letheaders=["x-auth":"abcdef1234"]Request(method:.get,url:"https://myweb.com/api/todos",headers: headers,params: params).success(responseType:Todo.self){ todosinprint(todos)}.failure{ errorinprint(error.localizedDescription)}

or withasync await:

letrequest=Request(method:.get,                       url:"https://myweb.com/api/todos",                       headers: headers,                       params: params)do{lettodos:[Todo]=tryawait request.async()print(todos)}catchlet error{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 aError and the responseData (if any).

Advanced usage of Request with Configuration and custom Queue

The following example uses a customQueue withmaxConcurrentOperationCount and a configuration object. To see the full list of available configuration properties, take a look atConfiguration properties in documentation.

letmyQueue=Queue(failureMode:.continue)myQueue.maxConcurrentOperationCount=2letconfiguration=Configuration(    cachePolicy:.useProtocolCachePolicy,    timeoutInterval:30,    requestBodyType:.JSON)letparams=["title":"Go shopping."]letheaders=["x-auth":"abcdef1234"]Request(method:.post,        url:"https://myweb.com/todos",        headers: headers,        params: params,        configuration: configuration).queue(queue).success(responseType:String.self){ responseinprint(response)}.failure{ errorinprint(error.localizedDescription)}

or withasync await:

do{letresponse=tryRequest(    method:.post,url:"https://myweb.com/todos",headers: headers,params: params,configuration: configuration).queue(queue).async(as:String.self)}catchlet error{print(error.localizedDescription)}

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

Complete setup withEnvironments andRepositories

The complete and recommended setup of TermiNetwork consists of definingEnvironments andRepositories.

Environment setup

Create a swiftenum that implements theEnvironmentProtocol and define your environments.

Example
enumMyAppEnvironments:EnvironmentProtocol{case developmentcase qafunc configure()->Environment{switchself{case.development:returnEnvironment(scheme:.https,                               host:"localhost",                               suffix:.path(["v1"]),                               port:3000)case.qa:returnEnvironment(scheme:.http,                               host:"myqaserver.com",                               suffix:.path(["v1"]))}}}

Optionally you can pass aconfiguration object to make all Repositories and Endpoints to inherit the given configuration settings.

To set your global environment use Environment.set method

Environment.set(MyAppEnvironments.development)

Repository setup

Create a swiftenum that implements theEndpointProtocol and define your endpoints.

The following example creates a TodosRepository with all the required endpoints as cases.

Example
enumTodosRepository:EndpointProtocol{    // Define your endpointscase listcase show(id:Int)case add(title:String)case remove(id:Int)case setCompleted(id:Int, completed:Bool)staticletconfiguration=Configuration(requestBodyType:.JSON,                                             headers:["x-auth":"abcdef1234"])    // Set method, path, params, headers for each endpointfunc configure()->EndpointConfiguration{switchself{case.list:return.init(method:.get,                         path:.path(["todos"]), // GET /todos                         configuration:Self.configuration)case.show(let id):return.init(method:.get,                         path:.path(["todo",String(id)]), // GET /todos/[id]                         configuration:Self.configuration)case.add(let title):return.init(method:.post,                         path:.path(["todos"]), // POST /todos                         params:["title": title],                         configuration:Self.configuration)case.remove(let id):return.init(method:.delete,                         path:.path(["todo",String(id)]), // DELETE /todo/[id]                         configuration: configuration)case.setCompleted(let id,let completed):return.init(method:.patch,                         path:.path(["todo",String(id)]), // PATCH /todo/[id]                         params:["completed": completed],                         configuration: configuration)}}}

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

Make a request

To create the request you have to initialize aClient instance and specialize it with your defined Repository, in our caseTodosRepository:

Client<TodosRepository>().request(for:.add(title:"Go shopping!")).success(responseType:Todo.self){ todoin        // do something with todo}.failure{ errorin        // do something with error}

or withasync await

do{lettoto:Todo=Client<TodosRepository>().request(for:.add(title:"Go shopping!")).async()}catchlet error{print(error.localizedDescription)}

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={ completedWithErrorin    // e.g. hide progress loader}Queue.shared.beforeEachRequestCallback={ requestin    // do something with request}Queue.shared.afterEachRequestCallback={ request, data, urlResponse, error    // do something with request, data, urlResponse, error}

For more information take a look atQueue in documentation.

File/Data Upload

You can use the.upload,.asyncUpload methods of a Request object to start an upload operation. The upload is perfomed by passing a Content-Type: multipart/form-data request header. All the param values should be passed as MultipartFormDataPartType.

Example

do{tryawaitRequest(method:.post,    url:"https://mywebsite.com/upload",    params:["file1":MultipartFormDataPartType.url(.init(filePath:"/path/to/file.zip")),"file2":MultipartFormDataPartType.data(data:Data(), filename:"test.png", contentType:"zip"),"expiration_date":MultipartFormDataPartType.value(value:Date.now.ISO8601Format())]).asyncUpload(as:ResponseModel.self){ _, _, progressindebugPrint("\(progress*100)% completed")}    debugPrint("Upload finished)} catch let error {    debugPrint(error)}

File Download

You can use the.download,.asyncDownload methods of a Request object to start a download operation. The only thing you need to pass is the local file path of the file to be saved.

Example

guardvar localFile=FileManager.default.urls(for:.cachesDirectory, in:.userDomainMask).first.appendPathComponent("download.zip")else{return}do{tryawaitRequest(method:.get,       url:"https://mywebsite.com/files/download.zip").asyncDownload(destinationPath: localFile.path,       progressUpdate:{ bytesSent, totalBytes, progressindebugPrint("\(progress*100)% completed")})}catchlet error{debugPrint(error.localizedDescription)}debugPrint("File saved to:\(localFile.path)")

Error Handling

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

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

Example

Client<TodosRepository>().request(for:.add(title:"Go shopping!")).success(responseType:Todo.self){ todoin         // do something with todo}.failure:{ error inswitch error{case.notSuccess(let statusCode):debugPrint("Status code"+ String(statusCode))breakcase.networkError(let error):debugPrint("Network error:"+ error.localizedDescription)breakcase.cancelled:debugPrint("Request cancelled")breakdefault:debugPrint("Error:"+ error.localizedDescription)}

or withasync await

do{lettodo:Todo=Client<TodosRepository>().request(for:.add(title:"Go shopping!")).async()}catchlet error{switch erroras?TNError{case.notSuccess(let statusCode,let data):leterrorModel=try? data.deserializeJSONData()asMyErrorModeldebugPrint("Status code"+ String(statusCode)+". API Error:"+ errorModel?.errorMessage)breakcase.networkError(let error):debugPrint("Network error:"+ error.localizedDescription)breakcase.cancelled:debugPrint("Request cancelled")breakdefault:debugPrint("Error:"+ error.localizedDescription)}

Cancelling a request

You can cancel a request that is executing by calling the .cancel() method.

Example

letparams=["title":"Go shopping."]letheaders=["x-auth":"abcdef1234"]letrequest=Request(method:.get,       url:"https://myweb.com/api/todos",       headers: headers,       params: params)request.success(responseType:Todo.self){ todosinprint(todos)}.failure{ errorinprint(error.localizedDescription)}request.cancel()

or withasync await:

lettask=Task{letrequest=Request(method:.get, url:"https://myweb.com/api/todos", headers: headers, params: params)do{lettodos:[Todo]=tryawait request.async()print(todos)}catchlet error{print(error.localizedDescription)}}task.cancel()

Reachability

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

Example

letreachability=Reachability()try? reachability.monitorState{ stateinswitch state{case.wifi:        // Connected through wifi    case.cellular:        // Connected through cellular networkcase.unavailable:        // No connection}}

Transformers

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

The following example transforms an array ofRSCity (rest) to an array ofCity (domain) by overriding the transform function.

Example

finalclassCitiesTransformer:Transformer<[RSCity],[City]>{overridefunc transform(_ object:[RSCity])throws->[City]{        object.map{ rsCityinCity(id:UUID(),                 cityID: rsCity.id,                 name: rsCity.name,                 description: rsCity.description,                 countryName: rsCity.countryName,                 thumb: rsCity.thumb,                 image: rsCity.image)}}}

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

Example

Client<CitiesRepository>().request(for:.cities).success(transformer:CitiesTransformer.self){ citiesinself.cities= cities}.failure{ errorinswitch error{case.cancelled:breakdefault:self.errorMessage= error.localizedDescription}}

or withasync await

do{letcities=awaitClient<CitiesRepository>().request(for:.cities).async(using:CitiesTransformer.self)}catchlet error{switch erroras?TNError{case.cancelled:breakdefault: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

    letconfiguration=Configuration()iflet 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 endpoints.

    Example

    enumCitiesRepository:EndpointProtocol{case citiesfunc configure()->EndpointConfiguration{switchself{case.cities:returnEndpointConfiguration(method:.get,                                     path:.path(["cities"]),                                     mockFilePath:.path(["Cities","cities.json"]))}}}
    The example above loads theCities/cities.json fromMockData.bundle and returns its data as Request's response.

For a complete example, open the demo application and take a look atCity 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 theInterceptorProtocol.

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

Example

finalclassUnauthorizedInterceptor:InterceptorProtocol{letretryDelay:TimeInterval=0.1letretryLimit=5func 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){ responseinletauthorizationValue=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 reachedproceed(.continue)}default:proceed(.continue)}}}

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

Example

letconfiguration=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

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

    varbody:someView{TermiNetwork.Image(withRequest:Client<CitiesRepository>().request(for:.image(city: city)),                       defaultImage:UIImage(named:"DefaultThumbImage"))}

UIImageView, NSImageView, WKInterfaceImage Extensions

  1. Example with URL

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

    letimageView=UIImageView() // or NSImageView (macOS), or WKInterfaceImage (watchOS)imageView.tn_setRemoteImage(request:Client<CitiesRepository>().request(for:.thumb(withID:"3125")),                            defaultImage:UIImage(named:"DefaultThumbImage"),                            preprocessImage:{ imagein    // Optionally pre-process image and return the new image.return image}, onFinish:{ image, errorin    // 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 theRequestMiddlewareProtocol and passing it to aConfiguration 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 theverbose property totrue in yourConfiguration.

letconfiguration=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,alexanderathan@gmail.com

License

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


[8]ページ先頭

©2009-2025 Movatter.jp