SwiftNet-macroless 2.0.5

SwiftNet-macroless 2.0.5

Maciej Burdzicki 维护。



  • 作者
  • Maciej Burdzicki

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

SwiftNet

在添加 Swift Concurrency 支持后,CombineNetworking 成为 SwiftNet!这是一个超级轻量级且极易使用的框架,可以帮助您以方便的方式创建和处理网络请求。除了基本网络请求外,SwiftNet 允许您通过简单的 SSL 和证书固定机制轻松安全地发送请求。但这并非全部。使用 SwiftNet,您还可以轻松地通过内置的自动授权机制处理授权令牌。

安装(使用 CocoaPods)

pod 'SwiftNet-macroless'

请注意,为了使用 SwiftNet,您的 iOS 部署目标必须是 13.0 或更高版本。如果您为 macOS 编码,则您的部署目标必须是 10.15 或更高版本。

SwiftNet 的 CocoaPods 版本不包含 SwiftNetMacros,因此您无法使用它们。

主要功能

  • 使用 Endpoint 模型轻松发送请求
  • 只需两行代码即可启用 SSL 和证书固定
  • 支持 WebSocket 连接,使用 SNWebSocket
  • 使用密钥链安全存储访问令牌
  • 访问令牌存储策略 - 为所有或仅一些端点配置 globalendpoint specificdefault)或 custom 策略
  • 自动刷新令牌/回调请求

基本用法

  1. 基于枚举的联网
  2. 基于宏的联网

创建一个端点进行处理

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 也是一个枚举,具有以下选项

  • .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 和/或证书固定(可选)

要使您的应用程序中的 SSL 和/或证书固定有效,只需添加

SNConfig.pinningModes = [.ssl, .certificate]

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

自动授权机制

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

  • pinningModes - 启用/关闭 SSL 和证书固定。可用选项是 .ssl.certificate 或两者都是。
  • sitesExcludedFromPinning - 从 SSL/证书固定检查中排除的网站地址列表
  • defaultJSONDecoder - 使用此属性来全局设置您的自定义 JSONDecoder
  • defaultAccessTokenStrategy - 存储访问令牌的全局策略。可用的选项是 .global.custom(String)
  • keychainInstance - SwiftNet使用此密钥链实例从Apple的密钥链中存储/检索访问令牌。如果未提供,将关闭安全存储(更多信息见下文)
  • 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或更高版本)和具有自定义调试模式的记录器,对于每个请求都是如此。

网络连接监控

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

使用密钥链进行安全存储

SwiftNet允许您将访问令牌存储在密钥链中。要使用密钥链存储访问令牌,您需要通过设置SNConfig.keychainInstance的值来提供密钥链实例。

请记住,Apple的密钥链不会在应用被删除时自动删除由应用创建的条目。但是,请不要担心。只有您的应用程序可以访问这些条目。不过,如果不需要 anymore,您需要确保从密钥链中删除这些条目。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还允许您轻松连接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:) 宏来设置端点的基本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()
}

这就是全部内容。祝您享受 :)。