Async data loading swift - swift

I got a function such as scrollViewDidScroll that can trigger many times. And I need to call function loadMoreDataFromRemoteServerIfNeed only single time. How could I do this more elegantly without using any "flag" variables. Maybe I should use DispathGroup|DispatchWorkItem?
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let yOffset = scrollView.contentOffset.y
if yOffset > offset {
loadMoreDataFromRemoteServerIfNeed()
}
}
func loadMoreDataFromRemoteServerIfNeed() {
DispatchQueue.global(qos: .background).async {
sleep(2)
DispatchQueue.main.async {
// <Insert New Data>
}
}
}

The thing that you are trying to describe — "Do this, but only if you are not told to do it again any time in the next 2 seconds" — has a name. It's called debouncing. This is a well-solved problem in iOS programming, so now that you know its name, you can do a search and find some of the solutions.
While I'm here telling you about this, here's a solution you might not know about. Debouncing is now built in to iOS functionality! Starting in iOS 13, it's part of the Combine framework. I'm now using Combine all over the place: instead of notifications, instead of GCD, instead of Timer objects, etc. It's great!
Here's a Combine-based solution to this type of problem. Instead of a scroll view, suppose we have a button hooked up to an action handler, and we don't want the action handler to do its task unless 2 seconds has elapsed since the last time the user tapped the button:
var pipeline : AnyCancellable?
let pipelineStart = PassthroughSubject<Void,Never>()
#IBAction func doButton(_ sender: Any) {
if self.pipeline == nil {
self.pipeline = pipelineStart
.debounce(for: .seconds(2), scheduler: DispatchQueue.main)
.sink { [weak self] _ in self?.doSomething() }
}
self.pipelineStart.send()
}
func doSomething() {
print("I did it!")
}
I'm sure you can readily see how to adapt that to your own use case:
var pipeline : AnyCancellable?
let pipelineStart = PassthroughSubject<Void,Never>()
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let yOffset = scrollView.contentOffset.y
if yOffset > offset {
if self.pipeline == nil {
self.pipeline = pipelineStart
.debounce(for: .seconds(2), scheduler: DispatchQueue.main)
.sink { [weak self] _ in self?.loadMoreDataFromRemoteServerIfNeed()
}
self.pipelineStart.send()
}
}
func loadMoreDataFromRemoteServerIfNeed() {
// <Insert New Data>
}

You can create a flag from DispatchWorkItem to observe loading state e.g.:
var item: DispatchWorkItem?
func loadMoreDataFromRemoteServerIfNeed() {
assert(Thread.isMainThread)
guard item == nil else { return }
item = DispatchWorkItem {
print("loading items")
Thread.sleep(forTimeInterval: 2)
DispatchQueue.main.async {
item = nil
print("insert items")
}
}
DispatchQueue.global().async(execute: item!)
}
NOTE: to synchronise item var you must change its value on the same thread for instance the main thread.

Yes, you could use DispatchWorkItem, keep a reference to the old one, and cancel prior one if necessary. If you were going to do that, I might consider Operation, too, as that handles cancelation even more gracefully and has other advantages.
But that having been said, given that the work that you are dispatching is immediately sleeping for two seconds, this might suggest a completely different pattern, namely a Timer. You can schedule your timer, invalidating previously scheduled timers, if any:
weak var timer: Timer?
func loadMoreDataFromRemoteServerIfNeed() {
// cancel old timer if any
timer?.invalidate()
// schedule what you want to do in 2 seconds
timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { _ in
// <Insert New Data>
}
}
FWIW, if you ever find yourself sleeping, you should general consider either timers or asyncAfter. This avoids tying up the global queue’s worker thread. Sleeping is an inefficient pattern.
In this case, keeping a weak reference to the prior timer (if any) is probably the best pattern.

Related

DispatchQueue.main.asyncAfter hanging on repeat, but does not hang when using sleep

