How to Avoid Using Anypublisher/Erasetoanypublisher All Over the Place

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 composed lazy 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 LoginStates, 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



Leave a reply



Submit