网络连接 5.1.0

网络连接 5.1.0

测试已测试
语言语言 SwiftSwift
许可 NOASSERTION
发布上次发布2020年10月
SPM支持SPM

Elvis Nuñez维护。



  • 作者:
  • Elvis Nuñez

Networking

网络连接是基于需要有一个简单易用的网络库而诞生的,它不包含复杂的编程抽象,也不使用最新的响应式编程技术,而只是对NSURLSession的简单、直观和便捷的包装,支持常见的功能,例如请求模拟和内置图片缓存。这是一个足够小以至于可以一次阅读,但也能够在任何项目中使用的库。这就是网络连接的出现,一个对iOS、tvOS、watchOS和macOS进行全面测试的库,将始终为您服务。

  • 超级友好的API
  • 无需单例模式
  • 无外部依赖
  • 针对单元测试优化
  • 最小实现
  • 简单请求取消
  • 轻松模拟请求(模拟/存根)
  • 在自动测试环境中同步运行(较少的XCTestExpectations)
  • 图片下载和缓存
  • 免费

目录

选择配置

实例化 网络连接意味着您必须选择一个NSURLSessionConfiguration。可用的类型有DefaultEphemeralBackground,如果没有特殊需求,则默认使用Default

  • 默认:默认会话配置使用基于持久磁盘的缓存(除非结果下载到文件中),并将凭证存储在用户的钥匙链中。它还会在相同共享的cookie存储中存储cookies(默认设置),与NSURLConnectionNSURLDownload类相同。

  • 临时:临时会话配置对象类似于默认会话配置对象,除了对应的会话对象不会将缓存、凭证存储或任何与会话相关的数据存储到磁盘上。相反,与会话相关的数据存储在RAM中。只有当你告诉它将一个URL的内容写入文件时,临时会话才会将数据写入磁盘。使用临时会话的主要优点是隐私。通过不将可能敏感的数据写入磁盘,你可以降低数据被拦截并以后使用的机会。因此,临时会话非常适合网络浏览器的私人浏览模式和其他类似情况。

  • 后台:此配置类型适用于在应用在后台运行时传输数据文件。使用此对象配置的会话将传输控制权交给系统,系统在单独的进程中处理传输。在iOS中,此配置使得即使应用本身被挂起或终止,也可以继续传输。

// Default
let networking = Networking(baseURL: "http://httpbin.org")

// Ephemeral
let networking = Networking(baseURL: "http://httpbin.org", configuration: .ephemeral)

更改请求头

您可以在任何网络对象中设置headerFields

这将Appending(如果未找到)或覆盖(如果找到)每条请求URLSession发送的内容。

networking.headerFields = ["User-Agent": "your new user agent"]

认证

HTTP基本认证

要使用用户名为"aladdin"和密码为"opensesame"进行基本认证,您只需做下面这样:

let networking = Networking(baseURL: "http://httpbin.org")
networking.setAuthorizationHeader(username: "aladdin", password: "opensesame")
networking.get("/basic-auth/aladdin/opensesame") { result in
    // Successfully authenticated!
}

带有令牌认证

要使用令牌"AAAFFAAAA3DAAAAAA"进行认证,您只需做下面这样:

let networking = Networking(baseURL: "http://httpbin.org")
networking.setAuthorizationHeader(token: "AAAFFAAAA3DAAAAAA")
networking.get("/get") { result in
    // Successfully authenticated!
}

自定义认证头

要使用自定义认证头来认证,例如例如 "Token token=AAAFFAAAA3DAAAAAA",您需要设置以下头字段:Authorization: Token token=AAAFFAAAA3DAAAAAA。幸运的是,网络 提供了简单的方法来完成此操作

let networking = Networking(baseURL: "http://httpbin.org")
networking.setAuthorizationHeader(headerValue: "Token token=AAAFFAAAA3DAAAAAA")
networking.get("/get") { result in
    // Successfully authenticated!
}

提供以下认证头Anonymous-Token: AAAFFAAAA3DAAAAAA 也是可能的

let networking = Networking(baseURL: "http://httpbin.org")
networking.setAuthorizationHeader(headerKey: "Anonymous-Token", headerValue: "AAAFFAAAA3DAAAAAA")
networking.get("/get") { result in
    // Successfully authenticated!
}

发起请求

基础知识

发起请求就像调用 getpostputdelete 那样简单。

GET 举例:

let networking = Networking(baseURL: "http://httpbin.org")
networking.get("/get") { result in
    switch result {
    case .success(let response):
        let json = response.dictionaryBody
        // Do something with JSON, you can also get arrayBody
    case .failure(let response):
        // Handle error
    }
}

POST 举例:

let networking = Networking(baseURL: "http://httpbin.org")
networking.post("/post", parameters: ["username" : "jameson", "password" : "secret"]) { result in
    /*
    {
        "json" : {
            "username" : "jameson",
            "password" : "secret"
        },
        "url" : "http://httpbin.org/post",
        "data" : "{"password" : "secret","username" : "jameson"}",
        "headers" : {
            "Accept" : "application/json",
            "Content-Type" : "application/json",
            "Host" : "httpbin.org",
            "Content-Length" : "44",
            "Accept-Language" : "en-us"
        }
    }
    */
}

