SwiftNet
在支持Swift并发之后,CombineNetworking变成了SwiftNet!这是一个超级轻量级并且极其易于使用的框架,可以帮助您以一种方便的方式创建和处理网络请求。除了基本的网络请求之外,SwiftNet还允许您通过简单的SSL和证书固定机制轻松安全地发送请求。但这还不是全部。使用SwiftNet,您可以毫不费力地处理内置的自动授权机制。
安装(使用CocoaPods)
pod 'SwiftNet-macroless'
注意,为了使用SwiftNet,您的iOS部署目标必须是13.0或更高。如果您的代码是针对macOS,则部署目标必须是10.15或更高。
SwiftNet的CocoaPods版本不包含SwiftNetMacros,因此您无法使用它们。
关键功能
- 使用
Endpoint
模型轻松发送请求 - 使用2行代码实现SSL和证书固定
- 支持使用
SNWebSocket
进行WebSocket连接 - 使用密钥链安全存储访问令牌
- 访问令牌存储策略 - 配置
全局
、端点特定
(默认
)或自定义
策略,以适应所有或仅一些端点 - 自动刷新令牌/回调请求
基本用法
创建一个端点进行操作
enum TodosEndpoint {
case todos(Int)
}
extension TodosEndpoint: Endpoint {
var baseURL: URL? {
URL(string: "https://jsonplaceholder.typicode.com/")
}
var path: String {
switch self {
case .todos:
return "todos"
}
}
var method: RequestMethod {
.get
}
var headers: [String : Any]? {
nil
}
var data: EndpointData {
switch self {
case .todos(let id):
return .queryParams(["id": id])
}
}
}
RequestMethod
是一个枚举,具有以下选项:.get
、.post
、.put
、.delete
、patch
。 EndpointData
也是一个枚举,具有以下选项
`.plain`
`.queryParams([String: Any])`
`.queryString(String)`
`.bodyData(Data)
- 将Dictionary
解析为Data
,并用于请求数据- `.bodyParams([String: Any])` - 接受
Dictionary
并将其解析为数据类型,以发送到请求体 - `.urlEncodedBody([String: Any])` - 接受
Dictionary
并将其解析为url编码的数据类型,以发送到请求体 - `.urlEncodedModel(Encodable)` - 接受
Encodable
模型并将其解析为url编码的数据类型,以发送到请求体 - `.jsonModel(Encodable)` - 与
.dataParams
类似,除了它接受Encodable
并将其解析为数据类型,以发送到请求体
启用SSL和/或证书固定(可选)
为了让您的应用程序支持SSL和/或证书固定,只需添加以下内容
SNConfig.pinningModes = [.ssl, .certificate]
请记住,SSL/证书固定需要将证书文件附加到您的项目中。证书和SSL密钥将由SwiftNet自动加载。
自动授权机制
使用SwiftNet处理授权回调和请求非常简单。要使用它与您的Endpoint
,只需添加如下所示的requiresAccessToken
和callbackPublisher
字段
enum TodosEndpoint {
case token
case todos(Int)
}
extension TodosEndpoint: Endpoint {
//Setup all the required properties like baseURL, path, etc...
//... then determine which of your endpoints require authorization...
var requiresAccessToken: Bool {
switch self {
case .token:
return false
default:
return true
}
}
//... and prepare callbackPublisher to handle authorization callbacks
var callbackPublisher: AnyPublisher<AccessTokenConvertible?, Error>? {
try? SNProvider<TodosEndpoint>().publisher(for: .token, responseType: SNAccessToken?.self).asAccessTokenConvertible()
}
}
看!非常简单!请注意,您的令牌模型必须符合AccessTokenConvertible
。
SNConfig属性和方法
pinningModes
- 用于开启/关闭SSL和证书固定。可用选项有.ssl
、.certificate
或两者。sitesExcludedFromPinning
- 从SSL/证书固定检查中排除的网站地址列表defaultJSONDecoder
- 使用此属性全局设置自定义的JSONDecoderdefaultAccessTokenStrategy
- 存储访问令牌的全局策略。可用选项包括.global
和.custom(String)
。keychainInstance
- SwiftNet 用于从 Apple Keychain 存储/获取访问令牌的 Keychain 实例。如果不提供,将关闭安全存储(更多信息见下文)accessTokenStorage
- 实现 AccessTokenStorage 协议的对象的实例。它用于操作访问令牌。默认情况下,它使用内置的SNStorage
。要使用不同的存储,请提供自己的实例。accessTokenErrorCodes
- 包含应触发访问令牌刷新操作的错误代码的数组。默认:[401]。
访问令牌策略
SwiftNet allows you to specify access token strategies globally as well as individually for each endpoint. You can specify your strategy by setting it for `SNConfig.defaultAccessTokenStrategy` or inside your `Endpoint` by setting value for field `accessTokenStrategy`.
可用选项包括
.global
- 使用全局标签存储访问令牌.custom(String)
- 使用此选项可以指定自己的标签以存储访问令牌,并在多个端点上使用它
由于访问令牌策略在全局(通过 SNConfig
)和单个(在 Endpoint
内)层面都进行了设置,因此您可以在应用中混合使用不同的策略!
访问令牌操作
如果需要,您可以自己操作访问令牌。
可用方法包括
-
setAccessToken(_ token:, for:)
-
accessToken(for:)
-
removeAccessToken(for:)
-
setGlobalAccessToken(_ token:)
-
globalAccessToken()
-
removeGlobalAccessToken()
事件日志记录
SwiftNet 的 SNProvider 使用 iOS 的内置 Logger(如果在 iOS 14 或更高版本上运行)和默认情况下为每个请求使用的自定义调试模式仅的 Logger。
网络连接监控器
CombineNetworking 允许您持续监控网络连接状态。如果想要订阅网络连接监控器的发布者,可以像这样操作
private var subscriptions: Set<AnyCancellable> = []
func subscribeForNetworkChanges() {
SNNetworkMonitor.publisher()
.sink { status in
switch status {
case .wifi:
// Do something
case .cellular:
// Do something else
case .unavailable:
// Show connection error
}
}
.store(in: &subscriptions)
}
使用 Keychain 进行安全存储
SwiftNet 允许您将访问令牌存储在 Keychain 中。使用 Keychain 存储访问令牌需要您通过设置 SNConfig.keychainInstance
的值来提供 Keychain 实例。
请记住,Apple 的 Keychain 在应用被删除时不会自动删除由应用创建的条目。但是不要担心,只有您的应用可以访问这些条目。但是,确保这些条目在不再需要时从 Keychain 中移除是您的工作。SwiftNet 提供了 SNConfig.removeAccessToken(...)
方法来帮助您完成这项工作。
订阅发布者
private var subscriptions: Set<AnyCancellable> = []
var todo: Todo?
func subscribeForTodos() {
SNProvider<TodosEndpoint>().publisher(for: .todos(1), responseType: Todo?.self)
.catch { (error) -> Just<Todo?> in
print(error)
return Just(nil)
}
.assign(to: \.todo, on: self)
.store(in: &subscriptions)
}
如果您想订阅发布者但又不想立即解码体而想获取原始 Data 对象,请使用 rawPublisher
。
错误处理
如果请求失败,SwiftNet 返回类型为 SNError
的结构体,以 Error
的方式反射。
public struct SNError: Error {
let type: ErrorType
let details: SNErrorDetails?
let data: Data?
}
可用的错误类型包括:failedToBuildRequest
、failedToMapResponse
、unexpectedResponse
、authenticationFailed
、notConnected
、emptyResponse
、noInternetConnection
和 conversionFailed
。
SNErrorDetails
的样子如下
public struct SNErrorDetails {
public let statusCode: Int
public let localizedString: String
public let url: URL?
public let mimeType: String?
public let headers: [AnyHashable: Any]?
public let data: Data?
}
简化测试
如果您想对请求进行简单测试,仅为了确认响应的状态码满足为给定端点设置的期望,您只需像这样运行 testRaw()
方法
final class SwiftNetTests: XCTestCase {
private let provider = SNProvider<RemoteEndpoint>()
func testTodoFetch() throws {
let expectation = expectation(description: "Test todo fetching request")
var subscriptions: Set<AnyCancellable> = []
provider.testRaw(.todos, usingMocks: false, storeIn: &subscriptions) {
expectation.fulfill()
}
wait(for: [expectation], timeout: 10)
}
}
... 如果您要通过对状态码和响应模型进行测试来测试您的请求,可以使用 test()
方法像这样操作
final class SwiftNetTests: XCTestCase {
private let provider = SNProvider<RemoteEndpoint>()
func testTodoFetchWithModel() throws {
let expectation = expectation(description: "Test todo fetching request together with its response model")
var subscriptions: Set<AnyCancellable> = []
provider.test(.todos, responseType: Todo.self, usingMocks: false, storeIn: &subscriptions) {
expectation.fulfill()
}
wait(for: [expectation], timeout: 10)
}
}
您还可以在测试中使用模拟数据。为此,只需将 mockedData
添加到您的 Endpoint
中,并在调用 provider.test()
或 provider.testRaw()
时将 usingMocks
设置为 true
。
WebSockets
SwiftNet 还允许您轻松地连接到 WebSocket。只需像这样使用 SNWebSocket
let webSocket = SNWebSocket(url: URL(string: "wss://socketsbay.com/wss/v2/2/demo/")!)
webSocket.connect()
webSocket.listen { result in
switch result {
case .success(let message):
switch message {
case .data(let data):
print("Received binary: \(data)")
case .string(let string):
print("Received string: \(string)")
}
default:
return
}
}
webSocket.send(.string("Test message")) {
if let error = $0 {
log(error.localizedDescription)
}
}
如果想要关闭连接,只需调用 webSocket.disconnect()
。
宏驱动联网
从 2.0.0 版本开始,SwiftNet 引入了构建和执行网络请求的新方法。要启用 SwiftNet 的宏,请将下面内容添加到您的文件中
import SwiftNetMacros
创建端点
首先创建一个结构体或类来实现 EndpointModel
协议。
public protocol EndpointModel {
var defaultAccessTokenStrategy: AccessTokenStrategy { get }
var defaultHeaders: [String: Any] { get }
var callbackPublisher: AnyPublisher<AccessTokenConvertible, Error>? { get }
}
完成之后,您就可以创建端点。每个端点请求的类型应为 EndpointBuilder<T: Codable & Equatable>
。
- 使用
@Endpoint(url:)
宏来设置端点的 base URL - 使用
@GET(url:descriptor:)
、@POST(url:descriptor:)
、@PUT(url:descriptor:)
、@DELETE(url:descriptor:)
、@PATCH(url:descriptor:)
、@CONNECT(url:descriptor:)
、@HEAD(url:descriptor:)
、@OPTIONS(url:descriptor:)
、@QUERY(url:descriptor:)
或@TRACE(url:descriptor:)
指定端点请求的方法和路径 descriptor
参数是可选的
@Endpoint(url: "https://jsonplaceholder.typicode.com/")
struct TestEndpoint: EndpointModel {
@GET(url: "todos/1") var todos: EndpointBuilder<Todo>
@GET(url: "comments") var comments: EndpointBuilder<Data>
@POST(url: "posts") var post: EndpointBuilder<Data>
}
构建请求
现在端点已经准备好,是时候构建一个请求。
class NetworkManager {
private var subscriptions: Set<AnyCancellable> = []
private let endpoint = TestEndpoint()
var todo: Todo?
func callRequest() {
endpoint
.comments
.setRequestParams(.queryParams(["postId": 1]))
.buildPublisher()
.catch { (error) -> Just<Todo?> in
print(error)
return Just(nil)
}
.assign(to: \.todo, on: self)
.store(in: &subscriptions)
}
}
URL 中带有动态值的请求
有时我们需要在请求的 URL 中注入一些变量。为此,您可以使用两种模式:${variable}$
或 #{variable}#
。
${variable}$
应用于已经在您的代码中存在的变量
@Endpoint(url: "${myUrl}$")
struct MyStruct: EndpointModel {
}
宏展开后将是这样的
struct MyStruct: EndpointModel {
let url = "\(myUrl)"
}
#{variable}#
应用于在构建请求时自己提供的变量
@Endpoint(url: "www.someurl.com/comments/#{id}#")
struct MyStruct: EndpointModel {
}
宏展开后将是这样的
struct MyStruct: EndpointModel {
let url = "www.someurl.com/comments/#{id}#"
}
然后在构建请求时,使用 .setUrlValue(_ value: String, forKey key: String)
来进行替换
func buildRequest() async throws -> [Comment] {
endpoint
.comments
.setUrlValue("1", forKey: "id")
.buildAsyncTask()
}
“构建请求”的替代流程
从版本 2.0.1 开始,SwiftNet 允许您通过生成带有描述符的 EndpointBuilders 来更快地完成工作。多亏了描述符,您可以将端点设置提取出来,以减少构建有效端点所需的行数。
final class EndpointDescriptorFactory {
private init() {}
static func singleTodoDescriptor() -> EndpointDescriptor {
.init(urlValues: [.init(key: "id", value: "1")])
}
}
@Endpoint(url: "https://jsonplaceholder.typicode.com/")
struct TestEndpoint: EndpointModel {
@GET(url: "todos/#{id}#", descriptor: EndpointDescriptorFactory.singleTodoDescriptor()) var singleTodo: EndpointBuilder<Todo>
}
现在您只需构建请求,它就会知道如何转换 #{id}#
。
func buildRequest() async throws -> Todo {
endpoint
.singleTodo
.buildAsyncTask()
}
这就是全部了。祝您使用愉快:)