一个零依赖的网络解决方案,用于构建现代且安全的 iOS、watchOS、macOS 和 tvOS 应用程序。
🚀 TermiNetwork 已经在生产环境中进行了测试,经受住了大量的异步请求和每天数万独立客户端的重负载。
您可以使用以下方式之一安装 TermiNetwork...
将以下行添加到您的 Podfile 中,并在您的终端运行 pod install
pod 'TermiNetwork', '~> 4.0'
将以下行添加到您的 Carthage 文件中,并在您的终端运行 carthage update
github "billp/TermiNetwork" ~> 4.0
转到 File > Swift Packages > Add Package Dependency 并添加以下 URL
https://github.com/billp/TermiNetwork
要查看 TermiNetwork 的所有功能,请下载源代码并运行 TermiNetworkExamples 方案。
假设您有以下 Codable 模型
struct Todo: Codable {
let id: Int
let title: String
}
以下示例创建一个添加新 Todo 的请求
let params = ["title": "Go shopping."]
let headers = ["x-auth": "abcdef1234"]
Request(method: .get,
url: "https://myweb.com/api/todos",
headers: headers,
params: params)
.success(responseType: Todo.self) { todos in
print(todos)
}
.failure { error in
print(error.localizedDescription)
}
或者使用 async await
let request = Request(method: .get,
url: "https://myweb.com/api/todos",
headers: headers,
params: params)
do {
let todos: [Todo] = try await request.async()
print(todos)
} catch let error {
print(error.localizedDescription)
}
以下支持的 HTTP 方法之一
.get, .head, .post, .put, .delete, .connect, .options, .trace or .patch
以下支持的响应类型之一
Codable.self (implementations), UIImage.self, Data.self or String.self
一个回调,返回给定类型的对象。(在 responseType 参数中指定)
一个回调,返回一个 Error 和响应 Data (如果有)。
以下示例使用具有 maxConcurrentOperationCount 的自定义 Queue 和一个配置对象。要查看可用配置属性的完整列表,请查看文档中的 Configuration properties。
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)
}
或者使用 async await
do {
let response = try Request(
method: .post,
url: "https://myweb.com/todos",
headers: headers,
params: params,
configuration: configuration
)
.queue(queue)
.async(as: String.self)
} catch let error {
print(error.localizedDescription)
}
上面的请求使用自定义队列 myQueue,其故障模式为 .continue (默认),这意味着如果请求失败,队列将继续执行。
TermiNetwork 的完整且推荐的设置包括定义 Environments 和 Repositories。
创建一个实现 EnvironmentProtocol 的 swift enum 并定义您的环境。
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"]))
}
}
}
您可以选择传递一个 configuration 对象,以使所有 Repositories 和 Endpoints 继承给定的配置设置。
要设置您的全局环境,请使用 Environment.set 方法
Environment.set(MyAppEnvironments.development)
创建一个实现 EndpointProtocol 的 swift enum 并定义您的 endpoints。
以下示例创建一个 TodosRepository,其中所有必需的 endpoints 作为 cases。
enum TodosRepository: EndpointProtocol {
// Define your endpoints
case list
case show(id: Int)
case add(title: String)
case remove(id: Int)
case setCompleted(id: Int, completed: Bool)
static let configuration = Configuration(requestBodyType: .JSON,
headers: ["x-auth": "abcdef1234"])
// Set method, path, params, headers for each endpoint
func configure() -> EndpointConfiguration {
switch self {
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)
}
}
}
您可以选择为每个 case 传递一个 configuration 对象,如果您想为每个 endpoint 提供不同的配置。
要创建请求,您必须初始化一个 Client 实例,并使用您定义的 Repository 对其进行专门化,在本例中为 TodosRepository
Client<TodosRepository>().request(for: .add(title: "Go shopping!"))
.success(responseType: Todo.self) { todo in
// do something with todo
}
.failure { error in
// do something with error
}
或者使用 async await
do {
let toto: Todo = Client<TodosRepository>()
.request(for: .add(title: "Go shopping!"))
.async()
} catch let error {
print(error.localizedDescription)
}
Hooks 是在队列中请求执行之前和/或之后运行的闭包。 以下钩子可用
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
}
有关更多信息,请查看文档中的 Queue。
您可以使用 Request 对象的 .upload、.asyncUpload 方法来启动上传操作。 通过传递 Content-Type: multipart/form-data 请求标头来执行上传。 所有参数值都应作为 MultipartFormDataPartType 传递。
do {
try await Request(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) { _, _, progress in
debugPrint("\(progress * 100)% completed")
}
debugPrint("Upload finished)
} catch let error {
debugPrint(error)
}
您可以使用 Request 对象的 .download、.asyncDownload 方法来启动下载操作。 您唯一需要传递的是要保存的文件的本地文件路径。
guard var localFile = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first.appendPathComponent("download.zip") else {
return
}
do {
try await Request(method: .get,
url: "https://mywebsite.com/files/download.zip")
.asyncDownload(destinationPath: localFile.path,
progressUpdate: { bytesSent, totalBytes, progress in
debugPrint("\(progress * 100)% completed")
})
} catch let error {
debugPrint(error.localizedDescription)
}
debugPrint("File saved to: \(localFile.path)")
TermiNetwork 为所有可能的错误情况提供了自己的错误类型 (TNError)。 这些错误通常在 start 方法的 onFailure 回调中返回。
要查看所有可用的错误,请访问文档中的 TNError。
Client<TodosRepository>().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:
debugPrint("Request cancelled")
break
default:
debugPrint("Error: " + error.localizedDescription)
}
或者使用 async await
do {
let todo: Todo = Client<TodosRepository>()
.request(for: .add(title: "Go shopping!"))
.async()
} catch let error {
switch error as? TNError {
case .notSuccess(let statusCode, let data):
let errorModel = try? data.deserializeJSONData() as MyErrorModel
debugPrint("Status code " + String(statusCode) + ". API Error: " + errorModel?.errorMessage)
break
case .networkError(let error):
debugPrint("Network error: " + error.localizedDescription)
break
case .cancelled:
debugPrint("Request cancelled")
break
default:
debugPrint("Error: " + error.localizedDescription)
}
您可以通过调用 .cancel() 方法来取消正在执行的请求。
let params = ["title": "Go shopping."]
let headers = ["x-auth": "abcdef1234"]
let request = Request(method: .get,
url: "https://myweb.com/api/todos",
headers: headers,
params: params)
request.success(responseType: Todo.self) { todos in
print(todos)
}
.failure { error in
print(error.localizedDescription)
}
request.cancel()
或者使用 async await
let task = Task {
let request = Request(method: .get,
url: "https://myweb.com/api/todos",
headers: headers,
params: params)
do {
let todos: [Todo] = try await request.async()
print(todos)
} catch let error {
print(error.localizedDescription)
}
}
task.cancel()
使用 Reachability,您可以监视设备的网络状态,例如它是否通过 wifi 或蜂窝网络连接。
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
}
}
通过定义您的自定义 transform 函数,Transformers 使您能够将 Rest 模型转换为 Domain 模型。 为此,您必须创建一个继承 Transformer 类的类,并通过提供 FromType 和 ToType 泛型来专门化它。
以下示例通过覆盖 transform 函数将 RSCity (rest) 数组转换为 City (domain) 数组。
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)
}
}
}
最后,在 Request 的 start 方法中传递 CitiesTransformer
Client<CitiesRepository>()
.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
}
}
或者使用 async await
do {
let cities = await Client<CitiesRepository>()
.request(for: .cities)
.async(using: CitiesTransformer.self)
} catch let error {
switch error as? TNError {
case .cancelled:
break
default:
self.errorMessage = error.localizedDescription
}
}
模拟响应 (Mock responses) 是 TermiNetwork 的一项强大功能,使您能够提供本地资源文件作为 Request 的响应。 例如,当 API 服务尚不可用并且您需要实现应用程序的功能而又不浪费任何时间时,这非常有用。(此操作的前提是拥有 API 合同)
let configuration = Configuration()
if let path = Bundle.main.path(forResource: "MockData", ofType: "bundle") {
configuration.mockDataBundle = Bundle(path: path)
}
configuration.mockDataEnabled = true
enum CitiesRepository: EndpointProtocol {
case cities
func configure() -> EndpointConfiguration {
switch self {
case .cities:
return EndpointConfiguration(method: .get,
path: .path(["cities"]),
mockFilePath: .path(["Cities", "cities.json"]))
}
}
}
有关完整示例,请打开演示应用程序并查看 City Explorer - Offline Mode。
拦截器 (Interceptors) 提供了一种更改或扩充 Request 的通常处理周期的方法。 例如,您可以刷新过期的访问令牌(未经授权的状态代码 401),然后重试原始请求。 为此,您只需实现 InterceptorProtocol。
以下 Interceptor 实现尝试刷新访问令牌,并设置重试限制 (5)。
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)
}
}
}
最后,您必须将 UnauthorizedInterceptor 传递给 Configuration 中的 interceptors 属性
let configuration = Configuration()
configuration.interceptors = [UnauthorizedInterceptor.self]
TermiNetwork 提供了两种不同的助手来设置远程图像。
带有 URL 的示例
var body: some View {
TermiNetwork.Image(withURL: "https://example.com/path/to/image.png",
defaultImage: UIImage(named: "DefaultThumbImage"))
}
带有 Request 的示例
var body: some View {
TermiNetwork.Image(withRequest: Client<CitiesRepository>().request(for: .image(city: city)),
defaultImage: UIImage(named: "DefaultThumbImage"))
}
带有 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
})
带有 Request 的示例
let imageView = UIImageView() // or NSImageView (macOS), or WKInterfaceImage (watchOS)
imageView.tn_setRemoteImage(request: Client<CitiesRepository>().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) 使您能够在 header、params 和 response 到达 success/failure 回调之前修改它们。 您可以通过实现 RequestMiddlewareProtocol 并将其传递给 Configuration 对象来创建自己的中间件。
查看 ./Examples/Communication/Middleware/CryptoMiddleware.swift 获取一个示例,该示例为应用程序添加了额外的加密层。
您可以通过在 Configuration 中将 verbose 属性设置为 true 来启用调试日志。
let configuration = Configuration()
configuration.verbose = true
... 您将在调试窗口中看到漂亮的格式化调试输出
要运行测试,请打开 Xcode Project > TermiNetwork scheme,选择 Product -> Test 或只需在键盘上按 ⌘U。
Alex Athanasiadis, alexanderathan@gmail.com
TermiNetwork 在 MIT 许可下可用。 有关更多信息,请参见 LICENSE 文件。