alt [version] alt cocoapods available alt spm available alt carthage unavailable

SwiftNet

在添加了对 Swift Concurrency 的支持后,CombineNetworking 变成了 SwiftNet!这是一个易于使用的框架,可以帮助你方便地创建和处理网络请求。除了基本的网络请求外,SwiftNet 还允许你通过简单的 SSL/证书绑定和内置的自动授权机制安全地发送请求。

如果你正在寻找简洁、类似 Retrofit 的 Swift Macros 驱动的体验,请查看 Snowdrop

安装(使用 CocoaPods)

pod 'SwiftNet-macroless'

请注意,为了使用 SwiftNet,你的 iOS Deployment Target 必须是 13.0 或更高版本。如果你为 macOS 编码,你的 Deployment Target 必须是 10.15 或更高版本。

CocoaPods 版本的 SwiftNet 不包含 SwiftNetMacros,因此你不能使用它们。

主要功能

基本用法

创建一个要使用的端点

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.deletepatchEndpointData 也是一个枚举,包含以下选项

启用 SSL 和/或证书绑定(可选)

要在你的应用中启用 SSL 和/或证书绑定,只需添加

SNConfig.pinningModes = [.ssl, .certificate]

请记住,SSL/证书绑定需要将证书文件附加到你的项目中。SwiftNet 会自动加载证书和 SSL 密钥。

自动授权机制

使用 SwiftNet 处理授权回调非常容易。要将其与你的 Endpoint 一起使用,你只需添加 requiresAccessTokencallbackPublisher 字段,如下所示

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 属性和方法

访问令牌策略

SwiftNet 允许你全局以及为每个端点单独指定访问令牌策略。你可以通过为 SNConfig.defaultAccessTokenStrategy 设置策略,或者在你的 Endpoint 中为字段 accessTokenStrategy 设置值来指定你的策略。可用选项为

由于访问令牌策略既可以全局设置(通过 SNConfig),也可以单独设置(在 Endpoint 内部),你可以在你的应用中混合使用不同的策略!

访问令牌操作

如果需要,你可以自己操作访问令牌。

可用的方法有

事件日志记录

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)
}

使用 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?
}

可用的错误类型有:failedToBuildRequestfailedToMapResponseunexpectedResponseauthenticationFailednotConnectedemptyResponsenoInternetConnectionconversionFailed

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 还允许你轻松连接 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: "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()
}

就这样。享受吧 :)