I am writing a Hacker News iOS application using SwiftUI/Combine. They have an API call for getting the ids of top posts and then you are supposed to request each story by itself. For this I have created storyIds(:) -> AnyPublisher<[Int], Error> and story(for:) -> AnyPublisher<Post, Error> for those calls.
Now I want to combine them into one function, getStories() which first download the identifiers and then goes through them fetching the stories one by one. I suppose you can use MergeMany or something else in the API for achieving this but I am not sure
The last thing I need is a function that combine these into stories() -> AnyPublisher<[Post], Error>. I found another question doing almost that. What I miss however, is a way to report progress of the task. I would like to update a counter for each fetched story to show the user how much is left of the download. How can I do this?
struct Agent {
struct Response<T> {
let value: T
let response: URLResponse
}
func run<T: Decodable>(_ request: URLRequest, _ decoder: JSONDecoder = JSONDecoder()) -> AnyPublisher<Response<T>, Error> {
return URLSession.shared
.dataTaskPublisher(for: request)
.tryMap { result -> Response<T> in
let value = try decoder.decode(T.self, from: result.data)
return Response(value: value, response: result.response)
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
enum HackerNewsAPI {
static let agent = Agent()
static let base = URL(string: "https://hacker-news.firebaseio.com/v0/")!
}
extension HackerNewsAPI {
static func storyIds() -> AnyPublisher<[Int], Error> {
let request = URLRequest(url: base.appendingPathComponent("topstories.json"))
return agent.run(request)
.print()
.map(\.value)
.eraseToAnyPublisher()
}
}
extension HackerNewsAPI {
static func story(for id: Int) -> AnyPublisher<Post, Error> {
let request = URLRequest(url: base.appendingPathComponent("item/\(id).json"))
return agent.run(request)
.map(\.value)
.eraseToAnyPublisher()
}
}
extension HackerNewsAPI {
static func stories() -> AnyPublisher<[Post], Error> {
HackerNewsAPI.storyIds()
.flatMap { storyIDs -> AnyPublisher<[Post], Error> in
let stories = storyIDs.map { story(for: $0) }
return Publishers.MergeMany(stories)
.collect()
.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
}