I am trying to create a Robotic Process Automation tool for Macos using Swift. Users create an Automation that is an array of Step objects and then play it. One of the subclasses of Step is Pause which is supposed to pause the execution for a given number of seconds.
For some reason, execution hangs when I use the DispatchQueue.main.asyncAfter() method in the Pause class. Usually the first run through the automation is fine, but when it goes to repeat, it eventually hangs for much longer. The error goes away when I use sleep() instead.
The other weird thing about this bug is when I open Xcode to try and see what is happening, the hang resolves and execution continues. I am wondering if the process enters background somehow and then the DispatchQueue.main.asyncAfter() doesn't work. I have tried to change the Info.plist "Application does not run in background" to YES, but this doesn't have any effect.
The problem with using sleep() is it blocks the UI thread so users can't stop the automation if they need to. I have tried lots of different variations of threading with DispatchQueue, but it always seems to hang somewhere on repeat execution. I have also tried using a Timer.scheduledTimer() instead of DispatchQueue but that hangs as well. I'm sure I'm missing something simple, but I can't figure it out.
Creating the Step Array and Starting Automation
class AutomationPlayer {
static let shared = AutomationPlayer()
var automation: Automation?
var stepArray: [Step] = []
func play() {
// Create array of steps
guard let steps = automation?.steps, let array = Array(steps) as? [Step] else {
return
}
// If the automation repeats, add more steps to array.
for _ in 0..<(automation?.numberOfRepeats ?? 1) {
for (index, step) in array.enumerated() {
stepArray.append(step)
}
}
// Add small delay to allow window to close before execution.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { [weak self] in
self?.execute(index: 0)
}
}
private func execute(index: Int) {
let step = stepArray[index]
executeStep(step: step) { [weak self] success, error in
guard error == nil else { return }
let newIndex = index + 1
if newIndex < self?.stepArray.count ?? 0 {
//Need a small delay between steps otherwise execution is getting messed up.
usleep(400000)
self?.execute(index: newIndex)
} else {
self?.stepArray = []
}
}
}
private func executeStep(step: Step?, completionHandler: #escaping (Bool, Error?) -> Void) -> Void {
step?.execute(completionHandler: { [weak self] success, error in
guard error == nil else {
completionHandler(false, error)
return
}
completionHandler(true, nil)
})
}
Pause Class
#objc(Pause)
public class Pause: Step {
override func execute(completionHandler: #escaping (Bool, Error?) -> Void) {
print("Pause for: \(self.time) seconds")
// This will eventually hang when the automation repeats itself
DispatchQueue.main.asyncAfter(deadline: .now() + Double(self.time)) {
completionHandler(true, nil)
})
// This will also hang
Timer.scheduledTimer(withTimeInterval: self.time, repeats: false) { timer in
completionHandler(true, nil)
}
// If I use this instead, the automation repeats just fine
sleep(UInt32(self.time))
completionHandler(true, nil)
}
}
So I think I figured it out. MacOS was putting my app into AppNap after a certain period of time which would cause the DispatchQueue.main.async() to stop working. For some reason, AppNap does not affect delays when you use sleep()
I found an answer here
This answer was a little older. I am using SwiftUI to build my mac app so I added this my #main struct
#main
struct Main_App: App {
#State var activity: NSObjectProtocol?
var body: some Scene {
WindowGroup("") {
MainWindow()
.onAppear {
activity = ProcessInfo().beginActivity(options: .userInitiated, reason: "Good Reason")
}
}
}
This seems to prevent the app from going into AppNap and the automation continues. It's pretty ugly, but it works.

How to add DispatchQueue delay in swift while loop?

I'm trying to create a delay inside a while loop. I'm fairly new to this and it's currently just not working. It never fires even once with the dispatch delay, but if I remove the delay it fires repeatedly.
Basically what I'm doing is checking if the velocity of nodes in a SKScene is still moving, if they're still moving, don't end the game. But once they've slowed down, end the game.
func RemainingNodeCheck (complete:() -> Void) {
CountVelocites()
if (IdleVelocity.max()!.isLess(than: 1.0)) {
complete()
} else {
print("Velocity too high, begin wait...")
while !(IdleVelocity.max()?.isLess(than: 1.0))! {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1)) {
print("Second passed")
self.CountVelocites()
}
if (IdleVelocity.max()!.isLess(than: 1.0)) {
break
}
}
print("Velocity Calmed down")
complete()
}
}
I believe this might be something to do with threads? Or it's actually just telling the delay to begin waiting for one second so many times that it never gets to call?
UPDATE: I would use a timer, but the RemaingNodeCheck is being called from another part and it's waiting for RemainingNodeCheck to send back complete()
You never want to "wait". But you can set up a repeating timer that checks for some condition, and if so, calls the complete closure (invalidating the timer, if you want). E.g.
class ViewController: UIViewController {
var idleVelocity: ...
weak var timer: Timer?
deinit {
timer?.invalidate()
}
func startCheckingVelocity(complete: #escaping () -> Void) {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in
guard let self = self, let maxVelocity = self.idleVelocity.max() else { return }
if maxVelocity < 1 {
timer.invalidate()
complete()
return
}
print("velocity too high...")
}
}
}

