Say I have a three layered architecture (Data, Domain and View) and I want to access and provide some data. The three layers are part of different targets and are initialised using dependency injection.
In the domain layer I have the following types:
protocol BookListRepository: AnyObject {
func getAll() -> Future<[Book], Error>
}
final class BookService {
private let repository: BookListRepository
init(repository: BookListRepository) {
self.repository = repository
}
func getAll() -> Future<[Book], Error> {
repository.getAll()
}
}
In data I define the following:
class BookApi: BookListRepository {
func getAll() -> Future<[Book], Error> {
.init { promise in
let cancellable = urlSession
.dataTaskPublisher(for: url)
.tryMap() { element -> Data in
guard
let httpResponse = element.response as? HTTPURLResponse,
httpResponse.statusCode == 200
else { throw URLError(.badServerResponse) }
return element.data
}
.decode(type: [Book]].self, decoder: JSONDecoder())
.sink(receiveCompletion: { completion in
guard case let .failure(error) = completion
promise(.failure(error))
},
receiveValue: { books in
promise(.success(books))
}
}
}
In my view layer I would access this in a similar way to this:
let service: BookService = .init(repository: BookApi())
service
.getAll()
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { print($0) }) { books in
// Displau
}
.store(in: &cancelables)
My question here is the following: Is this in any way a good practice and if not what is the correct/preferred way to achieve what I want.
In Combine (and other similar frameworks), subscribers care about what values and what errors publishers emit, so it's customary to use a
AnyPublisher
at an API boundary.Operators
.sink
and.assign
create a subscription to the publisher. You'd want to subscribe only at the final consumption site of the data, and return the publisher in the intermediate steps: