使用Flatmap和多个订户时,将多次调用的Future块合并 [英] Combine Future block called multiple times when using Flatmap and multiple subscribers

查看:66
本文介绍了使用Flatmap和多个订户时,将多次调用的Future块合并的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我已经在我的应用程序中成功使用 BrightFutures ,主要用于异步网络请求.我决定现在该看看是否可以迁移到合并.但是,我发现的是,当我使用期货组合在一起时://developer.apple.com/documentation/combine/future/3333364-flatmap"rel =" nofollow noreferrer> flatMap 有两个订阅者,这是我的第二个

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

该代码模拟了进行两个网络调用,并将它们作为一个单元成功地进行了 flatmap 组合.结果输出为:

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

正在调用服务器进行身份验证
忙:请等待下载
下载用户信息
正在下载用户信息    < ----意外的第二次网络通话
隐藏活动指示器
received userInfo:解码后的用户数据"
已完成,没有错误.

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.

问题是 downloadUserInfo((username:)被调用两次.如果我只有一个订阅者,则 downloadUserInfo((username:)仅被调用一次.我有一个丑陋的解决方案,将 flatMap 包装在另一个 Future 中,但是感觉我缺少一些简单的东西.有什么想法吗?

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?

推荐答案

使用 let future 创建实际的发布者时,请附加 .share 运算符,以便两个订户订阅一个拆分管道.

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

编辑:正如我在评论中所说,我会在您的管道中进行其他一些更改.这是建议的重写.这些更改中有些是样式上的/修饰性的,以说明如何编写合并"代码;您可以选择接受或保留它.但是其他一些事情基本上都是严格"的.您需要在Future周围使用Deferred包装器以防止过早的联网(即在订阅发生之前).您需要存储您的管道,否则在开始网络连接之前该管道将不存在.我还用 .handleEvents 代替了第二个订阅者,但是如果您将上述解决方案与 .share 一起使用,您仍然可以使用第二个订阅者.这是一个完整的例子.您可以直接将其复制并粘贴到项目中.

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

输出:

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

这篇关于使用Flatmap和多个订户时,将多次调用的Future块合并的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