Swift Combine correct use of Future

2.2k views Asked by At

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.

1

There are 1 answers

0
New Dev On

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.

protocol BookListRepository: AnyObject {
    func getAll() -> AnyPublisher<[Book], Error>
}

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:

final class BookService {
    private let repository: BookListRepository

    func getAll() -> AnyPublisher<[Book], Error> {
        repository.getAll()
    }
}

class BookApi: BookListRepository {
    func getAll() -> AnyPublisher<[Book], Error> {
        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())
            .eraseToAnyPublisher()
    }
}