使用URLSession,Combine与Codable构建一个特Swift网络层

3个月前 190次点击 来自 iOS

标签: Swift

原文地址:Modern Networking in Swift 5 with URLSession, Combine and Codable

直接上代码:

Agent类的作用是接收单一URLRequest请求对象,将JSON数据转变为Codable对象,并返回AnyPublisher对象。

import Combine

struct Agent {    
    // 1
    struct Response<T> {
        let value: T
        let response: URLResponse
    }
    
    // 2
    func run<T: Decodable>(_ request: URLRequest, _ decoder: JSONDecoder = JSONDecoder()) -> AnyPublisher<Response<T>, Error> {
        return URLSession.shared
            .dataTaskPublisher(for: request) // 3
            .tryMap { result -> Response<T> in
                let value = try decoder.decode(T.self, from: result.data) // 4
                return Response(value: value, response: result.response) // 5
            }
            .receive(on: DispatchQueue.main) // 6
            .eraseToAnyPublisher() // 7
    }
}

使用Github REST API作为测试API,请求用户所有Repository:

enum GithubAPI {
    static let agent = Agent()
    static let base = URL(string: "https://api.github.com")!
}

extension GithubAPI {
    
    static func repos(username: String) -> AnyPublisher<[Repository], Error> {
        // 1
        let request = URLRequest(url: base.appendingPathComponent("users/\(username)/repos"))
        // 2
        return agent.run(request)
            .map(\.value)
            .eraseToAnyPublisher()
    }
}

// 3
struct Repository: Codable {
    // Skipping for brevity
}

OK,试一下效果:

let token = GithubAPI.repos(username: "V8tr")
    .print()
    .sink(receiveCompletion: { _ in },
          receiveValue: { print($0) })

输出如下:

receive subscription: (TryMap)
request unlimited
receive value: [... the list of repositories]
receive finished

如果需要中途取消请求:

token.cancel()

串行请求 Chaining Requests

先获取用户所有Repository,然后再获取第一个Repository的issues:

extension GithubAPI {

    static func issues(repo: String, owner: String) -> AnyPublisher<[Issue], Error> {
        let request = URLRequest(url: base.appendingPathComponent("repos/\(owner)/\(repo)/issues"))
        return agent.run(request)
            .map(\.value)
            .eraseToAnyPublisher()
    }
}

struct Issue: Codable {
    // Skipping for brevity
}

试一下:

let me = "V8tr"
let repos = GithubAPI.repos(username: me) // 1
let firstRepo = repos.compactMap { $0.first } // 2
// 3
let issues = firstRepo.flatMap { repo in
    GithubAPI.issues(repo: repo.name, owner: me)
}
// 4
let token = issues.sink(receiveCompletion: { _ in },
                        receiveValue: { print($0) })

Combine让代码简洁到原地爆炸的同时,逻辑书写保持清晰。

并行请求 Running Requests in Parallel

并行请求就是同时发起多个网络请求,其请求时长取决于响应最慢的那个请求。
例如,同时获取AppleGithubAPI.membersGithubAPI.repos信息:

1. 提取一个公共方法:

extension GithubAPI {

    static func run<T: Decodable>(_ request: URLRequest) -> AnyPublisher<T, Error> {
        return agent.run(request)
            .map(\.value)
            .eraseToAnyPublisher()
    }

    static func repos(username: String) -> AnyPublisher<[Repository], Error> {
        return run(URLRequest(url: base.appendingPathComponent("users/\(username)/repos"))
    }

    // Skipping `issues(repo:owner:)`
}

2. 获取数据

extension GithubAPI {

    static func repos(org: String) -> AnyPublisher<[Repository], Error> {
        return run(URLRequest(url: base.appendingPathComponent("orgs/\(org)/repos")))
    }
    
    static func members(org: String) -> AnyPublisher<[User], Error> {
        return run(URLRequest(url: base.appendingPathComponent("orgs/\(org)/members")))
    }
}

3. 发起并行请求

let members = GithubAPI.members(org: "apple") // 1
let repos = GithubAPI.repos(org: "apple") // 2
let token = Publishers.Zip(members, repos) // 3
    .sink(receiveCompletion: { _ in },
          receiveValue: { (members, repos) in print(members, repos) }) // 4
Card image cap
开发者雷

尘世间一个小小的开发者,每天增加一些无聊的知识,就不会无聊了

要加油~~~

技术文档 >> 系列应用 >>
热推应用
Let'sLearnSwift
学习Swift的入门教程
PyPie
Python is as good as Pie
标签