Publisher sink never runs completion - swift

I am trying to merge two publishers but one of the completions are never run.
The following is how I create my two publishers and try to use .sink to observe when they complete. The featureFlagPublisher will finish as expected and print "featureFlagPublisher done", but the migratePublisher and the merged publishers will never complete.publisher.send(completion: .finished) is run but nothing happens.
private var cancellables = Set<AnyCancellable>()
func start() {
let featureFlagPublisher = self.startFeatureFlagging()
let migratePublisher = self.migrate()
migratePublisher.sink { _ in
print("migratePublisher done")
} receiveValue: { _ in }.store(in: &cancellables)
featureFlagPublisher.sink { _ in
print("featureFlagPublisher done")
} receiveValue: { _ in }.store(in: &cancellables)
migratePublisher.merge(with: featureFlagPublisher)
.sink { completion in
print("All Done")
} receiveValue: { _ in }
.store(in: &cancellables)
}
private func startFeatureFlagging() -> AnyPublisher<Bool, Never> {
let future = Future<Bool, Never> { promise in
FeatureFlaggingService.shared.start {
promise(.success(true))
}
}
return future.eraseToAnyPublisher()
}
private func migrate() -> AnyPublisher<Bool, Never> {
let future = Future<Bool, Never> { promise in
FavoriteMigrationsAPI.shared.get { result in
switch result {
case .success(let favoriteIDs):
promise(.success(true))
...
}
}
return future.eraseToAnyPublisher()
}

It turned out that the issue was that the controller was deinitialized early and therefore the promise could not respond to the observer. So it had nothing to do with Combine or the code I posted, but with the way I initialized the controller.

Related

Sink does not receive values when connecting to finished a Publisher

I'm struggling to understand why no values are received to sink in the underlying code.
func somePublisher() -> AnyPublisher<Bool, Never> {
let subject = PassthroughSubject<Bool, Never>()
subject.send(true)
subject.send(completion: .finished)
return subject.eraseToAnyPublisher()
}
somePublisher()
.first()
.sink { _ in
print("Completed")
} receiveValue: {
print("Received \($0)")
}
.store(in: &sinks)
Output:
Completed
It looks like values are not received by the publishers down the stream if it was finished before it was connected. Is that right?
How could I fix that if my publisher can finish synchronously?
PassthroughSubject receives a value and passes it along, it doesn't store the value so if you subscribe to it after the value passed through it, you won't receive it.
You can use CurrentValueSubject, which will store the latest value and resend it whenever somebody subscribes to it.
All of this is moot if you send completion: .finished though. A completed publisher won't send any values to a subscriber because it's completed.
This is your fixed code:
func somePublisher() -> AnyPublisher<Bool, Never> {
let subject = CurrentValueSubject<Bool, Never>(true)
return subject.eraseToAnyPublisher()
}
var bag: Set<AnyCancellable> = []
somePublisher()
.first()
.sink { _ in
print("Completed")
} receiveValue: {
print("Received \($0)")
}
.store(in: &bag)
I was struggling with this same problem and I made use of RunLoop.current to overcome it...
func somePublisher() -> AnyPublisher<Bool, Never> {
let subject = PassthroughSubject<Bool, Never>()
RunLoop.current.perform {
// Delay send/completion until next run loop to give subscribers a chance to subscribe
subject.send(true)
subject.send(completion: .finished)
}
return subject.eraseToAnyPublisher()
}
somePublisher()
.first()
.sink { _ in
print("Completed")
} receiveValue: {
print("Received \($0)")
}
.store(in: &sinks)

.sink is not returning the promise values from a Future Publisher

I have this code in lrvViewModel.swift
func getVerificationID (phoneNumber: String) -> Future<String?, Error> {
return Future<String?, Error> { promise in
PhoneAuthProvider.provider().verifyPhoneNumber(phoneNumber, uiDelegate: nil) { (verificationID, error) in
if let e = error {
promise(.failure(e))
return
}
print("verification worked")
self.defaults.set(verificationID, forKey: "authVerificationID")
return promise(.success(verificationID))
}
}
}
and then i call and subscribe to the Publisher in another file like this
let _ = lrvViewModel.getVerificationID(phoneNumber: (lrvViewController?.textField.text)!)
.sink(receiveCompletion: {
print("Error worked")
// does a bunch of stuff
}, receiveValue: {
print("completion worked")
// does a bunch of stuff
})
I don't get any buildtime errors, but whenever I run the app the GetVerificationID function runs fine (prints "Verification worked"), but the code within .sink doesn't run (I don't get any print statements). What's going on?
Edit:
My solution was to give up on combine and go back to RXSwift where the code is simply:
var validateObs = PublishSubject<Any>()
func getVerificationID (phoneNumber: String) {
PhoneAuthProvider.provider().verifyPhoneNumber(phoneNumber, uiDelegate: nil) { (verificationID, error) in
if let e = error {
print("v error")
self.validateObs.onError(e)
return
}
self.defaults.set(verificationID, forKey: "authVerificationID")
self.validateObs.onCompleted()
}
}
and
lrvViewModel.getVerificationID(phoneNumber: (lrvViewController?.textField.text)!)
let _ = lrvViewModel.validateObs.subscribe(onError: {
let e = $0
print(e.localizedDescription)
// do stuff
}, onCompleted: {
// do stuff
})
Was hoping to not rely on a dependency but RxSwift implementation was much easier.
If someone knows the solution to the Combine Future problem please post! I would still like to know wtf is happening. It's very possible (and likely) I'm just using combine wrong.
Edit 2:
Was using combine wrong. I can duplicate the code I had with RXSwift like this:
let verifyPub = PassthroughSubject<Any, Error>()
func getVerificationID (phoneNumber: String) {
PhoneAuthProvider.provider().verifyPhoneNumber(phoneNumber, uiDelegate: nil) { (verificationID, error) in
if let e = error {
self.verifyPub.send(completion: .failure(e))
return
}
print("verification worked")
self.defaults.set(verificationID, forKey: "authVerificationID")
self.verifyPub.send(completion: .finished)
}
}
and
let subs = Set<AnyCancellable>()
let pub = lrvViewModel.verifyPub
.sink(receiveCompletion: { completion in
if case let .failure(error) = completion {
print("Error worked")
// do stuff
} else {
print("completion worked")
// do stuff
}
}, receiveValue: { _ in
print("this will never happen")
}).store(in: &subs)
I didnt' understand that in combine there are only two results to a sink, a completion or a value, and that completion is split up into multiple cases. Whereas in RxSwift you have OnNext, OnComplete, and OnError.
Shoutout to the book on Combine from raywanderlich.com. Good stuff.
What's going on is that your .sink is not followed by a .store command, so the pipeline goes out of existence before any value has a chance to come down it.
Your assignment of the pipeline to the empty _ effectively masks the problem. The compiler tried to warn you, and you shut it down.

Chaining Request in Combine

I'm struggling with combine two requests. I need id from the first one, then start the second one with this first id from the received list. I can't find a nice solution to do this with Swift Combine.
my first request looks like that CarSerview.shared.getCategories() -> AnyPublisher<[Category], CarError> :
private func getCategories() {
CarSerview.shared.getCategories()
.receive(on: DispatchQueue.main)
.map { car in
return car.id
}
.replaceError(with: [])
.assign(to: \.carsIds, on: self)
.store(in: &cancellable)
}
and the second one looks like that CarSerview.shared.getCar() -> AnyPublisher<Car, CarError>:
private func getCar(_ category: CategoryObject) {
SPService.shared.getExcursions(category)
.receive(on: DispatchQueue.main)
.map { car in
return car.cars.compactMap { $0.name }
}
.sink(receiveCompletion: { error in
print(error)
}, receiveValue: { [weak self] result in
self?.cars = result
})
}
how can in chain this two request in one?
Cannot test but the idea is to use .map + .switchToLatest, so can be like as follows
private func getCars() {
CarSerview.shared.getCategories()
.map { car in
return SPService.shared.getExcursions(car.id)
}
.switchToLatest()
.receive(on: DispatchQueue.main)
.map { car in
return car.cars.compactMap { $0.name }
}
.sink(receiveCompletion: { error in
print(error)
}, receiveValue: { [weak self] result in
self?.cars = result
})
}

Translating async method into Combine

I'm trying to wrap my head around Combine.
Here's a method I want to translate into Combine, so that it would return AnyPublisher.
func getToken(completion: #escaping (Result<String, Error>) -> Void) {
dispatchQueue.async {
do {
if let localEncryptedToken = try self.readTokenFromKeychain() {
let decryptedToken = try self.tokenCryptoHelper.decrypt(encryptedToken: localEncryptedToken)
DispatchQueue.main.async {
completion(.success(decryptedToken))
}
} else {
self.fetchToken(completion: completion)
}
} catch {
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
}
The whole thing executes on a separate dispatch queue because reading from Keychain and decryption can be slow.
My first attempt to embrace Combine
func getToken() -> AnyPublisher<String, Error> {
do {
if let localEncryptedToken = try readTokenFromKeychain() {
let decryptedToken = try tokenCryptoHelper.decrypt(encryptedToken: localEncryptedToken)
return Result.success(decryptedToken).publisher.eraseToAnyPublisher()
} else {
return fetchToken() // also rewritten to return AnyPublisher<String, Error>
}
} catch {
return Result.failure(error).publisher.eraseToAnyPublisher()
}
}
But how would I move reading from Keychain and decryption onto separate queue? It probably should look something like
func getToken() -> AnyPublisher<String, Error> {
return Future<String, Error> { promise in
self.dispatchQueue.async {
do {
if let localEncryptedToken = try self.readTokenFromKeychain() {
let decryptedToken = try self.tokenCryptoHelper.decrypt(encryptedToken: localEncryptedToken)
promise(.success(decryptedToken))
} else {
// should I fetchToken().sink here?
}
} catch {
promise(.failure(error))
}
}
}.eraseToAnyPublisher()
}
How would I return a publisher from my private method call? (see comment in code)
Are there any prettier solutions?
Assuming you’ve refactored readTokenFromKeyChain, decrypt, and fetchToken to return AnyPublisher<String, Error> themselves, you can then do:
func getToken() -> AnyPublisher<String, Error> {
readTokenFromKeyChain()
.flatMap { self.tokenCryptoHelper.decrypt(encryptedToken: $0) }
.catch { _ in self.fetchToken() }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
That will read the keychain, if it succeeded, decrypt it, and if it didn’t succeed, it will call fetchToken. And having done all of that, it will make sure the final result is delivered on the main queue.
I think that’s the right general pattern. Now, let's talk about that dispatchQueue: Frankly, I’m not sure I’m seeing anything here that warrants running on a background thread, but let’s imagine you wanted to kick this off in a background queue, then, you readTokenFromKeyChain might dispatch that to a background queue:
func readTokenFromKeyChain() -> AnyPublisher<String, Error> {
dispatchQueue.publisher { promise in
let query: [CFString: Any] = [
kSecReturnData: true,
kSecClass: kSecClassGenericPassword,
kSecAttrAccount: "token",
kSecAttrService: Bundle.main.bundleIdentifier!]
var extractedData: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &extractedData)
if
status == errSecSuccess,
let retrievedData = extractedData as? Data,
let string = String(data: retrievedData, encoding: .utf8)
{
promise(.success(string))
} else {
promise(.failure(TokenError.failure))
}
}
}
By the way, that’s using a simple little method, publisher that I added to DispatchQueue:
extension DispatchQueue {
/// Dispatch block asynchronously
/// - Parameter block: Block
func publisher<Output, Failure: Error>(_ block: #escaping (Future<Output, Failure>.Promise) -> Void) -> AnyPublisher<Output, Failure> {
Future<Output, Failure> { promise in
self.async { block(promise) }
}.eraseToAnyPublisher()
}
}
For the sake of completeness, this is a sample fetchToken implementation:
func fetchToken() -> AnyPublisher<String, Error> {
let request = ...
return URLSession.shared
.dataTaskPublisher(for: request)
.map { $0.data }
.decode(type: ResponseObject.self, decoder: JSONDecoder())
.map { $0.payload.token }
.eraseToAnyPublisher()
}
I think I could find a solution
private func readTokenFromKeychain() -> AnyPublisher<String?, Error> {
...
}
func getToken() -> AnyPublisher<String, Error> {
return readTokenFromKeychain()
.flatMap { localEncryptedToken -> AnyPublisher<String, Error> in
if let localEncryptedToken = localEncryptedToken {
return Result.success(localEncryptedToken).publisher.eraseToAnyPublisher()
} else {
return self.fetchToken()
}
}
.flatMap {
return self.tokenCryptoHelper.decrypt(encryptedToken: $0)
}
.subscribe(on: dispatchQueue)
.eraseToAnyPublisher()
}
But I had to make functions I call within getToken() return publishers too to Combine them well.
There probably should be error handling somewhere but this is the next thing for me to learn.

iOS13's Combine streams don't flow after operator using schedulers

iOS13's Combine streams of publishers don't appear to be flowing after operator using schedulers.
Here's my code:
import Foundation
import Combine
struct MyPublisher: Publisher {
typealias Output = Int
typealias Failure = Error
func receive<S>(subscriber: S) where S : Subscriber,
Failure == S.Failure,
Output == S.Input {
subscriber.receive(1)
print("called 1")
subscriber.receive(2)
print("called 2")
subscriber.receive(completion: .finished)
print("called finish")
}
}
MyPublisher()
// .receive(on: RunLoop.main) // If this line removed, it will be fine.
// .throttle(for: .milliseconds(1000), scheduler: RunLoop.main, latest: false)) // If this line removed, it will be fine.
// .debounce(for: .milliseconds(1000), scheduler: RunLoop.main)) // If this line removed, it will be fine.
// .delay(for: .milliseconds(1000), scheduler: DispatchQueue.main)) // If this line removed, it will be fine.
.print()
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("finished")
case .failure(let error):
print("error:\(error)")
}
}, receiveValue: { num in
print("\(num)")
})
I expected output to be
1
2
finished
but the actual output is nothing.
If I don't use receive or throttle or debounce or delay. The output will be fine.
Is it a bug or something wrong with my code?
I tried with Playground (Xcode 11 beta3).
Subscription:
I'm unsure of why it works in the case of a single thread but you should make sure to call received(subscription:) on the subscriber. If you do not need to handle the subscribers demands you can use Subscribers.empty:
struct MyPublisher: Publisher {
typealias Output = Int
typealias Failure = Never
func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
subscriber.receive(subscription: Subscriptions.empty)
_ = subscriber.receive(1)
Swift.print("called 1")
_ = subscriber.receive(2)
Swift.print("called 2")
_ = subscriber.receive(completion: .finished)
Swift.print("called finish")
}
}
AnyCancellable:
You should notice a warning:
Result of call to 'sink(receiveCompletion:receiveValue:)' is unused
That should appear since sink returns an AnyCancellable:
func sink(receiveCompletion: #escaping ((Subscribers.Completion<Self.Failure>) -> Void), receiveValue: #escaping ((Self.Output) -> Void)) -> AnyCancellable
Anything that returns an AnyCancellable will get canceled as soon as the AnyCancellable is deallocated.
My speculation is that if you are putting this on another thread, then when the end of the calling method is reached the cancellable will deallocate before the subscription is received. But when received on the current thread it seems to be executing just in time for the subscription and output to show. Most likely the cancellable is being deallocated when the current thread exits.
Use Cancellable
For example :
class ImageLoader: ObservableObject {
#Published var image: UIImage?
private var cancellable: AnyCancellable?
func fetchImages() {
guard let urlString = urlString,
let url = URL(string: urlString) else { return }
cancellable = URLSession.shared.dataTaskPublisher(for: url)
.map { UIImage(data: $0.data) }
.replaceError(with: nil)
.receive(on: DispatchQueue.main)
.sink { [weak self] in self?.image = $0 }
}
}
Use the underscore
You can pass the underscore to pass the warning. I've used the example from Naishta's answer.
For example
class ImageLoader: ObservableObject {
#Published var image: UIImage?
func fetchImages() {
guard let urlString = urlString,
let url = URL(string: urlString) else { return }
_ = URLSession.shared.dataTaskPublisher(for: url)
.map { UIImage(data: $0.data) }
.replaceError(with: nil)
.receive(on: DispatchQueue.main)
.sink { [weak self] in self?.image = $0 }
}
}