iOS Swift Combine: Emit Publisher with single value

iOS Swift Combine: Emit Publisher with single value

我正在使用 Combine,我多次遇到需要发出具有单一值的 Publisher 的情况。

例如,当我使用平面地图并且我必须 return 一个具有单个值作为错误或单个对象的 Publisher 时,我使用了这段代码,它工作得很好:

return AnyPublisher<Data, StoreError>.init(
           Result<Data, StoreError>.Publisher(.cantDownloadProfileImage)
        )

这将创建类型为 <Data, StoreError> 的 AnyPublisher 并发出错误,在本例中为:.cantDownloadProfileImage

这里有一个完整的例子,说明如何使用这段代码。

func downloadUserProfilePhoto(user: User) -> AnyPublisher<UIImage?, StoreError> {
        guard let urlString = user.imageURL,
            let url = URL(string: urlString)
            else {
                return AnyPublisher<UIImage?, StoreError>
                    .init(Result<UIImage?, StoreError>
                        .Publisher(nil))
        }
        return NetworkService.getData(url: url)
            .catch({ (_) -> AnyPublisher<Data, StoreError> in
                return AnyPublisher<Data, StoreError>
                    .init(Result<Data, StoreError>
                        .Publisher(.cantDownloadProfileImage))
            })
            .flatMap { data -> AnyPublisher<UIImage?, StoreError> in
                guard let image = UIImage(data: data) else {
                    return AnyPublisher<UIImage?, StoreError>
                        .init(Result<UIImage?, StoreError>.Publisher(.cantDownloadProfileImage))
                }
                return AnyPublisher<UIImage?, StoreError>
                    .init(Result<UIImage?, StoreError>.Publisher(image))
        }
        .eraseToAnyPublisher()
    }

有没有一种更简单、更快捷的方法来创建一个内部只有一个值的 AnyPublisher?

我想我应该以某种方式使用 Just() 对象,但我不明白如何使用,因为现阶段的文档非常不清楚。

我们可以做的主要事情是在所有地方使用 .eraseToAnyPublisher() 而不是 AnyPublisher.init 来加强您的代码。这是我对您的代码唯一挑剔的地方。使用 AnyPublisher.init 不是惯用的,而且会造成混淆,因为它添加了一层额外的嵌套括号。

除此之外,我们还可以做一些事情。请注意,您写的内容(除了没有正确使用 .eraseToAnyPublisher() 之外)很好,尤其是对于早期版本。以下建议是我在 得到一个更详细的编译器版本后我会做的事情。

我们可以使用OptionalflatMap方法将user.imageURL转化为URL。我们还可以让 Swift 推断 Result 类型参数,因为我们在 return 语句中使用 Result 所以 Swift 知道预期的类型。因此:

func downloadUserProfilePhoto(user: User) -> AnyPublisher<UIImage?, StoreError> {
    guard let url = user.imageURL.flatMap({ URL(string: [=10=]) }) else {
        return Result.Publisher(nil).eraseToAnyPublisher()
    }

我们可以用mapError代替catchcatch 运算符是通用的:只要 Success 类型匹配,您就可以从中 return 任何 Publisher。但在你的情况下,你只是丢弃传入的失败并 returning 一个持续的失败,所以 mapError 更简单:

    return NetworkService.getData(url: url)
        .mapError { _ in .cantDownloadProfileImage }

我们可以在这里使用点快捷方式,因为这是 return 语句的一部分。因为它是 return 语句的一部分,所以 Swift 推断出 mapError 转换必须 return 一个 StoreError。所以它知道去哪里寻找 .cantDownloadProfileImage.

的含义

flatMap 运算符需要转换为 return 固定的 Publisher 类型,但它不必 return AnyPublisher。因为您在 flatMap 之外的所有路径中都使用 Result<UIImage?, StoreError>.Publisher,所以不需要将它们包装在 AnyPublisher 中。事实上,如果我们将转换更改为使用 Optionalmap 方法而不是 guard,我们根本不需要指定转换的 return 类型声明:

        .flatMap({ data in
            UIImage(data: data)
                .map { Result.Publisher([=12=]) }
                ?? Result.Publisher(.cantDownloadProfileImage)
        })
        .eraseToAnyPublisher()

同样,这是 return 声明的一部分。这意味着 Swift 可以为我们推导出 Result.PublisherOutputFailure 类型。

另请注意,我在转换闭包周围放置了圆括号,因为这样做会使 Xcode 正确缩进右大括号,以便与 .flatMap 对齐。如果您不将闭包括在括号中,则 Xcode 会将右括号与 return 关键字对齐。呃.

全部都在这里:

func downloadUserProfilePhoto(user: User) -> AnyPublisher<UIImage?, StoreError> {
    guard let url = user.imageURL.flatMap({ URL(string: [=13=]) }) else {
        return Result.Publisher(nil).eraseToAnyPublisher()
    }
    return NetworkService.getData(url: url)
        .mapError { _ in .cantDownloadProfileImage }
        .flatMap({ data in
            UIImage(data: data)
                .map { Result.Publisher([=13=]) }
                ?? Result.Publisher(.cantDownloadProfileImage)
        })
        .eraseToAnyPublisher()
}
import Foundation
import Combine

enum AnyError<O>: Error {
    case forcedError(O)
}

extension Publisher where Failure == Never {
    public var limitedToSingleResponse: AnyPublisher<Output, Never> {
        self.tryMap {
            throw AnyError.forcedError([=10=])
        }.catch { error -> AnyPublisher<Output, Never> in
            guard let anyError = error as? AnyError<Output> else {
                preconditionFailure("only these errors are expected")
            }
            switch anyError {
            case let .forcedError(publishedValue):
                return Just(publishedValue).eraseToAnyPublisher()
            }
        }.eraseToAnyPublisher()
    }
}

let unendingPublisher = PassthroughSubject<Int, Never>()

let singleResultPublisher = unendingPublisher.limitedToSingleResponse

let subscription = singleResultPublisher.sink(receiveCompletion: { _ in
    print("subscription ended")
}, receiveValue: {
    print([=10=])
})

unendingPublisher.send(5)




在上面的代码片段中,我正在将一个 passthroughsubject 发布者转换成一个在发送第一个值后停止的东西。基于WWDCsession关于结合https://developer.apple.com/videos/play/wwdc2019/721/介绍的精华片段在这里

我们基本上强制在 tryMap 中抛出一个错误,然后使用 Just 解析发布者捕获它,正如问题所述,在订阅第一个值后将完成。

理想情况下,订阅者可以更好地表明需求。

另一个稍微古怪的替代方法是在发布者上使用 first 运算符

let subscription_with_first = unendingPublisher.first().sink(receiveCompletion: { _ in
    print("subscription with first ended")
}, receiveValue: {
    print([=11=])
})