Wait itself within endless loop, but user can cancel every time

In Swift 3 I have a loop which can be canceled by user pressing a button. Within the loop some checks are made. After the check, the task can sleep for a minute. But when calling the task with
let delayQueue = DispatchQueue(label: "com.myApp.queue3", qos: .utility)
let additionalTime: DispatchTimeInterval = .seconds(3)
repeat {
delayQueue.asyncAfter(deadline: .now() + additionalTime) { self.update() }
} while !self.stop
the loop itself needs to run all the time waiting for the user
"stop", indicates, that user clicked on stop button.
Is that waste of CPU power? How could I avoid this loop to be done all the time?
You should use Timer instead.
var timer: Timer?
let timeInterval: TimeInterval = 3
func didPressCancelButton() {
timer?.invalidate()
}
func beginUpdates() {
timer = Timer.scheduledTimer(
timeInterval: timeInterval,
target: self,
selector: #selector(self.update),
userInfo: nil,
repeats: true
);
}
func update() {
print("Updated")
}
Instead of delaying execution in thread with an outer loop you can put your loop in thread instead and make it to sleep.
import Foundation
class YourUpdatingClass {
private let updateQueue: OperationQueue
init() {
updateQueue = OperationQueue()
updateQueue.name = "com.myApp.queue3"
updateQueue.qualityOfService = .utility
}
private var updateOperation: BlockOperation?
#IBAction func startUpdating() {
guard updateOperation == nil else {
// In case if updating already started
return
}
updateOperation = BlockOperation { [weak self] in
while true {
Thread.sleep(forTimeInterval: 3)
self?.update()
}
}
updateQueue.addOperation(updateOperation!) // we just created updateOperation, so we can use `!`, but use it with caution
}
#IBAction func stopUpdating() {
updateOperation?.cancel()
updateOperation = nil
}
private func update() {
print("update") // Whatever your update does
}
}
You updating is contained in eternal while loop which takes a nap every 3 seconds.
Stopping is managed by cancelling operation, instead of checking some var in the loop.

How do I run an asynchronous thread that only runs as long as the view that uses it is presented?