您可以在成功的回调内部获取响应头。

let networking = Networking(baseURL: "http://httpbin.org")
networking.get("/get") { result in
    switch result {
    case .success(let response):
        let headers = response.allHeaderFields
        // Do something with headers
    case .failure(let response):
        // Handle error
    }
}

默认情况下,所有请求都是异步的,您可以使用 isSynchronous 使一个 Networking 实例的所有请求都同步执行。

let networking = Networking(baseURL: "http://httpbin.org")
networking.isSynchronous = true

结果类型

如果您不熟悉 Result 类型,它就是大多数网络库在当今用来处理网络操作时遇到的众多可选性和解包问题。在 Result 类型之前,我们遇到这样的问题

// The old way
let networking = Networking(baseURL: "http://httpbin.org")
networking.get("/get") { json, headers, error in // Both are optional
    if let error = error {
        // OK, now we can handle the error
    } else if let jsonArray = json as? [[String: Any]] {
        // A valid JSON! Yay!
    } else {
      // Oh god, this shouldn't be happening, what do we do?!
    }
}

现在,我们不再需要这样做,利用 Result 类型可以解决这个问题,Result 类型是一个枚举,有两个情况:`success` 和 `failure`。`success` 情况有一个响应,`failure` 情况有一个错误和响应,这些都不是可选的,不再需要解包!

下面是如何使用它的示例

// The best way
let networking = Networking(baseURL: "http://fakerecipes.com")
networking.get("/recipes") { result in
    switch result {
    case .success(let response):
        // We know we'll be receiving an array with the best recipes, so we can just do:
        let recipes = response.arrayBody // BOOM, no optionals. [[String: Any]]

        // If we need headers or response status code we can use the HTTPURLResponse for this.
        let headers = response.headers // [String: Any]
    case .failure(let response):
        // Non-optional error ✨
        let errorCode = response.error.code

        // Our backend developer told us that they will send a json with some
        // additional information on why the request failed, this will be a dictionary.
        let json = response.dictionaryBody // BOOM, no optionals here [String: Any]

        // We want to know the headers of the failed response.
        let headers = response.headers // [String: Any]
    }
}

这就是我们在 Networking 中处理没有可选性的方式。

let networking = Networking(baseURL: "http://httpbin.org")
networking.post("/post", parameters: ["name" : "jameson"]) { result in
   // Successfull post using `application/json` as `Content-Type`
}

let networking = Networking(baseURL: "http://httpbin.org")
networking.post("/post", parameterType: .formURLEncoded, parameters: ["name" : "jameson"]) { result in
   // Successfull post using `application/x-www-form-urlencoded` as `Content-Type`
}

let networking = Networking(baseURL: "https://example.com")
let imageData = UIImagePNGRepresentation(imageToUpload)!
let part = FormDataPart(data: imageData, parameterName: "file", filename: "selfie.png")
networking.post("/image/upload", part: part) { result in
  // Successfull upload using `multipart/form-data` as `Content-Type`
}

let networking = Networking(baseURL: "https://example.com")
let part1 = FormDataPart(data: imageData1, parameterName: "file1", filename: "selfie1.png")
let part2 = FormDataPart(data: imageData2, parameterName: "file2", filename: "selfie2.png")
let parameters = ["username" : "3lvis"]
networking.post("/image/upload", parts: [part1, part2], parameters: parameters) { result in
    // Do something
}

FormDataPart 内容类型:

其他

目前,网络默认支持四种类型的ParameterTypeJSONFormURLEncodedMultipartFormDataCustom。同时,JSONFormURLEncoded会以某种方式序列化你的参数,Custom(String)以纯NSData的形式发送参数,并将Custom内部的值设置为Content-Type

例如

let networking = Networking(baseURL: "http://httpbin.org")
networking.post("/upload", parameterType: .Custom("application/octet-stream"), parameters: imageData) { result in
   // Successfull upload using `application/octet-stream` as `Content-Type`
}

取消请求

使用路径

取消对特定路径的任何请求非常简单。请注意,取消请求将导致请求返回一个错误,状态码为URLError.cancelled。

let networking = Networking(baseURL: "http://httpbin.org")
networking.get("/get") { result in
    // Cancelling a GET request returns an error with code URLError.cancelled which means cancelled request
}

networking.cancelGET("/get")

使用请求标识符

使用cancelPOST("/upload")将取消针对特定路径的所有POST请求,但在某些情况下,这并不是我们想要的。例如,如果您正在尝试上传两张照片,但用户请求取消其中一张上传,使用`cancelPOST("/upload")`将取消所有上传,这时基于ID的取消非常有用。

let networking = Networking(baseURL: "http://httpbin.org")

// Start first upload
let firstRequestID = networking.post("/upload", parts: ...) { result in
    //...
}

// Start second upload
let secondRequestID = networking.post("/upload", parts: ...) { result in
    //...
}

// Cancel only the first upload
networking.cancel(firstRequestID)

模拟请求

模拟请求意味着在特定路径上调用此方法后,对该资源的任何调用都将返回您注册的响应。这种技术也称为模拟或占位。

