Show an activity indicator after x seconds, using RxSwift - ios

I am currently using RxSwift and the ActivityIndicator extension (https://github.com/ReactiveX/RxSwift/blob/master/RxExample/RxExample/Services/ActivityIndicator.swift).
I am trying to achieve showing an activity indicator (spinner) image when an API request takes more than 2 seconds. The RxSwift ActivityIndicator is basically a (so-called hot) Observable<Bool> that emits true when the API calls is made, and false when the whole thing is done.
To get the desired behavior, I have played around with many combinations of throttleing, debounceing and such, together with operators like window and pausable, but I can never get it quite right.
Thoughts?

let disposeBag = DisposeBag()
let activityUntil = 1
// `true` at 0 seconds, `false` at `activityUntil` seconds
let trueUntil = Observable<Int>.timer(0,
period: 1,
scheduler: MainScheduler.instance)
.map { $0 < activityUntil }
.distinctUntilChanged()
.shareReplayLatestWhileConnected()
// this is to show you the actual values coming through
_ = trueUntil
.subscribe { print("raw: \($0)") }
let yourDesiredObservable = trueUntil
.delaySubscription(2, scheduler: MainScheduler.instance)
yourDesiredObservable
.subscribe { print("still activity?: \($0)") }
With activityUntil = 1:
raw: next(true)
raw: next(false)
still activity?: next(false)
With activityUntil = 3:
raw: next(true)
still activity?: next(true)
raw: next(false)
still activity?: next(false)
As you can see, yourDesiredObservable only emits elements after 2 seconds, replaying the last element from before the 2 second mark.

Related

Creating a sequential publisher in Swift Combine

I want to debounce a batch of events and process them after a delay of ~1.5 seconds. Here's what I've done.
class ViewModel: ObservableObject {
#Published var pending: [TaskInfo]
private var cancellable: AnyCancellable? = nil
init() {
processPendingTasks()
}
func queueTask(task: TaskInfo) {
pending.append(task)
}
private func processPendingTasks() {
cancellable = $pendingTasks
.debounce(for: 1.5, scheduler: RunLoop.main)
.sink(receiveValue: { batch in
// Iterate though elements and process events.
})
}
}
Issue: This works fine, but the issue that I've is that it performs unnecessary view updates since the array is tagged #Published.
What I'm looking for: The ideal approach would be a streaming setup where I get all events (in a batched fashion) but the sink should wait exactly for 1.5 seconds after the last event was added.
I tried PassthroughSubject, but it seems like it only gets me the last event that happened in the last 1.5 seconds.
A possible solution is a combination of a PassthroughSubject and the collect operator. In queueTask send the tasks to the subject.
func queueTask(task: TaskInfo) {
subject.send(task)
}
1.5 seconds after receiving the last item send
subject.send(completion: .finished)
and subscribe
subject
.collect()
.sink { [weak self] tasks in
self?.pending = tasks
}
If the interval of the incoming tasks is < 1.5 seconds you could also use the .timeout(1.5) operator which terminates the pipeline after the timeout interval.

How to break out of a loop that uses async DispatchQueue inside

I am using a for loop coupled with a DispatchQueue with async to incrementally increase playback volume over the course of a 5 or 10-minute duration.
How I am currently implementing it is:
for i in (0...(numberOfSecondsToFadeOut*timesChangePerSecond)) {
DispatchQueue.main.asyncAfter(deadline: .now() + Double(i)/Double(timesChangePerSecond)) {
if self.activityHasEnded {
NSLog("Activity has ended") //This will keep on printing
} else {
let volumeSetTo = originalVolume - (reductionAmount)*Float(i)
self.setVolume(volumeSetTo)
}
}
if self.activityHasEnded {
break
}
}
My goal is to have activityHasEnded to act as the breaker. The issue as noted in the comment is that despite using break, the NSLog will keep on printing over every period. What would be the better way to fully break out of this for loop that uses DispatchQueue.main.asyncAfter?
Updated: As noted by Rob, it makes more sense to use a Timer. Here is what I did:
self.fadeOutTimer = Timer.scheduledTimer(withTimeInterval: timerFrequency, repeats: true) { (timer) in
let currentVolume = self.getCurrentVolume()
if currentVolume > destinationVolume {
let volumeSetTo = currentVolume - reductionAmount
self.setVolume(volumeSetTo)
print ("Lowered volume to \(volumeSetTo)")
}
}
When the timer is no longer needed, I call self.fadeOutTimer?.invalidate()
You don’t want to use asyncAfter: While you could use DispatchWorkItem rendition (which is cancelable), you will end up with a mess trying to keep track of all of the individual work items. Worse, a series of individually dispatch items are going to be subject to “timer coalescing”, where latter tasks will start to clump together, no longer firing off at the desired interval.
The simple solution is to use a repeating Timer, which avoids coalescing and is easily invalidated when you want to stop it.
You can utilise DispatchWorkItem, which can be dispatch to a DispatchQueue asynchronously and can also be cancelled even after it was dispatched.
for i in (0...(numberOfSecondsToFadeOut*timesChangePerSecond)) {
let work = DispatchWorkItem {
if self.activityHasEnded {
NSLog("Activity has ended") //This will keep on printing
} else {
let volumeSetTo = originalVolume - (reductionAmount)*Float(i)
self.setVolume(volumeSetTo)
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + Double(i)/Double(timesChangePerSecond), execute: work)
if self.activityHasEnded {
work.cancel() // cancel the async work
break // exit the loop
}
}

How to get cancellation state for multiple DispatchWorkItems

Background
I'm implementing a search. Each search query results in one DispatchWorkItem which is then queued for execution. As the user can trigger a new search faster than the previous one can be completed, I'd like to cancel the previous one as soon as I receive a new one.
This is my current setup:
var currentSearchJob: DispatchWorkItem?
let searchJobQueue = DispatchQueue(label: QUEUE_KEY)
func updateSearchResults(for searchController: UISearchController) {
let queryString = searchController.searchBar.text?.lowercased() ?? ""
// if there is already an (older) search job running, cancel it
currentSearchJob?.cancel()
// create a new search job
currentSearchJob = DispatchWorkItem() {
self.filter(queryString: queryString)
}
// start the new job
searchJobQueue.async(execute: currentSearchJob!)
}
Problem
I understand that dispatchWorkItem.cancel() doesn't kill the running task immediately. Instead, I need to check for dispatchWorkItem.isCancelled manually. But how do I get the right dispatchWorkItemobject in this case?
If I were setting currentSearchJob only once, I could simply access that attribute like done in this case. However, this isn't applicable here, because the attribute will be overriden before the filter() method will be finished. How do I know which instance is actually running the code in which I want to check for dispatchWorkItem.isCancelled?
Ideally, I'd like to provide the newly-created DispatchWorkItem as an additional parameter to the filter() method. But that's not possible, because I'll get a Variable used within its own initial value error.
I'm new to Swift, so I hope I'm just missing something. Any help is appreciated very much!
The trick is how to have a dispatched task check if it has been canceled. I'd actually suggest consider OperationQueue approach, rather than using dispatch queues directly.
There are at least two approaches:
Most elegant, IMHO, is to just subclass Operation, passing whatever you want to it in the init method, and performing the work in the main method:
class SearchOperation: Operation {
private var queryString: String
init(queryString: String) {
self.queryString = queryString
super.init()
}
override func main() {
// do something synchronous, periodically checking `isCancelled`
// e.g., for illustrative purposes
print("starting \(queryString)")
for i in 0 ... 10 {
if isCancelled { print("canceled \(queryString)"); return }
print(" \(queryString): \(i)")
heavyWork()
}
print("finished \(queryString)")
}
func heavyWork() {
Thread.sleep(forTimeInterval: 0.5)
}
}
Because that's in an Operation subclass, isCancelled is implicitly referencing itself rather than some ivar, avoiding any confusion about what it's checking. And your "start a new query" code can just say "cancel anything currently on the the relevant operation queue and add a new operation onto that queue":
private var searchQueue: OperationQueue = {
let queue = OperationQueue()
// queue.maxConcurrentOperationCount = 1 // make it serial if you want
queue.name = Bundle.main.bundleIdentifier! + ".backgroundQueue"
return queue
}()
func performSearch(for queryString: String) {
searchQueue.cancelAllOperations()
let operation = SearchOperation(queryString: queryString)
searchQueue.addOperation(operation)
}
I recommend this approach as you end up with a small cohesive object, the operation, that nicely encapsulates a block of work that you want to do, in the spirit of the Single Responsibility Principle.
While the following is less elegant, technically you can also use BlockOperation, which is block-based, but for which which you can decouple the creation of the operation, and the adding of the closure to the operation. Using this technique, you can actually pass a reference to the operation to its own closure:
private weak var lastOperation: Operation?
func performSearch(for queryString: String) {
lastOperation?.cancel()
let operation = BlockOperation()
operation.addExecutionBlock { [weak operation, weak self] in
print("starting \(identifier)")
for i in 0 ... 10 {
if operation?.isCancelled ?? true { print("canceled \(identifier)"); return }
print(" \(identifier): \(i)")
self?.heavyWork()
}
print("finished \(identifier)")
}
searchQueue.addOperation(operation)
lastOperation = operation
}
func heavyWork() {
Thread.sleep(forTimeInterval: 0.5)
}
I only mention this for the sake of completeness. I think the Operation subclass approach is frequently a better design. I'll use BlockOperation for one-off sort of stuff, but as soon as I want more sophisticated cancelation logic, I think the Operation subclass approach is better.
I should also mention that, in addition to more elegant cancelation capabilities, Operation objects offer all sorts of other sophisticated capabilities (e.g. asynchronously manage queue of tasks that are, themselves, asynchronous; constrain degree of concurrency; etc.). This is all beyond the scope of this question.
you wrote
Ideally, I'd like to provide the newly-created DispatchWorkItem as an
additional parameter
you are wrong, to be able to cancel running task, you need a reference to it, not to the next which is ready to dispatch.
cancel() doesn't cancel running task, it only set internal "isCancel" flag by the thread-safe way, or remove the task from the queue before execution. Once executed, checking isCancel give you a chance to finish the job (early return).
import PlaygroundSupport
import Foundation
PlaygroundPage.current.needsIndefiniteExecution = true
let queue = DispatchQueue.global(qos: .background)
let prq = DispatchQueue(label: "print.queue")
var task: DispatchWorkItem?
func work(task: DispatchWorkItem?) {
sleep(1)
var d = Date()
if task?.isCancelled ?? true {
prq.async {
print("cancelled", d)
}
return
}
sleep(3)
d = Date()
prq.async {
print("finished", d)
}
}
for _ in 0..<3 {
task?.cancel()
let item = DispatchWorkItem {
work(task: task)
}
item.notify(queue: prq) {
print("done")
}
queue.asyncAfter(deadline: .now() + 0.5, execute: item)
task = item
sleep(1) // comment this line
}
in this example, only the very last job is really fully executed
cancelled 2018-12-17 23:49:13 +0000
done
cancelled 2018-12-17 23:49:14 +0000
done
finished 2018-12-17 23:49:18 +0000
done
try to comment the last line and it prints
done
done
finished 2018-12-18 00:07:28 +0000
done
the difference is, that first two execution never happened. (were removed from the dispatch queue before execution)

Periodically call an API with RxSwift

I'm trying to periodically (every 10 seconds) call an API that returns a Json object of model :
struct MyModel {
var messagesCount: Int?
var likesCount: Int?
}
And update the UI if messageCount or likesCount value changes.
I tried the Timer solution but i find it a little bit messy and i want a cleaner solution with RxSwift and RxAlamofire.
Any help is highly appreciated as i'm new to Rx.
Welcome to StackOverflow!
There's quite a lot of operators required for this, and I would recommend to look them up on the ReactiveX Operator page, which I check every time I forget something.
First off, ensure MyModel conforms to Decodable so it can be constructed from a JSON response (see Codable).
let willEnterForegroundNotification = NotificationCenter.default.rx.notification(.UIApplicationWillEnterForeground)
let didEnterBackgroundNotification = NotificationCenter.default.rx.notification(.UIApplicationDidEnterBackground)
let myModelObservable = BehaviorRelay<MyModel?>(value: nil)
willEnterForegroundNotification
// discard the notification object
.map { _ in () }
// emit an initial element to trigger the timer immediately upon subscription
.startWith(())
.flatMap { _ in
// create an interval timer which stops emitting when the app goes to the background
return Observable<Int>.interval(10, scheduler: MainScheduler.instance)
.takeUntil(didEnterBackgroundNotification)
}
.flatMapLatest { _ in
return RxAlamofire.requestData(.get, yourUrl)
// get Data object from emitted tuple
.map { $0.1 }
// ignore any network errors, otherwise the entire subscription is disposed
.catchError { _ in .empty() }
}
// leverage Codable to turn Data into MyModel
.map { try? JSONDecoder().decode(MyModel.self, from: $0) } }
// operator from RxOptional to turn MyModel? into MyModel
.filterNil()
.bind(to: myModelObservable)
.disposed(by: disposeBag)
Then, you can just continue the data stream into your UI elements.
myModelObservable
.map { $0.messagesCount }
.map { "\($0) messages" }
.bind(to: yourLabel.rx.text }
.disposed(by: disposeBag)
I didn't run this code, so there might be some typos/missing conversions in here, but this should point you in the right direction. Feel free to ask for clarification. If are really new to Rx, I recommend going through the Getting Started guide. It's great! Rx is very powerful, but it took me a while to grasp.
Edit
As #daniel-t pointed out, the background/foreground bookkeeping is not necessary when using Observable<Int>.interval.
CloakedEddy got real close with his answer and deserves upvotes. However he made it a little more complex than necessary. Interval uses a DispatchSourceTimer internally which will automatically stop and restart when the app goes to the background and comes back to the foreground. He also did a great job remembering to catch the error to stop the stream from unwinding.
I'm assuming the below code is in the AppDelegate or a high level Coordinator. Also, myModelSubject is a ReplaySubject<MyModel> (create it with: ReplaySubject<MyModel>.create(bufferSize: 1) that should be placed somewhere that view controllers have access to or passed down to view controllers.
Observable<Int>.interval(10, scheduler: MainScheduler.instance) // fire at 10 second intervals.
.flatMapLatest { _ in
RxAlamofire.requestData(.get, yourUrl) // get data from the server.
.catchError { _ in .empty() } // don't let error escape.
}
.map { $0.1 } // this assumes that alamofire returns `(URLResponse, Data)`. All we want is the data.
.map { try? JSONDecoder().decode(MyModel.self, from: $0) } // this assumes that MyModel is Decodable
.filter { $0 != nil } // filter out nil values
.map { $0! } // now that we know it's not nil, unwrap it.
.bind(to: myModelSubject) // store the value in a global subject that view controllers can subscribe to.
.disposed(by: bag) // always clean up after yourself.

ReactiveCocoa 4 - Delaying and filtering signal events

I am implementing a search textfield using ReactiveCocoa 4, and want to only hit the search API after no text has been inputted for X amount of time. I have done this previously by canceling previously scheduled and firing off a "executeSearch" selector in the textDidChange delegate method. This ensures that every time I type, any previously scheduled "executeSearch" selector is canceled, and a new one is scheduled to fire in X seconds.
I now want to do this same behavior, but from a signal producer bound to my input text. My current implementation is close, but not the same. This behavior merely throttles the text input event to only fire every 0.5 seconds, instead of canceling the previous event.
searchTextInput.producer.delay(0.3, onScheduler: RACScheduler.currentScheduler())
.throttle(0.5, onScheduler: RACScheduler.currentScheduler())
.producer.startWithNext({ [unowned self] searchText in
self.executeSearch(searchText)
})
I'm having a hard time sifting through the ReactiveCocoa 4 documentation to know which signal functions I should be using! Thank you!
You need use DateSchedulerType. For example:
textField.rac_textSignal()
.toSignalProducer()
.map { $0 as! String }
.flatMapError { _ in SignalProducer<String, NoError>.empty }
.throttle(2.0, onScheduler: QueueScheduler.mainQueueScheduler)
.filter { $0.isEmpty }
.startWithNext { text in
print("t: \(text)")
}
Also you can write your executeSearch as SignalProducer and use flatMap(.Latest) for create signal-chains.
And don't forget using mainQueueSheduler for get result to UI

Resources