Creating a sequential publisher in Swift Combine - ios

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.

Related

Premature completion of publisher in flatMap in Combine

I have this minimal example:
import UIKit
import Combine
var values = [1,2,3,4,5]
var cancel = values.publisher
.delay(for: 0.1, scheduler: DispatchQueue.global())
.print()
.flatMap() { i in
[i].publisher.first()
}
.sink { completion in
print("Received Completion: \(completion)")
} receiveValue: { v in
print("Received Value: \(v)")
}
My expectation is that the source publisher emits the values from 1 to 5 into the stream. Each number gets transformed into (just for the sake of it) a new publisher that emits exactly the first value and then completes. Since this is done with each number, I would expect that all values reach the sink. This is not the case, however. Output looks like this:
request unlimited
receive value: (1)
Received Value: 1
receive value: (2)
Received Value: 2
receive value: (4)
Received Value: 4
receive finished
Received Completion: finished
receive value: (3)
receive value: (5)
In fact, only 3 values reach the sink before the completion event arrives. Why is this? The documentation states:
successful completion of the new Publisher does not complete the overall stream.
Even more curious, when you replace .flatMap() for .flatMap(maxPublishers: .max(1)) and add a .share() to the original source publisher only the first value makes it to the sink.
Any pointers are much appreciated!
Your use of DispatchQueue.global() is the problem. The values.publisher sends all of its outputs, and its completion, downstream to the delay operator as soon as values.publisher receives the subscription. The delay operator schedules six blocks (five for the output numbers and one for the completion) to run on DispatchQueue.global() 0.1 seconds later.
DispatchQueue.global() is a concurrent queue. This means that it can run any number of blocks simultaneously. So there is no guarantee which of the six scheduled blocks will finish first.
It is in general a bad idea to use a concurrent queue as a Combine scheduler. You should use a serial queue instead. Here's a simple example:
var cancel = values.publisher
.delay(for: 0.1, scheduler: DispatchQueue(label: "my queue"))
.print()
.flatMap() { i in
[i].publisher.first()
}
.sink { completion in
print("Received Completion: \(completion)")
} receiveValue: { v in
print("Received Value: \(v)")
}
But you probably want to create the queue once and store it in a property, so you can use it with multiple publishers.
If you actually want the outputs to be delivered on the main queue (perhaps because you're going to use them to update a view), you should just use DispatchQueue.main.

How to write iOS Unit Testing in SwiftUI when switching from background thread to main thread

Once my background operation completed i need to call handleError function. Since isToast, errorMessage are published variables i need to put in main thread. I wrote a func for test test__Failure() but before simulateRequestFailure complete, this line is executed in function XCTAssertTrue(self.viewModel.isToast). How to put wait, delay for few seconds
#Published var isToast: Bool = false
#Published var eMessage: String = ""
func handleError() {
DispatchQueue.main.async {
self.isToast = true
self.eMessage = “Test message”
}
}
func test__Failure() {
// Some simulate response which call handleError
self.simulateRequestFailure()
XCTAssertTrue(self.vm.isToast)
}
You can delay verification and check it on main thread as well, something like this:
let expectation = XCTestExpectation()
self.simulateRequestFailure()
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) {
XCTAssertTrue(self.vm.isToast)
expectation.fulfill()
}
wait(for: [expectation], timeout: 10.0)
What this does:
Expectation allows to synchronize test thread with main thread. I.e. test will not complete until either expectation.fulfill() happens, or 10 seconds expire (you can of course change 10 sec to whatever)
simulateRequestFailure() runs on main thread asynchronously, so we let it run and schedule verification on the same thread, but a bit delayed (by 1 sec, but you can change it to whatever makes sense)

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)

Get the latest result from DispatchGroup wait

Problem Desctiption:
I want to do a bunch of asynchronous tasks by 'DispatchGroup' and when all of them finished it returned the result. In addition, I want to set timeout that limits the process and send me back the successful results by that time. I used the following structure:
Code Block
let myGroup = DispatchGroup()
var result = [Data]()
for i in 0 ..< 5 {
myGroup.enter()
Alamofire.request("https://httpbin.org/get", parameters: ["foo": "bar"]).responseJSON { response in
print("Finished request \(i)")
result.append(response.data)
myGroup.leave()
}
}
// Timeout for 10 seconds
myGroup.wait(timeout: DispatchTime(uptimeNanoseconds: 10000000000))
myGroup.notify(queue: .main) {
return result
}
How can I get the latest result if timeout happened?
Ok, so you are correctly using the enter/leave functionality of the DispatchGroup, but are having trouble with how to access the results of these. I think you are going wrong by trying to use both wait and notify, these two functions provide two different pieces of functionality not usually used together. After having setup up your work items, as you have done, you have two options:
The wait approach
This function blocks the calling queue and wait synchronously for either, the passed in wall time to elapse, or all work items in the group to leave. Because it is blocking the caller, it is important to always have a timeout in this function.
The notify approach
The function takes a target queue, and a block to be run when all work items in your group have completed. Here you are basically asking the system to notify you, asynchronously once all work items have been completed. Since this is asynchronous we are usually less worried about the timeout, it's not blocking anything.
Asynchronous wait (this appears to be what you want?)
If, as it seems you do, we want to be notified once all work items are complete, but also have a timeout, we have to do this ourselves, and it's not all that tricky. We can add a simple extension for the DispatchGroup class...
extension DispatchGroup {
func notifyWait(target: DispatchQueue, timeout: DispatchTime, handler: #escaping (() -> Void)) {
DispatchQueue.global(qos: .default).async {
_ = self.wait(timeout: timeout)
target.async {
handler()
}
}
}
}
This simple function dispatches asynchronously on a global background queue, then calls wait, which will wait for all work items to complete, or the specified timeout, whichever comes first. Then it will call back to your handler on the specified queue.
So that's the theory, how can you use this. We can keep your initial setup exactly the same
let myGroup = DispatchGroup()
var result = [Data]()
for i in 0 ..< 5 {
myGroup.enter()
Alamofire.request("https://httpbin.org/get", parameters: ["foo": "bar"]).responseJSON { response in
print("Finished request \(i)")
result.append(response.data)
myGroup.leave()
}
}
and then use our new function to wait for the end
myGroup.notifyWait(target: .main,
timeout: DispatchTime.now() + 10) {
// here you can access the `results` list, with any data that has
// been appended by the work items above before the timeout
// was reached
}

Show an activity indicator after x seconds, using RxSwift

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.

Resources