Is there a way to avoid using AnyPublisher/eraseToAnyPublisher all over the place?
UPDATE
The future is now! If you use Xcode 14.0 beta 2 or later, you can write your createPublisher
function like this, and it should back-deploy to all systems that support Combine:
func createPublisher() -> some Publisher<Int, Never> {
return Just(1)
}
Furthermore, you can use an opaque parameter type to make a function take a generic Publisher
argument with less syntax and without resorting to an existential or to AnyPublisher
:
func use(_ p: some Publisher<Int, Never>) { }
// means exactly the same as this:
func use<P: Publisher>(_ p: P) where P.Output == Int, P.Failure == Never { }
// and is generally more efficient than either of these:
func use(_ p: any Publisher<Int, Never>) { }
func use(_ p: AnyPublisher<Int, Never>) { }
ORIGINAL
Swift, as of this writing, doesn't have the feature you want. Joe Groff specifically describes what is missing in the section titled “Type-level abstraction is missing for function returns” of his “Improving the UI of generics” document:
However, it's common to want to abstract a return type chosen by the
implementation from the caller. For instance, a function may produce
a collection, but not want to reveal the details of exactly what kind
of collection it is. This may be because the implementer wants to
reserve the right to change the collection type in future versions, or
because the implementation uses composedlazy
transforms and doesn't
want to expose a long, brittle, confusing return type in its
interface. At first, one might try to use an existential in this
situation:func evenValues<C: Collection>(in collection: C) -> Collection where C.Element == Int {
return collection.lazy.filter { $0 % 2 == 0 }
}but Swift will tell you today that
Collection
can only be used as a
generic constraint, leading someone to naturally try this instead:func evenValues<C: Collection, Output: Collection>(in collection: C) -> Output
where C.Element == Int, Output.Element == Int
{
return collection.lazy.filter { $0 % 2 == 0 }
}but this doesn't work either, because as noted above, the
Output
generic argument is chosen by the caller—this function signature is
claiming to be able to return any kind of collection the caller asks
for, instead of one specific kind of collection used by the
implementation.
It's possible that the opaque return type syntax (some Publisher
) will be extended to support this use someday.
You have three options today. To understand them, let's consider a concrete example. Let's say you want to fetch a text list of integers, one per line, from a URL, and publish each integer as a separate output:
return dataTaskPublisher(for: url)
.mapError { $0 as Error }
.flatMap { data, response in
(response as? HTTPURLResponse)?.statusCode == 200
? Result.success(data).publisher
: Result.failure(URLError(.resourceUnavailable)).publisher
}
.compactMap { String(data: $0, encoding: .utf8) }
.map { data in
data
.split(separator: "\n")
.compactMap { Int($0) }
}
.flatMap { $0.publisher.mapError { $0 as Error } }
Option 1: Spell out the return type
You can use the full, complex return type. It looks like this:
extension URLSession {
func ints(from url: URL) -> Publishers.FlatMap<
Publishers.MapError<
Publishers.Sequence<[Int], Never>,
Error
>,
Publishers.CompactMap<
Publishers.FlatMap<
Result<Data, Error>.Publisher,
Publishers.MapError<
URLSession.DataTaskPublisher,
Error
>
>,
[Int]
>
> {
return dataTaskPublisher(for: url)
... blah blah blah ...
.flatMap { $0.publisher.mapError { $0 as Error } }
}
}
I didn't figure out the return type myself. I set the return type to Int
and then the compiler told me that Int
is not the correct return type, and the error message included the correct return type. This is not pretty, and if you change the implementation you'll have to figure out the new return type.
Option 2: Use AnyPublisher
Add .eraseToAnyPublisher()
to the end of the publisher:
extension URLSession {
func ints(from url: URL) -> AnyPublisher<Int, Error> {
return dataTaskPublisher(for: url)
... blah blah blah ...
.flatMap { $0.publisher.mapError { $0 as Error } }
.eraseToAnyPublisher()
}
}
This is the common and easy solution, and usually what you want. If you don't like spelling out eraseToAnyPublisher
, you can write your own Publisher
extension to do it with a shorter name, like this:
extension Publisher {
var typeErased: AnyPublisher<Output, Failure> { eraseToAnyPublisher() }
}
Option 3: Write your own Publisher
type
You can wrap up your publisher in its own type. Your type's receive(subscriber:)
constructs the “real” publisher and then passes the subscriber to it, like this:
extension URLSession {
func ints(from url: URL) -> IntListPublisher {
return .init(session: self, url: url)
}
}
struct IntListPublisher: Publisher {
typealias Output = Int
typealias Failure = Error
let session: URLSession
let url: URL
func receive<S: Subscriber>(subscriber: S) where
S.Failure == Self.Failure, S.Input == Self.Output
{
session.dataTaskPublisher(for: url)
.flatMap { $0.publisher.mapError { $0 as Error } }
... blah blah blah ...
.subscribe(subscriber)
}
}
Publisher vs AnyPublisher in Combine
Publisher
is a Protocol with associated types, while AnyPublisher
is a struct.
Try casting to Publisher
and you get an error
let x = Just(1) as Publisher
Protocol 'Publisher' can only be used as a generic constraint because it has Self or associated type requirements
This despite the fact that Just
is a Publisher
.
The Publisher
type can't be utilized in the same way as AnyPublisher
to achieve type erasure.
Where you could use Publisher
is when you define a function that has generics as part of the definition.
Most common reason to use AnyPublisher
:
Return an instance of a Publisher from a function.
Most common reason to use Publisher
:
Create a protocol extension to create a custom Combine operator. For example:
extension Publisher {
public func compactMapEach<T, U>(_ transform: @escaping (T) -> U?)
-> Publishers.Map<Self, [U]>
where Output == [T]
{
return map { $0.compactMap(transform) }
}
}
How to convert to/from AnyPublisher Void, Never and AnyPublisher Never, Never ?
I haven't found a more elegant way to do #2.
Here is a better #3 (Void -> Never
, complete immediately) solution:
let voidPublisher: AnyPublisher<Void, Never> = ...
let neverPublisher: AnyPublisher<Never, Never> = Publishers.Merge(Just<Void>(()), saveToDatabase)
.first()
.ignoreOutput()
.eraseToAnyPublisher()
And here is the timing Playground I used to ensure everything is functioning as expected:
import Combine
import Foundation
let delaySec: TimeInterval = 0.1
let halfDelaySec: TimeInterval = delaySec / 2
let halfDelayMicroSeconds: useconds_t = useconds_t(halfDelaySec * 1_000_000)
let sleepBufferMicroSeconds: useconds_t = useconds_t(0.01 * 1_000_000)
var cancellables = [AnyCancellable]()
var output: [String] = []
func performVoidAction(voidPublisher: AnyPublisher<Void, Never>) {
voidPublisher
.handleEvents(
receiveCompletion: { _ in output.append("performVoidAction - completion") },
receiveCancel: { output.append("performVoidAction - cancel") })
.sink(receiveValue: { output.append("performVoidAction - sink") })
.store(in: &cancellables)
}
func performNeverAction(neverPublisher: AnyPublisher<Never, Never>) {
neverPublisher
.handleEvents(
receiveCompletion: { _ in output.append("performNeverAction - completion") },
receiveCancel: { output.append("performNeverAction - cancel") })
.sink(receiveValue: { _ in output.append("performNeverAction - sink") })
.store(in: &cancellables)
}
func makeSaveToDatabasePublisher() -> AnyPublisher<Void, Never> {
Deferred { _saveToDatabase() }.eraseToAnyPublisher()
}
func makeSaveToUserDefaultsPublisher() -> AnyPublisher<Never, Never> {
Deferred { _saveToUserDefaults() }.eraseToAnyPublisher()
}
// --->(Void)|
// AnyPublisher<Void, Never> wraps an API that does something and finishes upon completion.
private func _saveToDatabase() -> AnyPublisher<Void, Never> {
return Future<Void, Never> { promise in
output.append("saving to database")
DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: DispatchTime.now() + delaySec) {
output.append("saved to database")
promise(.success(()))
}
}.eraseToAnyPublisher()
}
// |
// AnyPublisher<Never, Never> wraps an API that does something and completes immediately, it does not wait for completion.
private func _saveToUserDefaults() -> AnyPublisher<Never, Never> {
output.append("saved to user defaults")
return Empty<Never, Never>(completeImmediately: true)
.eraseToAnyPublisher()
}
func assert(_ value: Bool) -> String {
value ? "✅" : "❌"
}
// tests
assert(output.isEmpty)
var saveToDatabase = makeSaveToDatabasePublisher()
assert(output.isEmpty, "It should not fire the action yet.")
// verify database save, first time
performVoidAction(voidPublisher: saveToDatabase)
assert(!output.isEmpty && output.removeFirst() == "saving to database")
assert(output.isEmpty)
usleep(halfDelayMicroSeconds + sleepBufferMicroSeconds)
assert(output.isEmpty)
usleep(halfDelayMicroSeconds + sleepBufferMicroSeconds)
assert(!output.isEmpty && output.removeFirst() == "saved to database")
assert(!output.isEmpty && output.removeFirst() == "performVoidAction - sink")
assert(!output.isEmpty && output.removeFirst() == "performVoidAction - completion")
assert(output.isEmpty)
// verify database save, second time
performVoidAction(voidPublisher: saveToDatabase)
assert(!output.isEmpty && output.removeFirst() == "saving to database")
assert(output.isEmpty)
usleep(halfDelayMicroSeconds + sleepBufferMicroSeconds)
assert(output.isEmpty)
usleep(halfDelayMicroSeconds + sleepBufferMicroSeconds)
assert(!output.isEmpty && output.removeFirst() == "saved to database")
assert(!output.isEmpty && output.removeFirst() == "performVoidAction - sink")
assert(!output.isEmpty && output.removeFirst() == "performVoidAction - completion")
assert(output.isEmpty)
var saveToUserDefaults = makeSaveToUserDefaultsPublisher()
assert(output.isEmpty, "It should not fire the action yet.")
// verify user defaults save, first time
performNeverAction(neverPublisher: saveToUserDefaults)
assert(!output.isEmpty && output.removeFirst() == "saved to user defaults")
assert(!output.isEmpty && output.removeFirst() == "performNeverAction - completion")
assert(output.isEmpty) // 'perform never action' should never be output
// verify user defaults save, second time
performNeverAction(neverPublisher: saveToUserDefaults)
assert(!output.isEmpty && output.removeFirst() == "saved to user defaults")
assert(!output.isEmpty && output.removeFirst() == "performNeverAction - completion")
assert(output.isEmpty) // 'perform never action' should never be output
// MARK: - Problem: AnyPublisher<Never, Never> -> AnyPublisher<Void, Never>
// MARK: Solution 1
// `|` ➡️ `(Void)|`
performVoidAction(
voidPublisher: saveToUserDefaults
.map { _ in () }
.append(Just(()))
.eraseToAnyPublisher())
assert(output.removeFirst() == "saved to user defaults")
assert(!output.isEmpty && output.removeFirst() == "performVoidAction - sink")
assert(!output.isEmpty && output.removeFirst() == "performVoidAction - completion")
assert(output.isEmpty) // 'perform never action' should never be output"
// MARK: - Problem: AnyPublisher<Void, Never> -> AnyPublisher<Never, Never>
// MARK: Solution 2 (Wait)
// `--->(Void)|` ➡️ `--->|`
performNeverAction(
neverPublisher: saveToDatabase.ignoreOutput().eraseToAnyPublisher())
assert(!output.isEmpty && output.removeFirst() == "saving to database")
assert(output.isEmpty)
usleep(halfDelayMicroSeconds + sleepBufferMicroSeconds)
assert(output.isEmpty)
usleep(halfDelayMicroSeconds + sleepBufferMicroSeconds)
assert(!output.isEmpty && output.removeFirst() == "saved to database")
assert(!output.isEmpty && output.removeFirst() == "performNeverAction - completion")
assert(output.isEmpty)
// MARK: Solution 3 (No wait)
// `--->(Void)|` ➡️ `|`
performNeverAction(
neverPublisher: Publishers.Merge(Just<Void>(()), saveToDatabase)
.first()
.ignoreOutput()
.eraseToAnyPublisher())
assert(!output.isEmpty && output.removeFirst() == "performNeverAction - completion")
assert(!output.isEmpty && output.removeFirst() == "saving to database")
assert(output.isEmpty)
usleep(halfDelayMicroSeconds + sleepBufferMicroSeconds)
assert(output.isEmpty)
usleep(halfDelayMicroSeconds + sleepBufferMicroSeconds)
assert(!output.isEmpty && output.removeFirst() == "saved to database")
assert(output.isEmpty)
print("done")
Finally, here are the solutions as operators on Publisher:
extension Publisher where Output == Never {
func asVoid() -> AnyPublisher<Void, Failure> {
self
.map { _ in () }
.append(Just(()).setFailureType(to: Failure.self))
.eraseToAnyPublisher()
}
}
extension Publisher where Output == Void {
func asNever(completeImmediately: Bool) -> AnyPublisher<Never, Failure> {
if completeImmediately {
return Just<Void>(())
.setFailureType(to: Failure.self)
.merge(with: self)
.first()
.ignoreOutput()
.eraseToAnyPublisher()
} else {
return self
.ignoreOutput()
.eraseToAnyPublisher()
}
}
}
Map an AnyPublisher to another AnyPublisher
That error message doesn't make any sense, the real error was that isEmailValid
is Optional
, so you need to use optional chaining on it to be able to call map
.
emailColor = isEmailValid?
.map { self.emailColor(isValid: $0) }
.eraseToAnyPublisher()
Swift Combine - Accessing separate lists of publishers
Presuming that the question is how to turn an array of URLs into an array of what you get when you download and process the data from those URLs, the answer is: turn the array into a sequence publisher, process each URL by way of flatMap
, and collect
the result.
Here, for instance, is how to turn an array of URLs representing images into an array of the actual images (not identically what you're trying to do, but probably pretty close):
func publisherOfArrayOfImages(urls:[URL]) -> AnyPublisher<[UIImage],Error> {
urls.publisher
.flatMap { (url:URL) -> AnyPublisher<UIImage,Error> in
return URLSession.shared.dataTaskPublisher(for: url)
.compactMap { UIImage(data: $0.0) }
.mapError { $0 as Error }
.eraseToAnyPublisher()
}.collect().eraseToAnyPublisher()
}
And here's how to test it:
let urls = [
URL(string:"http://www.apeth.com/pep/moe.jpg")!,
URL(string:"http://www.apeth.com/pep/manny.jpg")!,
URL(string:"http://www.apeth.com/pep/jack.jpg")!,
]
let pub = publisherOfArrayOfImages(urls:urls)
pub.sink { print($0) }
receiveValue: { print($0) }
.store(in: &storage)
You'll see that what pops out the bottom of the pipeline is an array of three images, corresponding to the array of three URLs we started with.
(Note, please, that the order of the resulting array is random. We fetched the images asynchronously, so the results arrive back at our machine in whatever order they please. There are ways around that problem, but it is not what you asked about.)
Combine convert Just to AnyPublisher
Use .setFailureType
. The situation you are in is exactly what it is for:
Just([Int]())
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
iOS Swift Combine: Emit Publisher with single value
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()
}
Swift: Return publisher after another publisher is done
It is the caller of this function who will need to store the token (AnyCancellable) so there is no need for the sink (which is the reason for the error you are seeing). There is no need to wrap a publisher in a do-try-catch block.
This approach would compile:
func refreshToken() -> AnyPublisher<Bool, Never> {
let request = URLRequest(url: URL(string: "refresh_token_url")!)
return URLSession.shared
.dataTaskPublisher(for: request)
.map { _ in true }
.replaceError(with: false)
.eraseToAnyPublisher()
}
Though it does not make much sense for a method called refreshToken to return a Bool
. You should decode the response, extract the token and publish that (AnyPublisher<String, Error>
).
Combine: Transform an AnyPublisher into a another AnyPublisher
It looks like you want to use the catch
operator to convert an failure
completion into a LoginState
.
I guess you have an enum LoginRepositoryError
like this:
public enum LoginRepositoryError: Error {
case wrongPassword
case userNotExist
case emailValidationPending
}
And you have an enum LoginState
like this:
public enum LoginState {
case success
case invalidEmail
case invalidPassword
case wrongPassword
case error
case emailValidationPending
}
Since you want to convert your errors to LoginState
s, let's add an initializer to do that:
extension LoginState {
public init(_ repoError: LoginRepositoryError) {
switch repoError {
case .wrongPassword: self = .wrongPassword
case .userNotExist: self = .error
case .emailValidationPending: self = .emailValidationPending
}
}
}
And since the Output
of LoginUseCase.login
is Bool
, let's also add an initializer to convert a Bool
to a LoginState
:
extension LoginState {
public init(success: Bool) {
self = success ? .success : .error
}
}
Now we can use these initializers with the map
and catch
operators to create the AnyPublisher<LoginState, Never>
you're looking for:
extension LoginUseCase {
public func loginState(withEmail email: String?, password: String?) -> AnyPublisher<LoginState, Never> {
guard let email=email, !email.isEmpty else {
return Just<LoginState>(.invalidEmail).eraseToAnyPublisher()
}
guard let password=password, !password.isEmpty else {
return Just<LoginState>(.invalidPassword).eraseToAnyPublisher()
}
return login(email: email, password: password)
.map { LoginState(success: $0) }
.catch { Just(LoginState(error: $0)) }
.eraseToAnyPublisher()
}
}
Related Topics
How to Check If a String Contains Letters in Swift
How to Record Video in Realitykit
How to Zip More Than 4 Publishers
How to Add Caching to Asyncimage
Why Do Servicesubscribercellularproviders Return Nil? (In iOS 12)
How to Test That Statictexts Contains a String Using Xctest
Delegate Using Container View in Swift
How to Check If Annotation Is Clustered (Mkmarkerannotationview and Cluster)
How to Capture Local Variable Inside an Async Closure in Swift
Swift Combine Alternative to Rx Observable.Create
How to Avoid Using Anypublisher/Erasetoanypublisher All Over the Place
Swiftui MACos Nswindow Instance
Window Visible on All Spaces (Including Other Fullscreen Apps)
Scene Kit Performance with Cube Test
A Different Bridging Between Array and Dictionary
Xcode10 - Dyld: Library Not Loaded for Pod Installed in Framework
Guarantees About the Lifetime of a Reference in a Local Variable