这是我参与更文挑战的第28天,活动详情查看: 更文挑战
如何在Swift中创建通用的网络API
App实现网络获取数据时,通常需要支持许多不同的服务器站点,虽然这些站点会返回不同类型的结构数据,但调用它们的底层网络逻辑是非常相似的。
Modeling shared structures
当使用某些Web API时,尤其是那些遵循类似REST设计的API时,收到嵌套数据结构,同时包含通用密钥的数据JSON,这是非常常见的。例如,以下JSON使用result
作为顶级密钥:
{
"result": {
"id": "D4F28578-51BD-40F4-A8BD-387668E06EF8",
"name": "John Sundell",
"twitterHandle": "johnsundell",
"gitHubUsername": "johnsundell"
}
}
复制代码
那在客户端怎么最优雅的处理这种数据逻辑呢?
一种方式是提取成一个专用的响应类型,然后我们可以直接对数据进行解码。比如,上面的JSON代表一个User
模型对象
struct User: Identifiable, Codable {
let id: UUID
var name: String
var twitterHandle: String
var gitHubUsername: String
}
extension User {
struct NetworkResponse: Codable {
var result: User
}
}
复制代码
有了上面的对象,我们就可以这样对返回的数据进行解码
struct UserLoader {
var urlSession = URLSession.shared
func loadUser(withID id: User.ID) -> AnyPublisher<User, Error> {
urlSession.dataTaskPublisher(for: resolveURL(forID: id))
.map(\.data)
.decode(type: User.NetworkResponse.self, decoder: JSONDecoder())
.map(\.result)
.eraseToAnyPublisher()
}
}
复制代码
虽然上面的代码肯定没有问题,但总是得为返回的对象创建专用的NetworkResponse
包装器;这会导致非常多的重复代码。所以让在看看是否能想出一个更通用、可重用的抽象逻辑。
因为每个网络响应都遵循相同的结构,可以先创建一个通用的网络NetworkResponse
类型开始,使用它来加载任何网络数据模型
struct NetworkResponse<Wrapped: Decodable>: Decodable {
var result: Wrapped
}
复制代码
我们现在可以不需要为每种请求创建和维护单独的包装器类型,可以这样做
struct UserLoader {
var urlSession = URLSession.shared
func loadUser(withID id: User.ID) -> AnyPublisher<User, Error> {
urlSession.dataTaskPublisher(for: resolveURL(forID: id))
.map(\.data)
.decode(type: NetworkResponse<User>.self, decoder: JSONDecoder())
.map(\.result)
.eraseToAnyPublisher()
}
}
复制代码
使用泛型类型对网络请求进行改造,为我们创建了便利性API
除了上述loaduser
方法的返回类型之外,它的内部逻辑实际上没有任何User
的东西——事实上,当加载我们应用的任何模型时,我们可能会编写或多或少完全相同的代码——所以让我们将该逻辑提取到共享抽象中:
extension URLSession {
func publisher<T: Decodable>(
for url: URL,
responseType: T.Type = T.self,
decoder: JSONDecoder = .init()
) -> AnyPublisher<T, Error> {
dataTaskPublisher(for: url)
.map(\.data)
.decode(type: NetworkResponse<T>.self, decoder: decoder)
.map(\.result)
.eraseToAnyPublisher()
}
}
复制代码
回到前面的UserLoader
类,现在只需要一行代码
struct UserLoader {
var urlSession = URLSession.shared
func loadUser(withID id: User.ID) -> AnyPublisher<User, Error> {
urlSession.publisher(for: resolveURL(forID: id))
}
}
复制代码
同时,我们可以创建一个通用的模型加载器,模型可以为URL提供指定ID,这样可以加载任何模型对象
struct ModelLoader<Model: Identifiable & Decodable> {
var urlSession = URLSession.shared
var urlResolver: (Model.ID) -> URL
func loadModel(withID id: Model.ID) -> AnyPublisher<Model, Error> {
urlSession.publisher(for: urlResolver(id))
}
}
复制代码
本文原文来自swiftbysundell,非常喜欢他们的写文章,通俗易懂,而且非常严谨,虽然我的英语很烂,但只要你认真读下来也不是很难。swiftbysundell里面有许多关与Swift,SwiftUI开发的文章,每天学习一点,进步就多一点点。
今天算是学习swift第20天吧,坚持不容易,哈哈,给自己点个赞!
明天继续
-
封装多端点网络请求逻辑
-
静态工厂方法应用