How do I create a forever emitting event observable? - ios

I have an observable that is of type bool and I need this observable to never stop emitting events, a forever emitting event observable. I know I could not call the dispose bag but that would create a memory leak so what would not be the best way to create this observable. I looked into using .interval but it seems like it only works on an observable type.
viewModel.outputs.isAgentAvailable
.subscribe(
onNext: { [unowned self] isAgentAvailable in
if isAgentAvailable {
self.liveChatButton.isEnabled = true
self.isAgentAvailableLabel.text = agentAvailableText
} else {
self.liveChatButton.isEnabled = false
self.isAgentAvailableLabel.text = agentUnavailableText
self.liveChatButton.alpha = 0.5
}
}
).disposed(by: self.disposeBag)

Try this:
extension Observable {
public func never() -> Observable<Element> {
return .merge(self, .never())
}
}
P.S.: But I don't understand why you need infinity observable?

if viewModel.outputs.isAgentAvailable isn't emitting a completed or error event, then you already have what you want. Otherwise:
if it's cold, then use interval and concatMap (something like:)
Observable<Int>.interval(.seconds(3), scheduler: MainScheduler.instance)
.concatMap { _ in viewModel.outputs.isAgentAvailable }
If isAgentAvailable is hot, then remove the code that causes the completed event and it will stay alive.

Related

iOS Combine Start new request only if previous has finished

I have network request that triggers every last cell in switui appearas. Sometimes if user scrolls fast enough down -> up -> request will trigger before first one finishes. Without combine or reactive approach I have completion block and bool value to handle this:
public func load() {
guard !isLoadingPosts else { return }
isLoadingPosts = true
postsDataProvider.loadMorePosts { _ in
self.isLoadingPosts = false
}
}
I was wondering if with combine this can be resolved more elegantly, without the need to use bool value. For example execute request only if previous has finished?
It looks like you want to skip making the call if it's already in progress.
Since you didn't share any of the Combine code you might have, I'll assume that you have a publisher-returning function like this:
func loadMorePosts() -> AnyPublisher<[Post], Error> {
//...
}
Then you can use a subject to initiate a load call, a flatMap(maxPublishers:_:) downstream, with a number of publishers limited to 1:
let loadSubject = PassthroughSubject<Void, Never>()
loadSubject
.flatMap(maxPublishers: .max(1)) {
loadMorePosts()
}
.sink(
receiveCompletion: { _ in },
receiveValue: { posts in
// update posts
})
.store(in: &cancellables)
The above pipeline subscribes to the subject, but if another value arrives before flatMap is ready to receive it, it would simply be dropped.
Then the load function becomes:
func load() {
loadSubject.send(())
}

Dynamically terminate Observable based on another observable | RxSwift

I have array of Observables, say [Observable <WriteTaskResult>]
I want to perform all write tasks keeping their order, and if any one of them fails then I want to perform Observable<ResetTaskResult>
Following function will return observable of type BatchTasksResult for tracking tasks progress.
Sample Code:
enum BatchTasksResult{
case elapsedTime(Double)
case failedFatal
case rolledback
case success
}
func writeBlocks(tasks: [WriteTask]) -> Observable<BatchTasksResult>{
return Observable.create {(observable) -> Disposable in
let allTasks: [Observable<WriteTaskResult>] = self.writeSomewhere(tasks)
Observable.concat(allTasks)
.subscribe { writeTaskResult in
observable.onNext(.elapsedTime(writeTaskResult.totalTime))
}
onError: { (err) in
// Perform Observable<ResetTaskResult>
// if ResetTask was successful then observable.onNext(.rolledback)
// if ResetTask failed then observable.onNext(.failedFatal)
}
onCompleted: {
observable.onNext(.success)
}
.disposed(by: disposeBag)
return Disposables.create()
}
}
How do I trigger rollback logic using Observable from onError of allTasks' observable?
Simple solution seems nested observable, but that's not good practice, I guess? I tried FlatMap, but it can't really solve "If any sinlge task fails, then rollback and reset" Any other solution to this?
There's no need to add the extra level of indirection with the create function. Every Observable operator already creates a new object.
And when you do use Observable.create, do not dispose in an external dispose bag and return a Disposables.create(). Just return the disposable that you just created.
Here's the appropriate way to do what you want:
func writeBlocks(tasks: [WriteTask], resetTask: Single<ResetTaskResult>) -> Observable<BatchTasksResult> {
// create the array of write tasks and concat them. You seem to have that down.
let result = Observable.concat(tasks.map(writeSomewhere(task:)).map { $0.asObservable() })
.share() // the share is needed because you are using the value twice below.
return Observable.merge(
// push out the elapsed time for each task.
result.map { BatchTasksResult.elapsedTime($0.totalTime) },
// when the last one is done, push out the success event.
result.takeLast(1).map { _ in BatchTasksResult.success }
)
.catch { _ in
resetTask // the resetTask will get subscribed to if needed.
.map { _ in BatchTasksResult.rolledback } // if successful emit a rollback
.catch { _ in Single.just(BatchTasksResult.failedFatal) } // otherwise emit the failure.
.asObservable()
}
}
func writeSomewhere(task: WriteTask) -> Single<WriteTaskResult> {
// create a Single that performs the write and emits a result.
}

