Объединить блок Future, вызываемый несколько раз при использовании Flatmap и нескольких подписчиков - PullRequest
0 голосов
/ 19 июня 2020

Я успешно использую BrightFutures в своих приложениях в основном для асинхронных c сетевых запросов. Я решил, что пора посмотреть, смогу ли я перейти на Combine . Однако я обнаружил, что когда я объединяю два Future с использованием flatMap с двумя подписчиками, мой второй блок кода Future выполняется дважды. Вот пример кода, который будет запускаться непосредственно на игровой площадке:

import Combine
import Foundation

extension Publisher {
    func showActivityIndicatorWhileWaiting(message: String) -> AnyCancellable {
        let cancellable = sink(receiveCompletion: { _ in Swift.print("Hide activity indicator") }, receiveValue: { (_) in })
        Swift.print("Busy: \(message)")
        return cancellable
    }
}

enum ServerErrors: Error {
    case authenticationFailed
    case noConnection
    case timeout
}

func authenticate(username: String, password: String) -> Future<Bool, ServerErrors> {
    Future { promise in
        print("Calling server to authenticate")
        DispatchQueue.main.async {
            promise(.success(true))
        }
    }
}

func downloadUserInfo(username: String) -> Future<String, ServerErrors> {
    Future { promise in
        print("Downloading user info")
        DispatchQueue.main.async {
            promise(.success("decoded user data"))
        }
    }
}

func authenticateAndDownloadUserInfo(username: String, password: String) -> some Publisher {
    return authenticate(username: username, password: password).flatMap { (isAuthenticated) -> Future<String, ServerErrors> in
        guard isAuthenticated else {
            return Future {$0(.failure(.authenticationFailed)) }
        }
        return downloadUserInfo(username: username)
    }
}

let future = authenticateAndDownloadUserInfo(username: "stack", password: "overflow")
let cancellable2 = future.showActivityIndicatorWhileWaiting(message: "Please wait downloading")
let cancellable1 = future.sink(receiveCompletion: { (completion) in
    switch completion {
    case .finished:
        print("Completed without errors.")
    case .failure(let error):
        print("received error: '\(error)'")
    }
}) { (output) in
    print("received userInfo: '\(output)'")
}

Код имитирует выполнение двух сетевых вызовов и flatmap объединяет их как единое целое, которое либо успешно, либо терпит неудачу. Результатом будет:

Calling server to authenticate
Busy: Please wait downloading
Downloading user info
Downloading user info <---- неожиданный второй сетевой вызов <br>Hide activity indicator
received userInfo: 'decoded user data'
Completed without errors.

Проблема в том, что downloadUserInfo((username:) вызывается дважды. Если у меня только один абонент, то downloadUserInfo((username:) вызывается только один раз. У меня есть уродливое решение, которое оборачивает flatMap в другой Future, но чувствую, что мне не хватает чего-то простого. Есть мысли?

1 Ответ

1 голос
/ 19 июня 2020

Когда вы создаете фактического издателя с помощью let future, добавьте оператор .share, чтобы два ваших подписчика подписались на один разделенный конвейер.

EDIT: Как я уже сказал в своих комментариях, я бы внес некоторые другие изменения в ваш конвейер. Вот предлагаемая переписать. Некоторые из этих изменений являются стилистическими c / cosmeti c, как иллюстрация того, как я пишу код Combine; Вы можете принять это или оставить. Но другие вещи в значительной степени de rigueur . Вам нужны отложенные оболочки вокруг ваших Futures, чтобы предотвратить преждевременное сетевое взаимодействие (то есть до того, как произойдет подписка). Вам нужно store конвейер, иначе он go перестанет существовать, прежде чем сеть сможет начать работу. Я также заменил .handleEvents вашего второго подписчика, хотя, если вы используете вышеуказанное решение с .share, вы все равно можете использовать второго подписчика, если действительно хотите. Это полный пример; вы можете просто скопировать и вставить его прямо в проект.

class ViewController: UIViewController {
    enum ServerError: Error {
        case authenticationFailed
        case noConnection
        case timeout
    }
    var storage = Set<AnyCancellable>()
    func authenticate(username: String, password: String) -> AnyPublisher<Bool, ServerError> {
        Deferred {
            Future { promise in
                print("Calling server to authenticate")
                DispatchQueue.main.async {
                    promise(.success(true))
                }
            }
        }.eraseToAnyPublisher()
    }
    func downloadUserInfo(username: String) -> AnyPublisher<String, ServerError> {
        Deferred {
            Future { promise in
                print("Downloading user info")
                DispatchQueue.main.async {
                    promise(.success("decoded user data"))
                }
            }
        }.eraseToAnyPublisher()
    }
    func authenticateAndDownloadUserInfo(username: String, password: String) -> AnyPublisher<String, ServerError> {
        let authenticate = self.authenticate(username: username, password: password)
        let pipeline = authenticate.flatMap { isAuthenticated -> AnyPublisher<String, ServerError> in
            if isAuthenticated {
                return self.downloadUserInfo(username: username)
            } else {
                return Fail<String, ServerError>(error: .authenticationFailed).eraseToAnyPublisher()
            }
        }
        return pipeline.eraseToAnyPublisher()
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        authenticateAndDownloadUserInfo(username: "stack", password: "overflow")
            .handleEvents(
                receiveSubscription: { _ in print("start the spinner!") },
                receiveCompletion: { _ in print("stop the spinner!") }
        ).sink(receiveCompletion: {
            switch $0 {
            case .finished:
                print("Completed without errors.")
            case .failure(let error):
                print("received error: '\(error)'")
            }
        }) {
            print("received userInfo: '\($0)'")
        }.store(in: &self.storage)
    }
}

Вывод:

start the spinner!
Calling server to authenticate
Downloading user info
received userInfo: 'decoded user data'
stop the spinner!
Completed without errors.
...