使用成功响应进行模拟:

let networking = Networking(baseURL: "https://api-news.layervault.com/api/v2")
networking.fakeGET("/stories", response: [["id" : 47333, "title" : "Site Design: Aquest"]])
networking.get("/stories") { result in
    // JSON containing stories
}

使用文件内容进行模拟:

如果您的文件不在主捆绑包中,您必须指定使用捆绑包参数;否则,将使用

let networking = Networking(baseURL: baseURL)
networking.fakeGET("/entries", fileName: "entries.json")
networking.get("/entries") { result in
    // JSON with the contents of entries.json
}

使用状态码进行模拟:

如果您没有为这个虚假请求提供状态码,则默认返回的状态码将是200(成功),但如果您提供了一个非2XX的状态码,那么网络将返回一个包含状态码和正确错误描述的NS错误对象。

let networking = Networking(baseURL: "https://api-news.layervault.com/api/v2")
networking.fakeGET("/stories", response: nil, statusCode: 500)
networking.get("/stories") { result in
    // error with status code 500
}

下载和缓存图像

下载:

let networking = Networking(baseURL: "http://httpbin.org")
networking.downloadImage("/image/png") { result in
   // Do something with the downloaded image
}

取消:

let networking = Networking(baseURL: baseURL)
networking.downloadImage("/image/png") { result in
    // Cancelling an image download returns an error with code URLError.cancelled which means cancelled request
}

networking.cancelImageDownload("/image/png")

缓存:

Networking 在下载图像时使用多缓存架构,当第一次调用特定路径的 downloadImage 方法时,它将结果保存在磁盘(文档文件夹)和内存(NSCache)中,因此在下一次调用时将返回缓存结果而无需连接到网络。

let networking = Networking(baseURL: "http://httpbin.org")
networking.downloadImage("/image/png") { result in
   // Image from network
   networking.downloadImage("/image/png") { result in
       // Image from cache
   }
}

如果您想删除已下载的图像,可以这样做:

let networking = Networking(baseURL: "http://httpbin.org")
let destinationURL = try networking.destinationURL(for: "/image/png")
if let path = destinationURL.path where NSFileManager.defaultManager().fileExistsAtPath(path) {
   try! NSFileManager.defaultManager().removeItemAtPath(path)
}

伪造:

let networking = Networking(baseURL: baseURL)
let pigImage = UIImage(named: "pig.png")!
networking.fakeImageDownload("/image/png", image: pigImage)
networking.downloadImage("/image/png") { result in
   // Here you'll get the provided pig.png image
}

记录错误

Networking 捕获的任何错误都会打印到您的控制台。这非常方便,因为无论如何您都想知道为什么您的网络调用失败。

例如,取消的请求将打印如下:

========== Networking Error ==========

Cancelled request: https://api.mmm.com/38bea9c8b75bfed1326f90c48675fce87dd04ae6/thumb/small

================= ~ ==================

404请求将打印如下:

========== Networking Error ==========

*** Request ***

Error 404: Error Domain=NetworkingErrorDomain Code=404 "not found" UserInfo={NSLocalizedDescription=not found}

URL: http://httpbin.org/posdddddt

Headers: ["Accept": "application/json", "Content-Type": "application/json"]

Parameters: {
  "password" : "secret",
  "username" : "jameson"
}

Data: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested URL was not found on the server.  If you entered the URL manually please check your spelling and try again.</p>


*** Response ***

Headers: ["Content-Length": 233, "Server": nginx, "Access-Control-Allow-Origin": *, "Content-Type": text/html, "Date": Sun, 29 May 2016 07:19:13 GMT, "Access-Control-Allow-Credentials": true, "Connection": keep-alive]

Status code: 404 — not found

================= ~ ==================

要禁用错误日志记录,请使用标志 disableErrorLogging

let networking = Networking(baseURL: "http://httpbin.org")
networking.disableErrorLogging = true

更新网络活动指示器

Networking 平衡网络活动指示器的显示。

网络活动指示器出现在状态栏中,显示正在进行网络活动。网络活动指示器

  • 在网络活动进行时在状态栏中旋转,在网络活动停止时消失
  • 不允许用户交互

当您的应用在超过几秒钟的时间内访问网络时,显示网络活动指示器以提供反馈。如果操作完成得更快,您不必显示网络活动指示器,因为指示器可能在用户注意到其存在之前就消失了。

iOS 人机界面指南

安装

网络功能通过 CocoaPods 提供。要安装它,只需将以下行添加到您的 Podfile 中

pod 'Networking', '~> 4'

网络功能也通过 Carthage 提供。要安装它,只需将以下行添加到您的 Cartfile 中

github "3lvis/Networking" ~> 4.4

作者

这个库是由 @3lvis 用爱制作的。

许可

Networking 在 MIT 许可证下提供。有关更多信息,请参阅 LICENSE 文件

归属

标志字体由 Sanid Jusić 提供。

中文描述

简单易用、功能强大,基于 NSURLSession 的网络封装库。功能包括带身份验证请求,支持单元测试(mocking/stubbing),异步执行,图片下载及缓存等实用特性。