I have several completable rx subscriptions in a stream like this:
viewModel?.setupDoorMode().subscribe(onNext: { shouldConnectToDoor in
if shouldConnectToDoor {
self.viewModel?.connectAndOpenDoor().subscribe(onCompleted: {
self.viewModel?.openOrCloseDoor().subscribe().disposed(by: self.disposeBag)
}).disposed(by: self.disposeBag)
} else {
self.viewModel?.openOrCloseDoor().subscribe().disposed(by: self.disposeBag)
}
}).disposed(by: disposeBag)
I have the feeling that this can be done in a nicer way, like flatMaping the streams into oneanother. But when I try using flatMap I get the error Type '()' cannot conform to 'ObservableConvertibleType'; only struct/enum/class types can conform to protocols. I'm not too familiar with rx to understand that message. Anyway, is there a way to create a more smooth looking stream rather than three subscriptions in a row?
You’re definitely on the right track thinking about flatMap to compose your observables. The thing to note here is that calling .subscribe() returns a Disposable type and calling .disposed(by:) on that disposable returns Void aka () type. You can't compose Voids. You have compose the observables, and THEN you subscribe to the result.
guard let viewModel = viewModel else { return }
// Combine the observables
let doorActionObservable = viewModel
.setupDoorMode()
.flatMap { /* mind the retain cycle */ shouldConnectToDoor in
if shouldConnectToDoor {
return self.viewModel.connectAndOpenDoor()
} else {
return self.viewModel.openOrCloseDoor()
}
}
// Subscribe to the the resulting observable
doorActionObservable
.subscribe()
.disposed(by: disposeBag)
Related
I have an rx observable returning a Bool that I need to bind to a SegmentedControl's selectedSegmentIndex. The function looks like this:
func getLockPower() -> Observable<Bool> {
do {
return try doorModeService.getDoorLockPower().map { $0.rawValue == 1 }.asObservable()
} catch {
return .error(error)
}
}
And I have tried binding it to my SegementedControl like this:
lockViewModel.getLockPower()
.bind(to: lockInstalledSegmentController.rx.selectedSegmentIndex)
.disposed(by: disposeBag)
But I get the error:
No exact matches in call to instance method 'bind'
I don't think I get the syntax, and I haven't been able to find any source on how it should look like. Any suggestions?
Alright, I worked it out. The index obviously need an Int, so instead of mapping to Bool I mapped to an Int and it worked.
just trying to implement SwiftUI and Combine in my new project.
But stuck in this:
func task() -> AnyPublisher<Int, Error> {
return AnyPublisher { subscriber in
subscriber.receive(Int(arc4random()))
subscriber.receive(completion: .finished)
}
}
This produces the following compiler error:
Type '(_) -> ()' does not conform to protocol 'Publisher'
Why?
Update
Actually Random here is just as an example. The real code will look like this:
func task() -> AnyPublisher<SomeCodableModel, Error> {
return AnyPublisher { subscriber in
BackendCall.MakeApiCallWithCompletionHandler { response, error in
if let error == error {
subscriber.receive(.failure(error))
} else {
subscriber.receive(.success(response.data.filter))
subscriber.receive(.finished)
}
}
}
}
Unfortunately, I don't have access to BackendCall API since it is private.
It's kind of pseudocode but, it pretty close to the real one.
You cannot initialise an AnyPublisher with a closure accepting a Subscriber. You can only initialise an AnyPublisher from a Publisher. If you want to create a custom Publisher that emits a single random Int as soon as it receives a subscriber and then completes, you can create a custom type conforming to Publisher and in the required method, receive(subscriber:), do exactly what you were doing in your closure.
struct RandomNumberPublisher: Publisher {
typealias Output = Int
typealias Failure = Never
func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
subscriber.receive(Int.random(in: 0...Int.max))
subscriber.receive(completion: .finished)
}
}
Then in your task method, you simply need to create a RandomNumberPublisher and then type erase it.
func task() -> AnyPublisher<Int, Never> {
return RandomNumberPublisher().eraseToAnyPublisher()
}
If all you want is a single random value, use Just
fun task() -> AnyPublisher<Int, Never> {
return Just(Int.random(in: 0...Int.max)).eraseToAnyPublisher()
}
Sidenote: don't use Int(arc4random()) anymore.
You're likely better off wrapping this in a Future publisher, possibly also wrapped with Deferred if you want it to response when subscriptions come in. Future is an excellent way to wrap external async API calls, especially ones that you can't fully control or otherwise easily adapt.
There's an example in Using Combine for "wrapping an async call with a Future to create a one-shot publisher" that looks like it might map quite closely to what you're trying to do.
If you want it to return more than a single value, then you may want to compose something out of PassthoughSubject or CurrentValueSubject that gives you an interface of -> AnyPublisher<YourType, Error> (or whatever you're looking for).
I have an async function that currently looks something like this
func startLoginFlow() {
IdentityProvider.shared.login { success, error in
// on success a user has completed authentication
if success {
delegate?.userIsAuthenticated()
}
// on error something wen't wrong
....
}
}
Essentially on success a delegate method is called and some action takes place as a result.
I'd like to wrap this as an observable instead. I do not have the option refactoring IdentityProvider.shared.login.
I essentially just need the observable to emit so I can subscribe and take action elsewhere using onNext.
I am currently doing the following
func startLoginFlow() -> Observable<Void> {
return Observable.create { [weak self] observable in
IdentityProvider.shared.login { success, error in
if success {
observable.onNext(Void())
}
}
return Disposables.create()
}
}
Is this the best way to do this? I wasn't sure if I should use Observable.of and subscribe to the result of IdentityProvider.shared.login
This is how I create Observables as well. The only thing I would note is to add in the errors so you can handle your observables when it errors out, and the completion, as well, to signal that your observable is complete.
func startLoginFlow() -> Observable<Void> {
return Observable.create { [weak self] observable in
IdentityProvider.shared.login { success, error in
if success {
observable.onNext(())
observable.onCompleted()
} else {
observable.onError(error)
}
}
return Disposables.create()
}
}
Observable.of's work in this case as well. It just emits the completed method. You can test this out yourself, if you were trying to create an Observable<String>, with both methods.
I find that doing Observable.create is beneficial here as you're doing network requests and that you can control how you want your observables to error, fail, or be completed.
Someone here gave a pretty good example as well:
Rxswift What difference between Observable.of and Observable<String>.create
I'd like to chain a Completable to an observable element. After calling flatMap, onCompleted and onError callbacks don't seem to be called on subscription.
var user = PublishRelay<User>()
func fetchUserInformation(_ userId: String) -> Completable {
return Completable.create { observer in
apiService.fetchInformation(for: userId, completion: { response in
if let name = response?.name {
user.accept(User(name: name))
observer(.completed)
} else {
observer(.error(ServiceError.userInformation))
}
})
return Disposables.create()
}
}
login()
.flatMap{ userId in fetchUserInformation(userId) }
.subscribe(
onCompleted: {
print("Success!") // Not being called at all
},
onError: { error in
print(error) // Not being called at all
}
).disposed(by: disposeBag)
Although fetchUserInformation and observer(.completed) are being called and user information is being successfully fetched, I won't be able to catch onCompleted on subscription (only when preceded by flatMap).
Is there a clean way to achieve this?
Already tried .materialized() just after the flatMap call in order to get an
Observable<Event<Never>>
rather than a
Observable<Never>
It doesn't work either.
The correct solution would be using the ‘andThen’ operator.
someCompletable
.andThen(someObservable)
Edit:
Just read the rest of your code - I'm not sure why you use a Completable at all since it seems you are actually returning some element from that stream.
You'll probably want to use a Single or Plain-ol' observable to relay that value without using an external Relay.
I think that you can do something like this:
login()
.flatMap{ userId -> Observable<Void> in
return fetchUserInformation(userId).andThen(.just(Void()))
}.subscribe(onNext: { _ in
...
}).disposed(by: disposeBag)
As far as I know you can't convert Completable to Observable since the later omits values while Completable does not.
I guess flatMap is returning Observables from Login and then you convert it to Completables and that's why it fails
I have a Completable being returned from a simple function.
This is not an async call, so I just need to return a succcessful completion or error depending on a conditional (using Rx here so I can tie into other Rx usages):
func exampleFunc() -> Completable {
if successful {
return Completable.just() // What to do here???
} else {
return Completable.error(SomeErrorType.someError)
}
}
The error case works pretty easily, but am having a block on how to just return a successful completable (without needing to .create() it).
I was thinking I just need to use Completable's .just() or .never(), but just is requiring a parameter, and never doesn't seem to trigger the completion event.
.empty() is the operator I was looking for!
Turns out, I had mixed up the implementations of .never() and .empty() in my head!
.never() emits no items and does NOT terminate
.empty() emits no items but does terminates normally
So, the example code above works like this:
func exampleFunc() -> Completable {
if successful {
return Completable.empty()
} else {
return Completable.error(SomeErrorType.someError)
}
}
Here is the documentation on empty/throw/never operators.
I would be more inclined to do the following:
func example() throws {
// do something
if !successful {
throw SomeErrorType.someError
}
}
Then in order to tie it into other Rx code, I would just use map as in:
myObservable.map { try example() }
But then, mapping over a Completable doesn't work because map's closure only gets called on next events. :-(
I tend to avoid Completable for this very reason, it doesn't seem to play well with other observables. I prefer to use Observable<Void> and send an empty event before the completed...
Something like this:
let chain = Observable<Void>.just()
let foo = chain.map { try example() }
foo.subscribe { event in print(event) }