Reputation: 7668
I'm using Combine and it happens to me many times that I have the need to emit Publishers with single values.
For example when I use flat map and I have to return a Publisher with a single value as an error or a single object I use this code, and it works very well:
return AnyPublisher<Data, StoreError>.init(
Result<Data, StoreError>.Publisher(.cantDownloadProfileImage)
)
This creates an AnyPublisher of type <Data, StoreError>
and emits an error, in this case: .cantDownloadProfileImage
Here a full example how may usages of this chunk of code.
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()
}
Is there an easier and shorter way to create an AnyPublisher with a single value inside?
I think I should use the Just()
object in somehow, but I can't understand how, because the documentation at this stage is very unclear.
Upvotes: 1
Views: 18176
Reputation: 51
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($0)
}.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($0)
})
unendingPublisher.send(5)
In the snippet above I am converting a passthroughsubject publisher which can send a stream of values into something that stops after sending the first value. The essence of the snippet in based on the WWDC session about introduction to combine https://developer.apple.com/videos/play/wwdc2019/721/ here.
We are esentially force throwing an error in tryMap and then catching it with a resolving publisher using Just which as the question states will finish after the first value is subscribed to.
Ideally the demand is better indicated by the subscriber.
Another slightly more quirky alternative is to use the first operator on a publisher
let subscription_with_first = unendingPublisher.first().sink(receiveCompletion: { _ in
print("subscription with first ended")
}, receiveValue: {
print($0)
})
Upvotes: 0
Reputation: 386068
The main thing we can do to tighten up your code is to use .eraseToAnyPublisher()
instead of AnyPublisher.init
everywhere. This is the only real nitpick I have with your code. Using AnyPublisher.init
is not idiomatic, and is confusing because it adds an extra layer of nested parentheses.
Aside from that, we can do a few more things. Note that what you wrote (aside from not using .eraseToAnyPublisher()
appropriately) is fine, especially for an early version. The following suggestions are things I would do after I have gotten a more verbose version past the compiler.
We can use Optional
's flatMap
method to transform user.imageURL
into a URL. We can also let Swift infer the Result
type parameters, because we're using Result
in a return
statement so Swift knows the expected types. Hence:
func downloadUserProfilePhoto(user: User) -> AnyPublisher<UIImage?, StoreError> {
guard let url = user.imageURL.flatMap({ URL(string: $0) }) else {
return Result.Publisher(nil).eraseToAnyPublisher()
}
We can use mapError
instead of catch
. The catch
operator is general: you can return any Publisher
from it as long as the Success
type matches. But in your case, you're just discarding the incoming failure and returning a constant failure, so mapError
is simpler:
return NetworkService.getData(url: url)
.mapError { _ in .cantDownloadProfileImage }
We can use the dot shortcut here because this is part of the return
statement. Because it's part of the return
statement, Swift deduces that the mapError
transform must return a StoreError
. So it knows where to look for the meaning of .cantDownloadProfileImage
.
The flatMap
operator requires the transform to return a fixed Publisher
type, but it doesn't have to return AnyPublisher
. Because you are using Result<UIImage?, StoreError>.Publisher
in all paths out of flatMap
, you don't need to wrap them in AnyPublisher
. In fact, we don't need to specify the return type of the transform at all if we change the transform to use Optional
's map
method instead of a guard
statement:
.flatMap({ data in
UIImage(data: data)
.map { Result.Publisher($0) }
?? Result.Publisher(.cantDownloadProfileImage)
})
.eraseToAnyPublisher()
Again, this is part of the return
statement. That means Swift can deduce the Output
and Failure
types of the Result.Publisher
for us.
Also note that I put parentheses around the transform closure because doing so makes Xcode indent the close brace properly, to line up with .flatMap
. If you don't wrap the closure in parens, Xcode lines up the close brace with the return
keyword instead. Ugh.
Here it is all together:
func downloadUserProfilePhoto(user: User) -> AnyPublisher<UIImage?, StoreError> {
guard let url = user.imageURL.flatMap({ URL(string: $0) }) else {
return Result.Publisher(nil).eraseToAnyPublisher()
}
return NetworkService.getData(url: url)
.mapError { _ in .cantDownloadProfileImage }
.flatMap({ data in
UIImage(data: data)
.map { Result.Publisher($0) }
?? Result.Publisher(.cantDownloadProfileImage)
})
.eraseToAnyPublisher()
}
Upvotes: 7