How can subscriber for CurrentValueSubject catch an error - swift

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/

Related

Catch publisher terminates the publisher [duplicate]

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

Reusable publishers (subscriptions?) in Combine

I've got a case where I'm using a dataTaskPublisher and then chaining the output, as shown below. Now I'm implementing a background download, using URLSession's downloadTask(with:completionHandler) and I need to perform the exact same operations.
So everything in the code, below, from the decode(type:decoder) onwards is common between both situations. Is there some way I can take a Data object and let it be passed through that same set of steps without duplicating the code?
anyCancellable = session.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: TideLowWaterHeightPredictions.self, decoder: Self.decoder)
.map { $0.predictions }
.eraseToAnyPublisher()
.sink {
...
} receiveValue: { predictions in
...
}
You can wrap it up in an extension:
extension Publisher where Output == Data {
func gargoyle() -> AnyCancellable {
return self
.decode(type: TideLowWaterHeightPredictions.self, decoder: Self.decoder)
.map { $0.predictions }
.sink {
...
} receiveValue: { predictions in
...
}
}
}
And use it like this:
session
.dataTaskPublisher(for: url)
.map { $0.data }
.gargoyle()
.store(in: &tickets)
Or like this if you already have a Data:
Just(data)
.gargoyle()
.store(in: &tickets)

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
}

Debounce in Combine Stop Publishing

Here is my code. If i remove/commment out debounce, it works. Not sure why?
URLSession.shared.dataTaskPublisher(for: url)
.debounce(for: .milliseconds(500), scheduler: RunLoop.main)
.map {
$0.data
}
.decode(type: [Course].self, decoder: JSONDecoder())
.replaceError(with: [])
.receive(on: RunLoop.main)
.print()
.eraseToAnyPublisher()
.sink(receiveValue: { posts in
self.courses = posts
})
.store(in: &bag)

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