How do I run an asynchronous thread that only runs as long as the view that uses it is presented?
I want the view to run this asynchronous thread. However, as soon as the view disappears, I want that thread to stop running. What's the best way to do this? I'm not sure where to start and might be thinking about this the wrong way. Nevertheless, what I described is how I want it to behave to the user.
You can use NSOperation to achieve what you want, NSOperation and NSOperationQueue are built on top of GCD. As a very general rule, Apple recommends using the highest-level abstraction, and then dropping down to lower levels when measurements show they are needed.
For example, You want to download images asynchronously when the view is loaded and cancel the task when the view is disappeared. First create a ImageDownloader object subclass to NSOperation. Notice that we check if the operation is cancelled twice, this is because the NSOperation has 3 states: isReady -> isExecuting -> isFinish and when the operation starts executing, it won't be cancelled automatically, we need to do it ourself.
class ImageDownloader: NSOperation {
//1
var photoRecord: NSURL = NSURL(string: "fortest")!
//2
init(photoRecord: NSURL) {
self.photoRecord = photoRecord
}
//3
override func main() {
//4
if self.cancelled {
return
}
//5
let imageData = NSData(contentsOfURL:self.photoRecord)
//6
if self.cancelled {
return
}
}
}
Then you can use it like: downloader.cancel(), downloader.start(). Notice that we need to check if the operation is cancelled in the completion block.
import UIKit
class ViewController: UIViewController {
let downloder = ImageDownloader(photoRecord: NSURL(string: "test")!)
override func viewDidLoad() {
super.viewDidLoad()
downloder.completionBlock = {
if self.downloder.cancelled {
return
}
print("image downloaded")
}
//Start the task when the view is loaded
downloder.start()
}
override func viewWillDisappear(animated: Bool) {
//Cancel the task when the view will disappear
downloder.cancel()
}
}
Once DetailViewController is presented, the asyncOperation method will be executed asynchronously.
Note: currently the asyncOperation method is executed every second so if you want the method to be called only once, you must change the repeats property to false.
class DetailViewController: UIViewController {
// timer that will execute
// asynchronously an operation
var timer: NSTimer!
// counter used in the async operation.
var counter = 0
// when view is about to appear
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
// setting up the timer
timer = NSTimer.scheduledTimerWithTimeInterval(
1.0,
target: self,
selector: #selector(asyncOperation),
userInfo: nil,
repeats: true //set up false if you don't want the operation repeats its execution.
)
}
// when view is about to disappear
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
// stopping the timer
timer.invalidate()
}
// async operation that will
// be executed
func asyncOperation() {
counter += 1
print("counter: \(counter)")
}
}
Source: https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSTimer_Class/
Result:

How do I cancel a completion handler?

I want to enhance the code below: when i click the "submitData" button, the added code should cancel the completion handler.
func returnUserData(completion:(result:String)->Void){
for index in 1...10000 {
print("\(index) times 5 is \(index * 5)")
}
completion(result: "END");
}
func test(){
self.returnUserData({(result)->() in
print("OK")
})
}
#IBAction func submintData(sender: AnyObject) {
self.performSegueWithIdentifier("TestView", sender: self)
}
Can you tell me how to do this?
You can use NSOperation subclass for this. Put your calculation inside the main method, but periodically check cancelled, and if so, break out of the calculation.
For example:
class TimeConsumingOperation : NSOperation {
var completion: (String) -> ()
init(completion: (String) -> ()) {
self.completion = completion
super.init()
}
override func main() {
for index in 1...100_000 {
print("\(index) times 5 is \(index * 5)")
if cancelled { break }
}
if cancelled {
completion("cancelled")
} else {
completion("finished successfully")
}
}
}
Then you can add the operation to an operation queue:
let queue = NSOperationQueue()
let operation = TimeConsumingOperation { (result) -> () in
print(result)
}
queue.addOperation(operation)
And, you can cancel that whenever you want:
operation.cancel()
This is, admittedly, a fairly contrived example, but it shows how you can cancel your time consuming calculation.
Many asynchronous patterns have their built-in cancelation logic, eliminating the need for the overhead of an NSOperation subclass. If you are trying to cancel something that already supports cancelation logic (e.g. NSURLSession, CLGeocoder, etc.), you don't have to go through this work. But if you're really trying to cancel your own algorithm, the NSOperation subclass handles this quite gracefully.