Map, catch error and assign with Combine in SwiftUI - swift

I learn to use Combine and I wrote this code:
serviceAgent.podcasts()
.sink(
receiveCompletion: { cpl in
switch cpl {
case .failure(let error):
print(error.localizedDescription)
case .finished:
return
}
},
receiveValue: { [weak self] (model) in
self?.items = model.map({ podcast in
let item = PodcastCardItemViewModel(podcast)
item.clickPublisher.sink { value in
self?.clickPublisher.send(value)
}.store(in: &(self!.cancelBag))
return item
})
}
).store(in: &cancelBag)
I'm pretty sure it's possible to simplify this code with map, catch and assign but I don't know how.
Any ideas?
Thanks

Advanced Combine
This is some of the hard stuff with combine. But it is doable. Without seeing the rest of your code, I don't know if this will work 100% correctly. But I was able to get this to compile on my machine.
serviceAgent
.podcasts()
.map(PodcastCardItemViewModel.init)
.flatMap { $0.clickPublisher }
.collect()
.eraseToAnyPublisher()
.sink(
receiveCompletion: { cpl in
switch cpl {
case .failure(let error):
print(error.localizedDescription)
case .finished:
return
}
},
receiveValue: { [weak self] value in
self?.clickPublisher.send(value)
}
).store(in: &cancelBag)

Related

Apple Combine: How to tell that a subscriber stopped listening to a publisher [duplicate]

I have this simple subscription where my subject is eminting strings. Just for curiosity I would like to know if my subscription is cancelled.
Afaik a pipeline that has been cancelled will not send any completions.
Are there some ways do achieve this?
The use case would be that I can cancel all subscriptions and receive a completion on this. Where I can clean up stuff a reflect this probably.
PlaygroundPage.current.needsIndefiniteExecution = true
var disposeBag: Set<AnyCancellable> = .init()
let subject = PassthroughSubject<String, Never>()
subject.sink(receiveCompletion: { completion in
switch completion {
case .failure(let error):
print("Failed with: \(error.localizedDescription)")
case .finished:
print("Finished")
}
}) { string in
print(string)
}.store(in: &disposeBag)
subject.send("A")
disposeBag.map { $0.cancel() }
subject.send("B")
It is possible via handling events
subject
.handleEvents(receiveCancel: {
print(">> cancelled") // << here !!
})
.sink(receiveCompletion: { completion in
switch completion {
case .failure(let error):
print("Failed with: \(error.localizedDescription)")
case .finished:
print("Finished")
}
}) { string in
print(string)
}.store(in: &disposeBag)

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
}

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