Catch publisher terminates the publisher [duplicate] - swift

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 2 years ago.
Improve this question
Is there a way in the following Combine chain to handle all errors at one place ?. If I don't handle the error in flatMap, the $text publisher will never emit again. Thank you.
$text
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
.flatMap {
Repository().retrieve(query: $0)
.receive(on: DispatchQueue.main)
.catch { err -> AnyPublisher<[Beer], Never> in
self.serverError = err.displayValue
return Just([]).eraseToAnyPublisher()
}
}
.map { $0.map { ItemViewModel($0) } }
.receive(on: DispatchQueue.main)
.sink {[weak self] val in
self?.items = val
}.store(in: &cancellables)

Thank you #NevDev and #matt for pushing me in the correct direction.
lazy var publisher: AnyPublisher<Result<[ItemViewModel], RepositoryError>, Never> = {
$text
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
.flatMap {
Repository().retrieve(query: $0)
.map { Result.success($0.map { ItemViewModel($0) }) }
.catch { error in Just(Result.failure(error)) }
}
.eraseToAnyPublisher()
}()
init() {
publisher
.receive(on: DispatchQueue.main)
.sink {[weak self] value in
switch value {
case let .success(items): self?.items = items
case .failure(let error): self?.serverError = error.displayValue
}
}
.store(in: &cancellables)
}
}

Related

How can subscriber for CurrentValueSubject catch an error

I'm using CurrentValueSubject to populate a diffabledatasource table.
How can I catch the error?
var strings = CurrentValueSubject<[String], Error>([String]())
viewModel.strings
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: {
print("completion \($0)")
}, receiveValue: { [weak self] in
self?.applySnapshot()
})
.store(in: &cancellables)
Now receiveCompletion receives the error, but https://www.avanderlee.com/swift/combine-error-handling/ mentions using .catch but I can't see that this works in this case?
You can use .catch to essentially substitute a valid [String] for your Error (probably an empty array in this case):
.receive(on: DispatchQueue.main)
.catch { error -> Just<[String]> in
print(error)
return Just([])
}
.sink(receiveValue: { [weak self] _ in
self?.applySnapshot()
})
.store(in: &cancellables)
In this case, replaceError (which the article you linked to also mentioned), may be a simpler approach:
.receive(on: DispatchQueue.main)
.replaceError(with: [])
.sink(receiveValue: { [weak self] _ in
self?.applySnapshot()
})
.store(in: &cancellables)
Additional reading: https://www.donnywals.com/catch-vs-replaceerror-in-combine/

MongoDB Realm SwiftUI continue session / auto login

I'm using SwiftUI 2 with a MongoDB synced Realm (10.5.2) and I'm currently stuck with how to continue a session once a user reopens the app.
As an authentication I use email and password via a publisher and I know that I can check if the user is logged in with this function:
app.currentUser!.isLoggedIn
which also returns true for my user. However I don't know how I can get the publisher $0 from the
app.login(credentials: .emailPassword(email: username, password: password))
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: {
self.state.shouldIndicateActivity = false
switch $0 {
case .finished:
break
case .failure(let error):
self.state.error = error.localizedDescription
}
}, receiveValue: {
self.state.cardPublisher.send($0)
})
.store(in: &state.cancellables)
method where I use
self.state.cardPublisher.send($0)
to fetch my data. So my question is how do I get the $0 if the user restarts the app.
Sorry if that's a stupid question but I'm quite new to combine.
Any help is much appreciated :)
If it's helpful for you, I currently have these two publishers:
var cardPublisher = PassthroughSubject<RealmSwift.User, Error>()
let cardRealmPublisher = PassthroughSubject<Realm, Error>()
which are used like this:
cardPublisher
.receive(on: DispatchQueue.main)
.flatMap { user -> RealmPublishers.AsyncOpenPublisher in
self.shouldIndicateActivity = true
var realmConfig = user.configuration(partitionValue: "teamID=123")
realmConfig.objectTypes = [Card.self, Dog.self]
return Realm.asyncOpen(configuration: realmConfig)
}
.receive(on: DispatchQueue.main)
.map {
self.shouldIndicateActivity = false
return $0
}
.subscribe(cardRealmPublisher)
.store(in: &self.cancellables)
cardRealmPublisher
.sink(receiveCompletion: { result in
if case let .failure(error) = result {
self.error = "Failed to log in and open realm: \(error.localizedDescription)"
}
}, receiveValue: { realm in
self.cardRealm = realm
self.loadData()
})
.store(in: &cancellables)
I managed to solve it myself and it was actually quite easy. For anyone who might need it as well just use this method once the app loads:
if app.currentUser!.isLoggedIn {
app.currentUser.publisher
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: {
self.shouldIndicateActivity = false
switch $0 {
case .finished:
break
case .failure(let error):
self.error = error.localizedDescription
}
}, receiveValue: {
self.error = nil
self.cardPublisher.send($0)
})
.store(in: &self.cancellables)
} else {
// Regular Login
}

Publisher sink never runs completion

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.

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
})
}

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 }
}
}