Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I loop over the output of a publisher with Combine?

Tags:

swift

combine

I'm working on rewriting my Hacker News reader to use Combine more heavily. I'm have two functions which both return an AnyPublisher, one of them get's the ids of a bunch of HN stories from the server and the other one fetches a story by it's id. I'm not sure how I could loop over the results of fetchStoryIds, run fetchStory with the id and end up with an array of Story objects with Combine.

import Combine
import Foundation

struct HackerNewsService {
    private var session = URLSession(configuration: .default)
    static private var baseURL = "https://hacker-news.firebaseio.com/v0"

    private func fetchStoryIds(feed: FeedType) -> AnyPublisher<[Int], Error> {
       let url = URL(string: "\(HackerNewsService.baseURL)/\(feed.rawValue.lowercased())stories.json")!

        return session.dataTaskPublisher(for: url)
            .retry(1)
            .map { $0.data }
            .decode(type: [Int].self, decoder: JSONDecoder())
            .eraseToAnyPublisher()
    }

    private func fetchStory(id: Int) -> AnyPublisher<Story, Error> {
        let url = URL(string: "\(HackerNewsService.baseURL)/item/\(id).json")!

        return session.dataTaskPublisher(for: url)
            .map { $0.data }
            .decode(type: Story.self, decoder: JSONDecoder())
            .eraseToAnyPublisher()
    }
}

Before I started the rewrite, I used this code to loop over the ids and get the stories.

func fetchStories(feed: FeedType, completionHandler: @escaping ([Story]?, Error?) -> Void) {
        fetchStoryIds(feed: feed) { (ids, error) in
            guard error == nil else {
                completionHandler(nil, error)
                return
            }

            guard let ids = ids else {
                completionHandler(nil, error)
                return
            }

            let dispatchGroup = DispatchGroup()

            var stories = [Story]()

            for id in ids {
                dispatchGroup.enter()

                self.fetchStory(id: id) { (story, error) in
                    guard error == nil else {
                        dispatchGroup.leave()
                        return
                    }

                    guard let story = story else {
                        dispatchGroup.leave()
                        return
                    }

                    stories.append(story)

                    dispatchGroup.leave()
                }
            }

            dispatchGroup.notify(queue: .main) {
                completionHandler(stories, nil)
            }
        }
    }
}

1 Answers

Hmm.. It doesn't look like there is a Publishers.ZipMany that accepts a collection of publishers, so instead I merged the stories and collected them instead. Ideally this would collect them in the correct order but I haven't tested that and the documentation is still somewhat sparse across Combine.

func fetchStories(feed: FeedType) -> AnyPublisher<[Story], Error> {
    fetchStoryIds(feed: feed)
        .flatMap { ids -> AnyPublisher<[Story], Error> in
            let stories = ids.map { self.fetchStory(id: $0) }
            return Publishers.MergeMany(stories)
                .collect()
                .eraseToAnyPublisher()
        }
        .eraseToAnyPublisher()
}

If you are open to external code this is a gist implementation of ZipMany that will preserve the order: https://gist.github.com/mwahlig/725fe5e78e385093ba53e6f89028a41c

Although I would think that such a thing would exist in the framework.

like image 112
keji Avatar answered Oct 23 '25 08:10

keji



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!