Combine Future block called multiple times when using Flatmap and multiple subscribers

和自甴很熟 提交于 2021-01-29 08:56:44

问题


I've been successfully using BrightFutures in my apps mainly for async network requests. I decided it was time to see if I could migrate to Combine. However what I find is that when I combine two Futures using flatMap with two subscribers my second Future code block is executed twice. Here's some example code which will run directly in a playground:

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)'")
}

The code simulates making two network calls and flatmaps them together as a unit which either succeeds or fails. The resulting output is:

Calling server to authenticate
Busy: Please wait downloading
Downloading user info
Downloading user info     <---- unexpected second network call
Hide activity indicator
received userInfo: 'decoded user data'
Completed without errors.

The problem is downloadUserInfo((username:) appears to be called twice. If I only have one subscriber then downloadUserInfo((username:) is only called once. I have an ugly solution that wraps the flatMap in another Future but feel I missing something simple. Any thoughts?


回答1:


When you create the actual publisher with let future, append the .share operator, so that your two subscribers subscribe to a single split pipeline.

EDIT: As I've said in my comments, I'd make some other changes in your pipeline. Here's a suggested rewrite. Some of these changes are stylistic / cosmetic, as an illustration of how I write Combine code; you can take it or leave it. But other things are pretty much de rigueur. You need Deferred wrappers around your Futures to prevent premature networking (i.e. before the subscription happens). You need to store your pipeline or it will go out of existence before networking can start. I've also substituted a .handleEvents for your second subscriber, though if you use the above solution with .share you can still use a second subscriber if you really want to. This is a complete example; you can just copy and paste it right into a project.

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)
    }
}

Output:

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


来源:https://stackoverflow.com/questions/62469222/combine-future-block-called-multiple-times-when-using-flatmap-and-multiple-subsc

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!