I'm wrapping some legacy completion-block code in an Observable. It will emit one event (next or error), and then complete. The problem is that calling onNext(), onCompleted() only sends the completed event to the observer. Why doesn't the next event get delivered?
UPDATE: The people stream actually works as expected. The issue turns out to be in the next stream, filteredPeople. The inner completed event is passed along to it, and I'm just returning it, which terminates the stream.
I need to filter out completed events from inner streams.
let people = Observable<Event<[Person]>>()
.flatMapLatest {
return fetchPeople().asObservable().materialize()
}
.share()
// this is bound to a search field
let filterText = PublishSubject<String>()
let filteredPeople = Observable.combineLatest(people, filterText) { peopleEvent, filter in
// this is the problem. the completed event from people is being returned, and it terminates the stream
guard let people = peopleEvent.element else { return peopleEvent }
if filterText.isEmpty { return .next(people) }
return .next(people.filter { ... })
}
func fetchPeople() -> Single<[Person]> {
return Single<[Person]>.create { observer in
PeopleService.fetch { result in
switch result {
case .success(let people):
observer(.success(people))
case .failure(let error):
observer(.error(error))
}
}
return Disposables.create()
}
}
filteredPeople.subscribe(
onNext: { event in
// ?! doesn't get called
},
onCompleted: {
// we get here, but why?
},
onError: {event in
...
}).disposed(by: disposeBag)
You haven't posted the code that is causing the problem. The code below works as expected:
struct Person { }
class PeopleService {
static func fetch(_ result: #escaping (Result<[Person], Error>) -> Void) {
result(.success([]))
}
}
let disposeBag = DisposeBag()
func fetchPeople() -> Single<[Person]> {
return Single<[Person]>.create { observer in
PeopleService.fetch { result in
switch result {
case .success(let people):
observer(.success(people))
case .failure(let error):
observer(.error(error))
}
}
return Disposables.create()
}
}
let people = Observable<Void>.just(())
.flatMapLatest { _ in
return fetchPeople().asObservable().materialize()
}
.share()
people.subscribe(
onNext: { event in
print("onNext does get called")
print("in fact, it will get called twice, once with a .next(.next([Person])) event")
print("and once with a .next(.completed) event.")
},
onCompleted: {
print("this prints after onNext gets called")
})
.disposed(by: disposeBag)
I fixed it by filtering out completed events from the inner stream. I am not sure this is the right way, but I can't think of a better solution.
let people = Observable<Event<[Person]>>()
.flatMapLatest {
return fetchPeople()
.asObservable()
.materialize()
// Our work is done, but don't end the parent stream
.filter { !$0.isCompleted }
}
.share()
Related
I have a case where I would like to validate form and then if everything is ok go to api request.
I've written some code and it works fine but errors dispose my stream. I know I could add .catch error at the end of flat map but then next flat map would be executed.
Can I add catch error at the end of stream without disposing it? Or the only way to deal with it is separate it to two streams validation and server responses?
enum Response {
case error(message: String)
case success
}
let start = input.validate
.withLatestFrom(input.textFields)
.flatMap { [unowned self] fields -> Observable<String> in
return self.validate(characters: fields)
}
.flatMapLatest { [unowned self] code -> Observable<String> in
return self.apiClient.rxSendData(code)
.retry(1)
}
.map { _ in return Response.success }
.asDriver { Driver.just(Response.error(message: $0.localizedDescription)) }
I'm making some assumptions about code you aren't showing. Your validate function is especially odd to me. It looks like it emits a String (which is ignored, if validation was successful and doesn't emit anything (or maybe an error) if validation failed?
let start = input.validate
.withLatestFrom(input.textFields)
.flatMapLatest { [unowned self] fields -> Observable<String> in
return self.validate(characters: fields)
.catchError { _ in Observable.empty() } // empty() doesn't emit a value so the next flatMap won't be executed.
}
.flatMapLatest { [unowned self] _ -> Observable<Response> in
return self.apiClient.rxSendData()
.retry(1)
.map { _ in Response.success }
.catchError { error in Observable.just(Response.error(message: error.localizedDescription)) }
}
.asDriver { Driver.just(Response.error(message: $0.localizedDescription)) }
If validate emits an error when validation fails, and you want to capture that error, then something like this would work:
let start = input.validate
.withLatestFrom(input.textFields)
.flatMapLatest { [unowned self] fields -> Observable<Response> in
return self.validate(characters: fields)
.map { _ in Response.success }
.catchError { Observable.just(Response.error(message: $0.localizedDescription)) }
}
.flatMapLatest { [unowned self] validation -> Observable<Response> in
// here, the above flatMap emits a value no matter what, so we have to switch on it to determine if we want to continue or just push the Response down the pipe.
switch validation {
case .error:
return Observable.just(validation)
case .success:
return self.apiClient.rxSendData()
.retry(1)
.map { _ in Response.success }
.catchError { error in Observable.just(Response.error(message: error.localizedDescription)) }
}
}
.asDriver { Driver.just(Response.error(message: $0.localizedDescription)) }
Have you considered the materialize operator? It converts an observable sequence into an observable sequence of event objects detailing what happened that cannot error but completes when the input sequence completes. You can then share that.
Something like:
let code = input.validate
.withLatestFrom(input.textFields)
.flatMap { [unowned self] fields -> Observable<String> in
self.validate(characters: fields)
.materialize()
}
.share(replay: 1)
code
.compactMap { $0.error }
.subscribe() // Show error from `self.validate`
.disposed(by: bag)
let request = code
.compactMap { $0.element }
// Will get to this flat map only if `self.validate` did not error
.flatMapLatest { [unowned self] code -> Observable<String> in
self.apiClient.rxSendData(code)
.retry(1)
.materialize()
}
.share(replay: 1)
request
.compactMap { $0.error }
.subscribe() // Show error from `self.apiClient.rxSendData`
.disposed(by: bag)
request
.compactMap { $0.element }
// Do something as a result of the request being successful
The chains would not cease upon self.validate and self.apiClient.rxSendData emitting errors.
I ran into a situation where I would fetch an API which will generate json data of registered users. I would then have to loop through each user and fetch their avatar from remote url and save it to disk. I can perform this second task inside subscribe but this is not a best practice. I am trying to implement it with map, flatMap etc.
Here is my sample code:
self.dataManager.getUsers()
.observeOn(MainScheduler.instance)
.subscribeOn(globalScheduler)
.map{ [unowned self] (data) -> Users in
var users = data
// other code for manipulating users goes here
// then below I am trying to use another loop to fetch their avatars
if let cats = users.categories {
for cat in cats {
if let profiles = cat.profiles {
for profile in profiles {
if let thumbnail = profile.thumbnail,
let url = URL(string: thumbnail) {
URLSession.shared.rx.response(request: URLRequest(url: url))
.subscribeOn(MainScheduler.instance)
.subscribe(onNext: { response in
// Update Image
if let img = UIImage(data: response.data) {
try? Disk.save(img, to: .caches, as: url.lastPathComponent)
}
}, onError: { (error) in
}).disposed(by: self.disposeBag)
}
}
}
}
}
return users
}
.subscribe(onSuccess: { [weak self] (users) in
}).disposed(by: disposeBag)
There are 2 problems in this code. First is with the rx on URLSession which execute the task in background on another thread and there is no way to acknowledge the main subscribe back when this operation will finish. Second is with the loop and rx which is not efficient as it should generate multiple observables and then process it.
Any idea to improve this logic is welcome.
This was a fun puzzle.
The "special sauce" that solves the problem is in this line:
.flatMap {
Observable.combineLatest($0.map {
Observable.combineLatest(
Observable.just($0.0),
URLSession.shared.rx.data(request: $0.1)
.materialize()
)
})
}
The map before the line creates an Observable<[(URL, URLRequest)]> and the line in question converts it to an Observable<[(URL, Event<Data>)]>.
The line does this by:
Set up the network call to create an Observable<Data>
Materialize it to create an Observable<Event<Data>> (this is done so an error in one download won't shutdown the entire stream.)
Lift the URL back into an Observable which gives us an Observable<URL>
Combine the observables from steps 2 & 3 to produce an Observable<(URL, Event<Data>)>.
Map each array element to produce [Observable<(URL, Event<Data>)>]
Combine the observables in that array to finally produce Observable<[(URL, Event<Data>)]>
Here is the code
// manipulatedUsers is for the code you commented out.
// users: Observable<Users>
let users = self.dataManager.getUsers()
.map(manipulatedUsers) // manipulatedUsers(_ users: Users) -> Users
.asObservable()
.share(replay: 1)
// this chain is for handling the users object. You left it blank in your code so I did too.
users
.observeOn(MainScheduler.instance)
.subscribe(onNext: { users in
})
.disposed(by: disposeBag)
// This navigates through the users structure and downloads the images.
// images: Observable<(URL, Event<Data>)>
let images = users.map { $0.categories ?? [] }
.map { $0.flatMap { $0.profiles ?? [] } }
.map { $0.compactMap { $0.thumbnail } }
.map { $0.compactMap { URL(string: $0) } }
.map { $0.map { ($0, URLRequest(url: $0)) } }
.flatMap {
Observable.combineLatest($0.map {
Observable.combineLatest(
Observable.just($0.0),
URLSession.shared.rx.data(request: $0.1)
.materialize()
)
})
}
.flatMap { Observable.from($0) }
.share(replay: 1)
// this chain filters out the errors and saves the successful downloads.
images
.filter { $0.1.element != nil }
.map { ($0.0, $0.1.element!) }
.map { ($0.0, UIImage(data: $0.1)!) }
.observeOn(MainScheduler.instance)
.bind(onNext: { url, image in
try? Disk.save(image, to: .caches, as: url.lastPathComponent)
return // need two lines here because this needs to return Void, not Void?
})
.disposed(by: disposeBag)
// this chain handles the download errors if you want to.
images
.filter { $0.1.error != nil }
.bind(onNext: { url, error in
print("failed to download \(url) because of \(error)")
})
.disposed(by: disposeBag)
I'm implementing a BLE protocol between a central (iPhone) and peripheral (custom device). The protocol works as follows:
central connects to peripheral and sets up notification
peripheral sends data on notification characteristic
central processes data and sends response on separate characteristic
peripheral sends addtnl data on notification characteristic
central process data and disconnects.
I'm attempting to implement this in a clean way using RxBluetoothKit. It currently works, but I'd like to solve the following challenges:
What is the best way to cleanly disconnect in step 5. I'm hoping to not have to dispose the overall observable, but rather just have it 'complete'. I'm currently using 'takeUntil', but not sure if that's the best way.
Allow for the notification to cleanup gracefully prior to disconnect. With my current code, I receive an 'API MISUSE can only accept commands while in the connected state' because I believe the notification is cleaning up while the disconnect is occurring.
Thanks.
enum TestPeripheralService: String, ServiceIdentifier {
case main = "CED916FA-6692-4A12-87D5-6F2764762B23"
var uuid: CBUUID { return CBUUID(string: self.rawValue) }
}
enum TestPeripheralCharacteristic: String, CharacteristicIdentifier {
case writer = "CED927B4-6692-4A12-87D5-6F2764762B2A"
case reader = "CED9D5D8-6692-4A12-87D5-6F2764762B2A"
var uuid: CBUUID { return CBUUID(string: self.rawValue) }
var service: ServiceIdentifier { return TestPeripheralService.main }
}
fileprivate lazy var centralManager: CentralManager = {
RxBluetoothKitLog.setLogLevel(.verbose)
return CentralManager(queue: .main)
}()
func executeConnectionAndHandshake() {
let disconnectSubject = PublishSubject<Bool>.init()
var peripheral: Peripheral?
var packetNum = 0
_ = centralManager
.observeState()
.startWith(centralManager.state)
.filter { $0 == .poweredOn }
.flatMap { _ in self.centralManager.scanForPeripherals(withServices: [TestPeripheralService.main.uuid]) }
.flatMap { $0.peripheral.establishConnection().takeUntil(disconnectSubject) }
.do(onNext: { peripheral = $0 })
.flatMap { $0.discoverServices([TestPeripheralService.main.uuid])}
.flatMap { $0[0].discoverCharacteristics(nil)}
.flatMap { _ in
Observable<Bool>.create { event in
let disposables = CompositeDisposable()
let readSubject = PublishSubject<Data>.init()
_ = disposables.insert(peripheral!.observeValueUpdateAndSetNotification(for: TestPeripheralCharacteristic.reader)
.subscribe(onNext: {
packetNum += 1
let packet = $0.value!
if (packetNum <= 1) {
readSubject.onNext(packet)
} else {
event.onNext(true)
event.onCompleted()
}
}, onError: { event.onError($0) })
)
_ = disposables.insert(readSubject
.flatMapLatest { data -> Single<Characteristic> in
var writeData = Data(capacity: 300)
for _ in 0..<300 {
writeData.append(0xFF)
}
return peripheral!.writeValue(writeData, for: TestPeripheralCharacteristic.writer, type: .withResponse)
}
.subscribe(onError: { event.onError($0) })
)
return Disposables.create {
disposables.dispose()
}
}
.do(onCompleted: { disconnectSubject.onNext(true) })
}
.subscribe(onError: { print($0) },
onCompleted: { print("Connection and handshake completed") })
}
im breaking my mind around how to do this in RX.
The actual usecase is mapping of LowerLevelEvent(val userId: String) to HigherLevelEvent(val user: User), where the User is provided by observable, so it can emit n times, so example output
LowerLevelEvent1(abc) -> HigherLevelEvent1(userAbc(nameVariation1)
LowerLevelEvent2(abc) -> HigherLevelEvent2(userAbc(nameVariation1)
LowerLevelEvent3(abc) -> HigherLevelEvent3(userAbc(nameVariation1)
LowerLevelEvent4(abc) -> HigherLevelEvent4(userAbc(nameVariation1)
HigherLevelEvent4(userAbc(nameVariation2)
HigherLevelEvent4(userAbc(nameVariation3)
So my naive solution was to use combineLatest. So while userId is not changed user observable is subscribed, i.e. not resubscribed when new lowerLevelEmits & its userId is not changed
val _lowerLevelEventObservable: Observable<LowerLevelEvent> = lowerLevelEventObservable
.replayingShare()
val _higherLevelEventObservable: Observable<HigherLevelEvent> = Observables
.combineLatest(
_lowerLevelEventObservable,
_lowerLevelEventObservable
.map { it.userId }
.distinctUntilChanged()
.switchMap { userRepository.findByIdObservable(it)
) { lowerLevelEvent, user -> createHigherLevelInstance... }
However this has glitch issues, since both sources in combineLatest originate from same observable.
Then I thought about
lowerLevelObservable.
.switchMap { lowerLevelEvent ->
userRepository.findByIdObservable(lowerLevelEvent.userId)
.map { user -> createHigherLevelInstance... }
}
This however can break if lowerLevelObservable emits fast, and since user observable can take some time, given lowerLevelX event can be skipped, which I cannot have. Also it resubscribes user observable each emit, which is wasteful since it wont change most likely
So, maybe concatMap? That has issue of that the user observable doesnt complete, so concatMap wouldnt work.
Anyone have a clue?
Thanks a lot
// Clarification:
basically its mapping of A variants (A1, A2..) to A' variants (A1', A2'..) while attaching a queried object to it, where the query is observable so it might reemit after the mapping was made, so AX' needs to be reemited with new query result. But the query is cold and doesnt complete
So example A1(1) -> A1'(user1), A2(1) -> A2'(user1), A3(1) -> A3'(user1) -- now somebody changes user1 somewhere else in the app, so next emit is A3'(user1')
Based on the comments you have made, the below would work in RxSwift. I have no idea how to translate it to RxJava. Honestly though, I think there is a fundamental misuse of Rx here. Good luck.
How it works: If it's allowed to subscribe it will, otherwise it will add the event to a buffer for later use. It is allowed to subscribe if it currently isn't subscribed to an inner event, or if the inner Observable it's currently subscribed to has emitted an element.
WARNING: It doesn't handle completions properly as it stands. I'll leave that to you as an exercise.
func example(lowerLevelEventObservable: Observable<LowerLevelEvent>, userRepository: UserRepository) {
let higherLevelEventObservable = lowerLevelEventObservable
.flatMapAtLeastOnce { event in // RxSwift's switchLatest I think.
Observable.combineLatest(
Observable.just(event),
userRepository.findByIdObservable(event.userId),
resultSelector: { (lowLevelEvent: $0, user: $1) }
)
}
.map { createHigherLevelInstance($0.lowLevelEvent, $0.user) }
// use higherLevelEventObservable
}
extension ObservableType {
func flatMapAtLeastOnce<U>(from fn: #escaping (E) -> Observable<U>) -> Observable<U> {
return Observable.create { observer in
let disposables = CompositeDisposable()
var nexts: [E] = []
var disposeKey: CompositeDisposable.DisposeKey?
var isAllowedToSubscribe = true
let lock = NSRecursiveLock()
func nextSubscription() {
isAllowedToSubscribe = true
if !nexts.isEmpty {
let e = nexts[0]
nexts.remove(at: 0)
subscribeToInner(e)
}
}
func subscribeToInner(_ element: E) {
isAllowedToSubscribe = false
if let key = disposeKey {
disposables.remove(for: key)
}
let disposable = fn(element).subscribe { innerEvent in
lock.lock(); defer { lock.unlock() }
switch innerEvent {
case .next:
observer.on(innerEvent)
nextSubscription()
case .error:
observer.on(innerEvent)
case .completed:
nextSubscription()
}
}
disposeKey = disposables.insert(disposable)
}
let disposable = self.subscribe { event in
lock.lock(); defer { lock.unlock() }
switch event {
case let .next(element):
if isAllowedToSubscribe == true {
subscribeToInner(element)
}
else {
nexts.append(element)
}
case let .error(error):
observer.onError(error)
case .completed:
observer.onCompleted()
}
}
_ = disposables.insert(disposable)
return disposables
}
}
}
I am setting up an iOS 8 app to request Heath Kit Store authorization to share types. The request Read/Write screen shows fine and on selecting Done, I see the completion callback immediately after. In this callback, I am pushing a new view controller. I set a breakpoint for the code that is programmatically pushing the next view controller and this is called immediately, but the transition doesn't occur until about 10 seconds later.
Some code:
#IBAction func enable(sender: AnyObject) {
let hkManager = HealthKitManager()
hkManager.setupHealthStoreIfPossible { (success, error) -> Void in
if let error = error {
println("error = \(error)")
} else {
println("enable HK success = \(success)")
self.nextStep()
}
}
}
func nextStep() {
self.nav!.pushViewController(nextController, animated: true)
}
class HealthKitManager: NSObject {
let healthStore: HKHealthStore!
override init() {
super.init()
healthStore = HKHealthStore()
}
class func isHealthKitAvailable() -> Bool {
return HKHealthStore.isHealthDataAvailable()
}
func setupHealthStoreIfPossible(completion: ((Bool, NSError!) -> Void)!) {
if HealthKitManager.isHealthKitAvailable()
{
healthStore.requestAuthorizationToShareTypes(dataTypesToWrite(), readTypes: dataTypesToRead(), completion: { (success, error) -> Void in
completion(success, error)
})
}
}
func dataTypesToWrite() -> NSSet {
let runningType = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierDistanceWalkingRunning)
let stepType = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierStepCount)
return NSSet(objects: runningType, stepType)
}
func dataTypesToRead() -> NSSet {
let runningType = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierDistanceWalkingRunning)
let stepType = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierStepCount)
let climbedType = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierFlightsClimbed)
return NSSet(objects: runningType, stepType, climbedType)
}
}
Any thoughts on what is causing the time delay for the transition?
The problem was that the completion block is returned in the background queue. I just put the transition call back onto the main queue as follows:
hkManager.setupHealthStoreIfPossible { (success, error) -> Void in
if let error = error {
println("error = \(error)")
} else {
dispatch_async(dispatch_get_main_queue(), {
println("enable HK success = \(success)")
self.nextStep()
});
}
}
}