Get values from two apis into two different Observable and perform some operation

I have two independent observables. I need to perform some operation when both of them are complete and each of them provided an array.
let myObj1Array = myObj1Manager.getMyObj1List()//returns Observable<[MyObj1]>
let myObj2Array = myObj2Manager.getMyObj2List()//returns Observable<[MyObj2]>
Now I need to compare values of myObj1Array and myObj2Array and on the basis of that create another array using values from both arrays. I know how to subscribe 1 variable but not sure how to observe completion of two different arrays.
Edit:
I also tried following but I get values only from first array:
let myObj1Array = myObj1Manager.getMyObj1List()
let myObj2Array = myObj1Array.flatMap { _ in myObj2Manager.getMyObj2List() }
Observable.combineLatest(myObj1Array, myObj2Array)
.subscribe(onNext: { (sss, sds) in
print(sss)
})
.addDisposableTo(disposeBag)
I am actually kind of clueless about how to handle such scenario.
Edit2:
function to get the observables in first array:
func getMyObj1List() -> Observable<[MyObj1]> {
return Observable.create { observer -> Disposable in
self.specialsRest.getMyObj1List { response, error in
if let error = error {
observer.onError(Exception(error))
return
}
guard let saleItems = MyObj1.decode(data: response?.data) else {
observer.onError(Exception("Could not decode specials!"))
return
}
queueBackground.async {
observer.onNext(saleItems)
observer.onCompleted()
}
}
return Disposables.create { self.specialsRest.cancel() }
}
}
DispatchGroup is probably the way to go here.
https://developer.apple.com/documentation/dispatch/dispatchgroup
When all work items finish executing, the group executes its completion handler. You can also wait synchronously for all tasks in the group to finish executing.
var dg:DispatchGroup = DispatchGroup()
//Wherever you start your observables.
//Start Observer1
dg.enter()
//Start Observer2
dg.enter()
...
...
...
//Wherever you retrieve data
SomeAsyncFuncForObserver1 {
//Get Data
dg.leave()
}
SomeAsyncFuncForObserver2 {
//Get Data
dg.leave()
}
dg.notify(queue: .main) {
print("all finished.")
}
I believe you need to use zip instead of combineLatest. From the docs
The CombineLatest operator behaves in a similar way to Zip, but while
Zip emits items only when each of the zipped source Observables have
emitted a previously unzipped item, CombineLatest emits an item
whenever any of the source Observables emits an item (so long as each
of the source Observables has emitted at least one item).
Observable
.zip(myObj1Array, myObj2Array)
.subscribe(onNext: { (sss, sds) in
print(sss)
})
.addDisposableTo(disposeBag)

How do I stop RxSwift ble scanner once it has found a match?

