在添加了对 Swift Concurrency 的支持后,CombineNetworking 变成了 SwiftNet!这是一个易于使用的框架,可以帮助你方便地创建和处理网络请求。除了基本的网络请求外,SwiftNet 还允许你通过简单的 SSL/证书绑定和内置的自动授权机制安全地发送请求。
如果你正在寻找简洁、类似 Retrofit 的 Swift Macros 驱动的体验,请查看 Snowdrop。
pod 'SwiftNet-macroless'
Endpoint
模型轻松发送请求SNWebSocket
支持 WebSocket 连接global
、endpoint specific
(default
) 或 custom
策略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)
.bodyParams([String: Any])
- 接收 Dictionary
并将其解析为 Data
以在请求正文中发送.urlEncodedBody([String: Any])
- 接收 Dictionary
并将其解析为 URL 编码的 Data
以在请求正文中发送.urlEncodedModel(Encodable)
- 接收 Encodable
模型并将其解析为 URL 编码的 Data
以在请求正文中发送.jsonModel(Encodable)
- 类似于 .dataParams
,但此选项接收 Encodable
并将其解析为 Data
以在请求正文中发送要在你的应用中启用 SSL 和/或证书绑定,只需添加
SNConfig.pinningModes = [.ssl, .certificate]
请记住,SSL/证书绑定需要将证书文件附加到你的项目中。SwiftNet 会自动加载证书和 SSL 密钥。
使用 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
协议。
pinningModes
- 开启/关闭 SSL 和证书绑定。可用选项为 .ssl
、.certificate
或两者兼有。sitesExcludedFromPinning
- 从 SSL/证书绑定检查中排除的网站地址列表defaultJSONDecoder
- 使用此属性全局设置你的自定义 JSONDecoderdefaultAccessTokenStrategy
- 存储访问令牌的全局策略。可用选项为 .global
和 .custom(String)
。keychainInstance
- SwiftNet 用于从 Apple 的 Keychain 存储/获取访问令牌的 keychain 实例。如果未提供,安全存储将被关闭(更多信息见下文)accessTokenStorage
- 实现 AccessTokenStorage 协议的对象的实例。它用于操作访问令牌。默认情况下,它使用内置的 SNStorage
。要使用不同的存储,请提供你自己的实例。accessTokenErrorCodes
- 包含应触发访问令牌刷新操作的错误代码的数组。默认值:[401]。SwiftNet 允许你全局以及为每个端点单独指定访问令牌策略。你可以通过为 SNConfig.defaultAccessTokenStrategy
设置策略,或者在你的 Endpoint
中为字段 accessTokenStrategy
设置值来指定你的策略。可用选项为
.global
- 使用全局标签存储访问令牌.custom(String)
- 使用此选项,你可以指定你自己的标签来存储访问令牌,并在任意数量的端点中使用它由于访问令牌策略既可以全局设置(通过 SNConfig
),也可以单独设置(在 Endpoint
内部),你可以在你的应用中混合使用不同的策略!
如果需要,你可以自己操作访问令牌。
可用的方法有
setAccessToken(_ token:, for:)
accessToken(for:)
removeAccessToken(for:)
setGlobalAccessToken(_ token:)
globalAccessToken()
removeGlobalAccessToken()
SwiftNet 的 SNProvider 默认对每个请求使用 iOS 内置的 Logger(如果在 iOS 14 或更高版本上运行)和自定义的仅在调试模式下使用的 logger。
CombineNetowrking 允许你持续监控网络连接状态。如果你想订阅网络连接监控器的发布者,你可以这样做
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)
}
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
。
SwiftNet 还允许你轻松连接 WebSockets。只需像这样使用 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:)
宏来设置你的端点的 baseURL@GET(url:descriptor:)
、@POST(url:descriptor:)
、@PUT(url:descriptor:)
、@DELETE(url:descriptor:)
、@PATCH(url:descriptor:)
、@CONNECT(url:descriptor:)
、@HEAD(url:descriptor:)
、@OPTIONS(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 中。为此,你可以使用两种模式:${variable}$
或 #{variable}#
。
@Endpoint(url: "${myUrl}$")
struct MyStruct: EndpointModel {
}
宏展开后将看起来像
struct MyStruct: EndpointModel {
let url = "\(myUrl)"
}
@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()
}
就这样。享受吧 :)