【问题标题】:Swift: How to perform concurrent API calls using CombineSwift:如何使用 Combine 执行并发 API 调用
【发布时间】:2021-09-20 07:05:36
【问题描述】:

我正在尝试使用 Combine 框架执行并发 API 调用。 API 调用的设置如下:

  1. 首先,调用API获取Posts的列表
  2. 对于每个post,调用另一个API来获取Comments

我想使用 Combine 将这两个调用同时链接在一起,以便它返回一个 Post 对象数组,每个 post 都包含 cmets 数组。

我的尝试:

struct Post: Decodable {
    let userId: Int
    let id: Int
    let title: String
    let body: String
    var comments: [Comment]?
}

struct Comment: Decodable {
    let postId: Int
    let id: Int
    let name: String
    let email: String
    let body: String
}

class APIClient: ObservableObject {
    @Published var posts = [Post]()
    
    var cancellables = Set<AnyCancellable>()
    
    init() {
        getPosts()
    }
    
    func getPosts() {
        let urlString = "https://jsonplaceholder.typicode.com/posts"
        guard let url = URL(string: urlString) else {return}
        
        URLSession.shared.dataTaskPublisher(for: url)
            .receive(on: DispatchQueue.main)
            .tryMap({ (data, response) -> Data in
                guard
                    let response = response as? HTTPURLResponse,
                    response.statusCode >= 200 else {
                    throw URLError(.badServerResponse)
                }
                
                return data
            })
            .decode(type: [Post].self, decoder: JSONDecoder())
            .sink { (completion) in
                print("Posts completed: \(completion)")
            } receiveValue: { (output) in
                //Is there a way to chain getComments such that receiveValue would contain Comments??
                output.forEach { (post) in
                    self.getComments(post: post)
                }
            }
            .store(in: &cancellables)
    }
    
    func getComments(post: Post) {
        let urlString = "https://jsonplaceholder.typicode.com/posts/\(post.id)/comments"
        guard let url = URL(string: urlString) else {
            return
        }
        
        URLSession.shared.dataTaskPublisher(for: url)
            .receive(on: DispatchQueue.main)
            .tryMap({ (data, response) -> Data in
                guard
                    let response = response as? HTTPURLResponse,
                    response.statusCode >= 200 else {
                    throw URLError(.badServerResponse)
                }
                
                return data
            })
            .decode(type: [Comment].self, decoder: JSONDecoder())
            .sink { (completion) in
                print("Comments completed: \(completion)")
            } receiveValue: { (output) in
                print("Comment", output)
            }
            .store(in: &cancellables)
    }
}

如何将getComments 链接到getPosts 以便可以在getPosts 中接收cmets 的输出?传统上使用 UIKit,我会使用 DispatchGroup 来完成这项任务。

请注意,我希望只接收来自 APIClient 的帖子的单个 Publisher 事件,以便 SwiftUI 视图仅刷新一次。

【问题讨论】:

  • 有没有像 rxjs 那样的 CombineLatest?
  • @DanChase 有一个 CombineLatest,但我想这个方法会监听 2 个发布者。知道如何在上述用例中应用它吗?
  • @Koh 对不起,我误读了用例。不是一个实际的答案,但过去我已经通过在后端创建一个组合结构来解决这个问题,并且只在 HTTP 端进行一次调用。在后端,API 表面可以调用多个业务层函数并创建一个结构来返回。在我正在进行的一个当前项目中,它已经增长到 8 个 HTTP Get,并且我开始遇到一些问题,一些问题先于其他人返回并给用户造成混乱,以及停止浏览器的问题。我相信 HTTP 1.1 的限制为 6 .. 我希望这比我的 prev 更有帮助。评论。
  • @Koh 更多地考虑 combineLatest,我认为我的思考过程是关于使用 combineLatest 和 2 个 observables,一个用于 master,一个用于 detail,每个循环。但我越想这个想法就越糟糕,这导致了我在上面的评论。

标签: swift swiftui


【解决方案1】:

感谢@matt 在上面 cmets 中的帖子,我已经针对上面的用例调整了该 SO 帖子中的解决方案。

不太确定它是否是最好的实现,但它现在解决了我的问题。

  func getPosts() {
        let urlString = "https://jsonplaceholder.typicode.com/posts"
        guard let url = URL(string: urlString) else {return}
        
        URLSession.shared.dataTaskPublisher(for: url)
            .receive(on: DispatchQueue.main)
            .tryMap({ (data, response) -> Data in
                guard
                    let response = response as? HTTPURLResponse,
                    response.statusCode >= 200 else {
                    throw URLError(.badServerResponse)
                }
                
                return data
            })
            .decode(type: [Post].self, decoder: JSONDecoder())
            .flatMap({ (posts) -> AnyPublisher<Post, Error> in
                //Because we return an array of Post in decode(), we need to convert it into an array of publishers but broadcast as 1 publisher
                Publishers.Sequence(sequence: posts).eraseToAnyPublisher()
            })
            .compactMap({ post in
                //Loop over each post and map to a Publisher
                self.getComments(post: post) 
            })
            .flatMap {$0} //Receives the first element, ie the Post
            .collect() //Consolidates into an array of Posts
            .sink(receiveCompletion: { (completion) in
                print("Completion:", completion)
            }, receiveValue: { (posts) in
                self.posts = posts
            })
            .store(in: &cancellables)
    }
    
    func getComments(post: Post) -> AnyPublisher<Post, Error>? {
        let urlString = "https://jsonplaceholder.typicode.com/posts/\(post.id)/comments"
        guard let url = URL(string: urlString) else {
            return nil
        }
        
        let publisher = URLSession.shared.dataTaskPublisher(for: url)
            .receive(on: DispatchQueue.main)
            .tryMap({ (data, response) -> Data in
                guard
                    let response = response as? HTTPURLResponse,
                    response.statusCode >= 200 else {
                    throw URLError(.badServerResponse)
                }

                return data
            })
            .decode(type: [Comment].self, decoder: JSONDecoder())
            .tryMap { (comments) -> Post in
                var newPost = post
                newPost.comments = comments
                return newPost
            }
            .eraseToAnyPublisher()
        
        return publisher
    }

本质上,我们需要从 getComments 方法返回一个发布者,以便我们可以在 getPosts 中遍历每个发布者。

【讨论】:

    猜你喜欢
    • 2023-02-02
    • 1970-01-01
    • 2020-12-10
    • 1970-01-01
    • 1970-01-01
    • 2018-11-06
    • 1970-01-01
    • 2022-01-02
    • 1970-01-01
    相关资源
    最近更新 更多