这是我参与更文挑战的第29天,活动详情查看: 更文挑战
前言
我们大多数的app 都需要依赖网络调用获取数据, 多亏有了 URLSession 和 Codable,让我们的 REST APIs 调用变得很简单;
但是我们仍然需要写很多代码来处理异步回调,JSON解析,http 错误处理等等;
当然我们可以使用像 Alamofire 这样强大的网络库,因为它强大,所有他必须是多功能,多用途的,才能对大家在想用的时候就用,同时也包括了很多我们永远不会使用的功能。
考虑到这一点,打算编写一个简单的网络库,专门用于REST API 数据请求。
The Request
我们定义一个网络请求协议,这里面包括我们需要的一些常见的属性功能;
- Path or URL
- The HTTP Method (GET, POST, PUT, DELETE)
- The request Body
- headers
import Foundation
import Combine
public enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case delete = "DELETE"
}
public protocol Request {
var path: String { get }
var method: HTTPMethod { get }
var contentType: String { get }
var body: [String: Any]? { get }
var headers: [String: String]? { get }
associatedtype ReturnType: Codable
}
复制代码
除了定义属性,我们还添加了一个关联类型:associatedType
,associatedType
作为一个占位符在我们实现协议时使用
定义了协议之后,我们在扩展设置一些默认值。默认情况下, 请求使用 GET
方法,content-type: application/json
类型,它的正文、headers和query参数将为空。
extension Request {
// Defaults
var method: String { return .get }
var contentType: String { return “application/json” }
var queryParams: [String: String]? { return nil }
var body: [String: Any]? { return nil }
var headers: [String: String]? { return nil }
}
复制代码
因为我们将 URLSession 执行所有网络调用,需要在写一个实用的方法,可以把自定义请求类型转换为普通的URL请求对象
两个方法:requestBodyFrom
序列化字典对象 ,asURLRequest 转换成一个 URLRequest
对象
extension Request {
/// Serializes an HTTP dictionary to a JSON Data Object
/// - Parameter params: HTTP Parameters dictionary
/// - Returns: Encoded JSON
private func requestBodyFrom(params: [String: Any]?) -> Data? {
guard let params = params else { return nil }
guard let httpBody = try? JSONSerialization.data(withJSONObject: params, options: []) else {
return nil
}
return httpBody
}
/// Transforms a Request into a standard URL request
/// - Parameter baseURL: API Base URL to be used
/// - Returns: A ready to use URLRequest
func asURLRequest(baseURL: String) -> URLRequest? {
guard var urlComponents = URLComponents(string: baseURL) else { return nil }
urlComponents.path = "\(urlComponents.path)\(path)"
guard let finalURL = urlComponents.url else { return nil }
var request = URLRequest(url: finalURL)
request.httpMethod = method.rawValue
request.httpBody = requestBodyFrom(params: body)
request.allHTTPHeaderFields = headers
return request
}
}
复制代码
有了这个协议,定一个一个网络请求对象就变得非常简单
// Model
struct Todo: Codable {
var title: String
var completed: Bool
}
// Request
struct FindTodos: Request {
typealias ReturnType = [Todo]
var path: String = "/todos"
}
复制代码
/todo
将转化成 GET 请求,返回 TODO 项列表
The Dispatcher 调度器
请求已经准备好了,但我们还需要调用网络功能同时获取数据并解析,这里我们将使用 Combine 和 Codable.
第一步是定义一个枚举来保存错误代码。
enum NetworkRequestError: LocalizedError, Equatable {
case invalidRequest
case badRequest
case unauthorized
case forbidden
case notFound
case error4xx(_ code: Int)
case serverError
case error5xx(_ code: Int)
case decodingError
case urlSessionFailed(_ error: URLError)
case unknownError
}
复制代码
编写调度函数。通过使用泛型,我们可以定义返回类型,并返回一个Publisher,将请求的输出传递给它的订阅者。
我们的NetworkDispatcher将收到一个URL请求,通过网络请求并为我们解析返回的JSON数据。
NetworkDispatcher.swiftstruct NetworkDispatcher {
let urlSession: URLSession!
public init(urlSession: URLSession = .shared) {
self.urlSession = urlSession
}
/// Dispatches an URLRequest and returns a publisher
/// - Parameter request: URLRequest
/// - Returns: A publisher with the provided decoded data or an error
func dispatch<ReturnType: Codable>(request: URLRequest) -> AnyPublisher<ReturnType, NetworkRequestError> {
return urlSession
.dataTaskPublisher(for: request)
// Map on Request response
.tryMap({ data, response in
// If the response is invalid, throw an error
if let response = response as? HTTPURLResponse,
!(200...299).contains(response.statusCode) {
throw httpError(response.statusCode)
}
// Return Response data
return data
})
// Decode data using our ReturnType
.decode(type: ReturnType.self, decoder: JSONDecoder())
// Handle any decoding errors
.mapError { error in
handleError(error)
}
// And finally, expose our publisher
.eraseToAnyPublisher()
}
}
复制代码
我们使用 URLSession’s dataTaskPublisher 执行请求, 然后映射响应,正确的处理错误,如果请求成功完成,继续解析返回的JSON数据;
定义处理错误的函数, 第一:httpError,处理返回的 HTTP errors。 第二:handleError 处理JSON Decoding 中发生的错误
NetworkDispatcher.swiftextension NetworkDispatcher {
/// Parses a HTTP StatusCode and returns a proper error
/// - Parameter statusCode: HTTP status code
/// - Returns: Mapped Error
private func httpError(_ statusCode: Int) -> NetworkRequestError {
switch statusCode {
case 400: return .badRequest
case 401: return .unauthorized
case 403: return .forbidden
case 404: return .notFound
case 402, 405...499: return .error4xx(statusCode)
case 500: return .serverError
case 501...599: return .error5xx(statusCode)
default: return .unknownError
}
}
/// Parses URLSession Publisher errors and return proper ones
/// - Parameter error: URLSession publisher error
/// - Returns: Readable NetworkRequestError
private func handleError(_ error: Error) -> NetworkRequestError {
switch error {
case is Swift.DecodingError:
return .decodingError
case let urlError as URLError:
return .urlSessionFailed(urlError)
case let error as NetworkRequestError:
return error
default:
return .unknownError
}
}
}
复制代码
APIClient
Now that we have both our Request and Dispatcher, let’s create a type to wrap our API Calls.
现在我们有了Request 和 Dispatche,再创建一个对象来包装我们的API请求;
我们的APIClient将收到一个NetworkDispatcher和一个BaseUrl,并将提供一个集中的请求方法。该方法将接收一个Request,将其转换为一个URL请求,并将其传递给提供的dispatcher。
APIClient.swiftstruct APIClient {
var baseURL: String!
var networkDispatcher: NetworkDispatcher!
init(baseURL: String,
networkDispatcher: NetworkDispatcher = NetworkDispatcher()) {
self.baseURL = baseURL
self.networkDispatcher = networkDispatcher
}
/// Dispatches a Request and returns a publisher
/// - Parameter request: Request to Dispatch
/// - Returns: A publisher containing decoded data or an error
func dispatch<R: Request>(_ request: R) -> AnyPublisher<R.ReturnType, NetworkRequestError> {
guard let urlRequest = request.asURLRequest(baseURL: baseURL) else {
return Fail(outputType: R.ReturnType.self, failure: NetworkRequestError.badRequest).eraseToAnyPublisher()
}
typealias RequestPublisher = AnyPublisher<R.ReturnType, NetworkRequestError>
let requestPublisher: RequestPublisher = networkDispatcher.dispatch(request: urlRequest)
return requestPublisher.eraseToAnyPublisher()
}
}
复制代码
返回的Publisher 要么响应请求错误,要么响应解析错误,而且可以自定义我们自己的NetworkDispatcher,测试爷非常容易。
Performing a request ,执行请求
到这里,如果我们要执行一个网络请求,我们可以这么做:
private var cancellables = [AnyCancellable]()
let dispatcher = NetworkDispatcher()
let apiClient = APIClient(baseURL: "https://jsonplaceholder.typicode.com")
apiClient.dispatch(FindTodos())
.sink(receiveCompletion: { _ in },
receiveValue: { value in
print(value)
})
.store(in: &cancellables)
复制代码
是不是很爽,在这种情况下,我们正在执行一个简单的GET请求,但是你也可以根据你的请求添加额外的参数,自定义你的请求对象
比如:如果我们想添加一个Todo,我们可以这样做:
// Our Add Request
struct AddTodo: Request {
typealias ReturnType = [Todo]
var path: String = "/todos"
var method: HTTPMethod = .post
var body: [String: Any]
init(body: [String: Any]) {
self.body = body
}
}
let todo: [String: Any] = ["title": "Test Todo", "completed": true]
apiClient.dispatch(AddTodo(body: todo))
.sink(receiveCompletion: { result in
// Do something after adding...
},
receiveValue: { _ in })
.store(in: &cancellables)
复制代码
在这种情况下,我们从一个简单的字典构建正文,但是为了让事情变得更容易,让我们扩展Encoable,并添加一个方法来将Encoable Type转换 asDictionary。
extension Encodable {
var asDictionary: [String: Any] {
guard let data = try? JSONEncoder().encode(self) else { return [:] }
guard let dictionary = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else {
return [:]
}
return dictionary
}
}
复制代码
有了这个,你可以这么写你的请求:
let otherTodo: Todo = Todo(title: "Test", completed: true)
apiClient.dispatch(AddTodo(body: otherTodo.asDictionary))
.sink(receiveCompletion: { result in
// Do something after adding...
},
receiveValue: { _ in })
.store(in: &cancellables)
复制代码
结论
感谢苹果提供的Combine 和 Codable,让我们能够编写一个非常简单的网络客户端。请求类型是可扩展的,易于维护,我们的网络调度器和应用编程接口客户端都易于测试,使用极其简单。
当然你还可以扩展一些额外的功能,比如添加身份验证、缓存和更详细的日志记录,这样变得更加健壮可用!
本文翻译自:danielbernal.co/writing-a-n…
建议结合昨天写得上篇如何在Swift中创建通用的网络API(上)学习, 你将对swift网络请求有了更深一层的认识!
如果对你有帮助,就点赞再走 ❤️
明天继续在Swift路上前行!