How to convert from callback to Future Publisher in Combine

Issue #527

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import Foundation
import Combine

public typealias TaskCompletion = (Result<(), Error>) -> Void

public protocol Task: AnyObject {
var name: String { get }
func run(workflow: Workflow, completion: TaskCompletion)
}

public extension Task {
func asPublisher(workflow: Workflow) -> AnyPublisher<(), Error> {
return Future({ completion in
self.run(workflow: workflow, completion: completion)
}).eraseToAnyPublisher()
}
}

let sequence = Publishers.Sequence<[AnyPublisher<(), Error>], Error>(
sequence: tasks.map({ $0.asPublisher(workflow: self) })
)

How to use objectWillChange in Combine

Issue #513

A publisher that emits before the object has changed

Use workaround DispatchQueue to wait another run loop to access newValue

1
2
3
4
5
6
7
8
9
.onReceive(store.objectWillChange, perform: {
DispatchQueue.main.async {
self.reload()
}
})

func reload() {
self.isFavorite = store.isFavorite(country: country)
}

Read more

How to map error in Combine

Issue #506

When a function expects AnyPublisher<[Book], Error> but in mock, we have Just

1
2
3
4
5
6
7
func getBooks() -> AnyPublisher<[Book], Error> {
return Just([
Book(id: "1", name: "Book 1"),
Book(id: "2", name: "Book 2"),
])
.eraseToAnyPublisher()
}

There will be a mismatch, hence compile error

Cannot convert return expression of type ‘AnyPublisher<[Book], Just.Failure>’ (aka ‘AnyPublisher<Array, Never>’) to return type ‘AnyPublisher<[Book], Error>’

The reason is because Just produces Never, not Error. The workaround is to introduce Error

1
2
3
enum AppError: Error {
case impossible
}
1
2
3
4
5
6
7
8
func getBooks() -> AnyPublisher<[Book], Error> {
return Just([
Book(id: "1", name: "Book 1"),
Book(id: "2", name: "Book 2"),
])
.mapError({ _ in AppError.impossible })
.eraseToAnyPublisher()
}

How to flat map publisher of publishers in Combine

Issue #451

For some services, we need to deal with separated APIs for getting ids and getting detail based on id.

To chain requests, we can use flatMap and Sequence, then collect to wait and get all elements in a single publish

FlatMap

Transforms all elements from an upstream publisher into a new or existing publisher.

1
struct FlatMap<NewPublisher, Upstream> where NewPublisher : Publisher, Upstream : Publisher, NewPublisher.Failure == Upstream.Failure

Publishers.Sequence

A publisher that publishes a given sequence of elements.

1
struct Sequence<Elements, Failure> where Elements : Sequence, Failure : Error
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
func fetchItems(completion: @escaping ([ItemProtocol]) -> Void) {
requestCancellable = URLSession.shared
.dataTaskPublisher(for: topStoriesUrl())
.map({ $0.data })
.decode(type: [Int].self, decoder: JSONDecoder())
.flatMap({ (ids: [Int]) -> AnyPublisher<[HackerNews.Item], Error> in
let publishers = ids.prefix(10).map({ id in
return URLSession.shared
.dataTaskPublisher(for: self.storyUrl(id: id))
.map({ $0.data })
.decode(type: HackerNews.Item.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
})

return Publishers.Sequence<[AnyPublisher<HackerNews.Item, Error>], Error>(sequence: publishers)
// Publishers.Sequence<[AnyPublisher<HackerNews.Item, Error>], Error>
.flatMap({ $0 })
// Publishers.FlatMap<AnyPublisher<HackerNews.Item, Error>, Publishers.Sequence<[AnyPublisher<HackerNews.Item, Error>], Error>>
.collect()
// Publishers.Collect<Publishers.FlatMap<AnyPublisher<HackerNews.Item, Error>, Publishers.Sequence<[AnyPublisher<HackerNews.Item, Error>], Error>>>
.eraseToAnyPublisher()
// AnyPublisher<[HackerNews.Item], Error>
})
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
.sink(receiveCompletion: { completionStatus in
switch completionStatus {
case .finished:
break
case .failure(let error):
print(error)
}
}, receiveValue: { items in
completion(items)
})
}