I have a ble scanner that works and looks like this:
func scan(serviceId: String) -> Observable<[BleHandler.BlePeripheral]> {
knownDevices = []
return waitForBluetooth()
.flatMap { _ in self.scanForPeripheral(serviceId: serviceId) }
.map { _ in self.knownDevices }
}
private func waitForBluetooth() -> Observable<BluetoothState> {
return self.manager
.observeState()
.startWith(self.manager.state)
.filter { $0 == .poweredOn }
.take(1)
}
Then in the viewModel class it filters matches from core data:
func scanAndFilter() -> Observable<[LocalDoorCoreDataObject]> {
let persistingDoors: [LocalDoorCoreDataObject] = coreDataHandler.fetchAll(fetchRequest: NSFetchRequest<LocalDoorCoreDataObject>(entityName: "LocalDoorCoreDataObject"))
return communicationService
.scanForDevices(register: false)
.map{ peripherals in
print("🐶 THIS WILL GO ON FOR ETERNITY", peripherals.count)
self.knownDevices = peripherals
return persistingDoors
.filter { door in peripherals.contains(where: { $0.identifier.uuidString == door.dPeripheralId }) }
}
}
And in the view I want to connect when the scan is completed:
private func scanAndConnect(data: LocalDoorCoreDataObject) {
viewModel.scanRelay().subscribe(
onNext: {
print("🐶SCANNED NAME", $0.first?.dName)},
onCompleted: {
print("🐶COMPLETED SCAN")
self.connectToFilteredPeripheral(localDoor: data)
}).disposed(by: disposeBag)
}
It never reaches onCompleted as it will just scan for eternity even after having found and filtered the core data match. In Apple's framework coreBluetooth I could simply call manager.stopScan() after it has found what I want, but that doesn't seem to be available on the Rx counterpart. How does it work for RxSwift
You can create a new Observable that looks for devices and then completes as soon as it finds the device(s) you're looking for. This would be something like:
func scanAndFilter() -> Observable<[LocalDoorCoreDataObject]> {
return Observable.deferred { in
let persistingDoors: [LocalDoorCoreDataObject] = coreDataHandler.fetchAll(fetchRequest: NSFetchRequest<LocalDoorCoreDataObject>(entityName: "LocalDoorCoreDataObject"))
return communicationService
.scanForDevices(register: false)
.filter { /* verify if the device(s) you're looking for is/are in this list */ }
.take(1)
}
}
The filter operator will make sure that only lists that contain the device you're looking for are passed on and the take(1) operator will take the first emitted value and complete immediately.
The deferred call makes sure that the fetch request that is performed in the first line is not executed when you call scanAndFilter() but only when somebody actually subscribes to the resulting Observable.
If you only want one event to exit the filter operator, then just use .take(1). The Observable will shut down after it emits a single value. If the BLE function is written correctly, it will call stopScan() when the Disposable is disposed of.
I have no idea why the other answer says to "always make sure to wrap all function that return Observables into a .deferred. I've been using RxSwift since 2015 and I've only ever needed deferred once. Certainly not every time I called a function that returned an Observable.

RxSwift - how to have readable combineLatest event element variable names?

var isCurrentlySearchingAddress = Variable<Bool>(false)
var currentSelectedPlace = Variable<GeocodePlace?>(nil)
when I make a subscription on
Observable.combineLatest(viewModel.isCurrentlySearchingAddress.asObservable(), viewModel.currentSelectedPlace.asObservable())
How can I have readable variable names instead of 0 and 1?
You can do it like this:
Observable.combineLatest(
varA.asObservable(), varB.asObservable(),
resultSelector: { value1, value2 in
print("\(value1) \(value2)")
}).observeOn(MainScheduler.instance)
.subscribe()
.disposed(by: disposeBag)
Here you're using the subscribe(on:) then force unwrapping element. In case of error the element will we nil and it will cause crash. It is not a good idea to force unwrap it.
If you're sure that the sequence will never emit error or you don't want to handle the error then it's better to use subscribe(onNext:) like this:
Observable.combineLatest(viewModel.isCurrentlySearchingAddress.asObservable(), viewModel.currentSelectedPlace.asObservable())
.subscribe(onNext: { (isCurrentlySearchingAddress, currentSelectedPlace) in
// Use isCurrentlySearchingAddress: Bool and currentSelectedPlace: GeocodePlace? here.
if (isCurrentlySearchingAddress) {
// TODO:
}
})
Also don't forget to add it to DisposeBag.
If you are only interested in next events you can do this:
Observable.combineLatest(isCurrentlySearchingAddress.asObservable(), currentSelectedPlace.asObservable())
.subscribe(onNext: { (isSearchingAddress, selectedPlace) in
})
Or this (if you want to keep using subscribe):
Observable.combineLatest(isCurrentlySearchingAddress.asObservable(), currentSelectedPlace.asObservable())
.subscribe { event in
if case let .next(isSearchingAddress, selectedPlace) = event {
}
}

Resources