Swift:如何使用组合执行并发 API 调用

Koh*_*Koh 6 swift swiftui

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

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

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

我的尝试:

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)
    }
}
Run Code Online (Sandbox Code Playgroud)

如何链接getCommentsgetPosts以便可以在 中接收评论的输出getPosts?传统上我会使用 UIKitDispatchGroup来完成此任务。

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

Koh*_*Koh 1

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

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

  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
    }
Run Code Online (Sandbox Code Playgroud)

本质上,我们需要从 getComments 方法返回一个 Publisher,以便我们可以循环 getPosts 内的每个发布者。