I have a function that starts playing an animation that is running asynchronously (in the background). This animation is called indefinitely using a completion handler (see below). Is there a way to close this function upon pressing another button?
Here is my code:
func showAnimation() {
UIView.animate(withDuration: 1, animations: {
animate1(imageView: self.Anime, images: self.animation1)
}, completion: { (true) in
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
self.showAnimation() // replay first showAnimation
}
})
}
Then upon pressing another button we closeout the above function
showAnimation().stop();
Thanks
You can add a property to the class to act as a flag indicating whether the animation should be run or not.
var runAnimation = true
func showAnimation() {
if !runAnimation { return }
UIView.animate(withDuration: 1, animations: {
animate1(imageView: self.Anime, images: self.animation1)
}, completion: { (true) in
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
if runAnimation {
self.showAnimation() // replay first showAnimation
}
}
})
}
Then in the button handler to stop the animation you simply do:
runAnimation = false
Note that this does not stop the currently running 1 second animation. This just prevent any more animations.
There are a lot of ways to do this. The simplest is to have a Boolean property (which you should make properly atomic) that you check in your asyncAfter block, and don't just don't call showAnimation() again if it's true.
Another thing you can do, and what I like to do for more complex tasks, is to use OperationQueue instead of DispatchQueue. This allows you to cancel operations, either individually or all at once, or even suspend the whole queue (obviously don't suspend the main queue or call removeAllOperations() on it, though, since there may be other operations in there unrelated to your code).
You can provide a variable outside of your function, then observe its value and handle your task. I can give you a solution:
class SomeClass {
private var shouldStopMyFunction: Bool = false // keep this private
public var deadline: TimeInterval = 0
func stopMyFunction() {
shouldStopMyFunction = true
}
func myFunction(completionHanlder: #escaping (String)->()) {
// -------
var isTaskRunning = true
func checkStop() {
DispatchQueue.global(qos: .background).async {
if self.shouldStopMyFunction, isTaskRunning {
isTaskRunning = false
completionHanlder("myFunction is forced to stop! 😌")
} else {
//print("Checking...")
checkStop()
}
}
}
checkStop()
// --------
// Start your task from here
DispatchQueue.global().async { // an async task for an example.
DispatchQueue.global().asyncAfter(deadline: .now() + self.deadline, execute: {
guard isTaskRunning else { return }
isTaskRunning = false
completionHanlder("My job takes \(self.deadline) seconds to finish")
})
}
}
}
And implement:
let anObject = SomeClass()
anObject.deadline = 5.0 // seconds
anObject.myFunction { result in
print(result)
}
let waitingTimeInterval = 3.0 // 6.0 // see `anObject.deadline`
DispatchQueue.main.asyncAfter(deadline: .now() + waitingTimeInterval) {
anObject.stopMyFunction()
}
Result with waitingTimeInterval = 3.0: myFunction is forced to stop! 😌
Result with waitingTimeInterval = 6.0: My job takes 5.0 seconds to finish
Related
so I'm reading the Modern Concurrency book from raywenderlich.com and I assume the book must be outdated or something, I'm trying to run the closure insde the AsyncStream but it doesn't seem to get there, I'm still pretty new to this Async/Await thing, but when adding some breakpoints I can see my code is not getting there. This is my code and a screenshot with some warnings showing. I am not really familiar with what the warnings mean, just trying to learn all this new stuff, I would truly appreciate some help and is there a way to fix it with Swift 6? Thanks in advance!
Reference to captured var 'countdown' in concurrently-executing code; this is an error in Swift 6
Mutation of captured var 'countdown' in concurrently-executing code; this is an error in Swift 6
func countdown(to message: String) async throws {
guard !message.isEmpty else { return }
var countdown = 3
let counter = AsyncStream<String> { continuation in
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
continuation.yield("\(countdown)...")
countdown -= 1
}
}
for await countDownMessage in counter {
try await say(countDownMessage)
}
}
Timer.scheduleTimer requires that it be scheduled on a run loop. In practical terms, that means we would want to schedule it on the main thread’s run loop. So, you either call scheduleTimer from the main thread, or create a Timer and manually add(_:forMode:) it to RunLoop.main . See the Scheduling Timers in Run Loops section of the Timer documentation.
The easiest way would be to just isolate this function to the main actor. E.g.,
#MainActor
func countdown(to message: String) async throws { … }
There a few other issues here, too:
I would suggest defining the countdown variable within the AsyncStream:
#MainActor
func countdown(to message: String) async throws {
guard !message.isEmpty else { return }
let counter = AsyncStream<String> { continuation in
var countdown = 3
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
continuation.yield("\(countdown)...")
countdown -= 1
}
}
for await countDownMessage in counter {
try await say(countDownMessage)
}
}
The AsyncStream is never finished. You might want to finish it when it hits zero:
#MainActor
func countdown(to message: String) async throws {
guard !message.isEmpty else { return }
let counter = AsyncStream<String> { continuation in
var countdown = 3
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
continuation.yield("\(countdown)...")
// presumably you want this countdown timer to finish when it hits zero
guard countdown > 0 else {
timer.invalidate()
continuation.finish()
return
}
// otherwise, decrement and carry on
countdown -= 1
}
}
for await countDownMessage in counter {
try await say(countDownMessage)
}
}
There should be a continuation.onTermination closure to handle cancelation of the asynchronous sequence.
#MainActor
func countdown(to message: String) async throws {
guard !message.isEmpty else { return }
let counter = AsyncStream<String> { continuation in
var countdown = 3
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
continuation.yield("\(countdown)...")
// presumably you want this countdown timer to finish when it hits zero
guard countdown > 0 else {
timer.invalidate()
continuation.finish()
return
}
// otherwise, decrement and carry on
countdown -= 1
}
continuation.onTermination = { _ in
timer.invalidate()
}
}
for await countDownMessage in counter {
try await say(countDownMessage)
}
}
Going back to the original question (why this is not running), I personally would avoid the use of Timer in conjunction with Swift concurrency at all. A GCD timer would be better, as it doesn’t require a RunLoop. Even better, I would advise Task.sleep. Needless to say, that is designed to work with Swift concurrency, and also is cancelable.
I personally would suggest something like:
func countdown(to message: String) async throws {
guard !message.isEmpty else { return }
let counter = AsyncStream<String> { continuation in
let task = Task {
for countdown in (0...3).reversed() {
try await Task.sleep(for: .seconds(1))
continuation.yield("\(countdown)...")
}
continuation.finish()
}
continuation.onTermination = { _ in
task.cancel()
}
}
for await countDownMessage in counter {
try await say(countDownMessage)
}
}
I am trying to cancel a delayed execution of a function running on the main queue, in a tap gesture, I found a way to create a cancellable DispatchWorkItem, but the issue I have is that it's getting created every time while tapping, and then when I cancel the execution, I actually cancel the new delayed execution and not the first one.
Here is a simpler example with a Timer instead of a DispatchQueue.main.asyncAfter:
.onTapGesture {
isDeleting.toggle()
let timer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { timer in
completeTask()
}
if !isDeleting {
timer.invalidate()
}
}
completeTask:
private func completeTask() {
tasksViewModel.deleteTask(task: task) // task is declared above this func at the top level of the struct and so is tasksViewModel, etc.
guard let userID = userViewModel.id?.uuidString else { return }
Task {
//do some async stuff
}
}
As you can see if I click it once the timer fires, but if I click it again, another timer fires and straight away invalidates, but the first timer is still running.
So I have to find a way to create only one instance of that timer.
I tried putting it in the top level of the struct and not inside the var body but the issue now is that I can't use completeTask() because it uses variables that are declared at the same scope.
Also, can't use a lazy initialization because it is an immutable struct.
My goal is to eventually let the user cancel a timed task and reactivate it at will on tapping a button/view. Also, the timed task should use variables that are declared at the top level of the struct.
First of all you need to create a strong reference of timer on local context like so:
var timer: Timer?
and then, set the timer value on onTapGesture closure:
.onTapGesture {
isDeleting.toggle()
self.timer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { timer in
completeTask()
}
if !isDeleting {
timer.invalidate()
}
}
and after that you can invalidate this Timer whenever you need by accessing the local variable timer like this:
func doSomething() {
timer?.invalidate()
}
that is my solution mb can help you
var timer: Timer?
private func produceWorkItem(withDelay: Double = 3) {
scrollItem?.cancel()
timer?.invalidate()
scrollItem = DispatchWorkItem.init { [weak self] in
self?.timer = Timer.scheduledTimer(withTimeInterval: withDelay, repeats: false) { [weak self] _ in
self?.goToNextPage(animated: true, completion: { [weak self] _ in self?.produceWorkItem() })
guard let currentVC = self?.viewControllers?.first,
let index = self?.pages.firstIndex(of: currentVC) else {
return
}
self?.pageControl.currentPage = index
}
}
scrollItem?.perform()
}
for stop use scrollItem?.cancel()
for start call func
I have this object:
lockWallTask = DispatchWorkItem(block: {
self.lockWall()
})
DispatchQueue.main.asyncAfter(deadline: .now() + 10, execute: lockWallTask)
So it is executing after 10 seconds. However, I am trying to pause this item and then resume it. For instance:
I would pause the item after 3 seconds, meaning there is 7 seconds left for execution. I do other stuff for like 5 minutes and then I resume the item and there is still 7 seconds left for execution. I was trying to achieve this life this:
DispatchQueue.resume(task)
DispatchQueue.suspend(task)
However, I was given this compile error:
I don't understand that error. The variable task is of type 'DispatchWorkItem'
How would I achieve pausing, or suspending, a DispatchWorkItem and then resuming it?
You need to use DispatchSource timer where you can pause, resume and stop and perform your operation accordingly.
var timer: DispatchSource?
func resumeTimer() {
guard let timer = DispatchSource.makeTimerSource(flags: DispatchSource.TimerFlags(rawValue: 0), queue: mainQueue) as? DispatchSource else { return }
timer.setEventHandler {
mainQueue.async(execute: { [weak self] in
self?.runTimer()
})
}
timer.scheduleRepeating(deadline: .now(), interval: .seconds(10), leeway: .seconds(1))
timer.resume()
}
func pausedTimer() {
if let timer = timer {
timer.suspend()
}
}
func stoppedTimer() {
if let timer = timer {
timer.cancel()
}
}
I have a UIControl that calls a function after 0.5 seconds depending on how many times the user presses it.
(Eg 1 press calls f1(), 2 presses calls f2(), 3 presses calls f3())
So basically I need to set a timer when a user presses the Control. If the Control is not pressed for 0.5 seconds then create a dialog. I have tried using a DispatchQueue, but when it gets to the point of making the dialog, it takes several seconds. I think it is because it is being called concurrently instead of on the main thread (apologies if poor terminology).
self.operationQueue.cancelAllOperations() //To cancel previous queues
self.mainAsyncQueue = DispatchQueue(label: "bubblePressed" + String(describing: DispatchTime.now()), qos: DispatchQoS.default, attributes: DispatchQueue.Attributes.concurrent)
let time = DispatchTime.now()
self.currentTime = time
self.mainAsyncQueue!.asyncAfter(deadline: time + 0.5){
guard self.currentTime == time else {
return
}
let tempOperation = BlockOperation(block:{
self.displayDialog()
})
self.operationQueue.addOperation(tempOperation)
}
operationQueue and mainAsycQueue are defined in viewDidLoad as
self.currentTime = DispatchTime.now()
self.operationQueue = OperationQueue()
How can I call my function displayDialog() in the main thread so that it loads faster?
Based on the question title, answer is:
let deadlineTime = DispatchTime.now() + .seconds(1)
DispatchQueue.main.asyncAfter(deadline: deadlineTime) {
//update UI here
self.displayDialog()
}
or
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.displayDialog()
}
I don't think it needs to be anywhere near that complicated. You can just use a Timer;
class MyClass: UIViewController {
var tapCount = 0
var tapTimer: Timer?
#IBAction tapped(_ sender: Any) {
if tapCount < 3 {
tapCount += 1
tapTimer?.invalidate()
tapTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false, block: { (timer) in
switch (self.tapCount) {
case 1:
self.f1()
case 2:
self.f2()
case 3:
self.f3()
default:
// Hmm, shouldn't happen
}
self.tapCount = 0
})
}
}
The timer will be scheduled on the main queue by default, so there is no need to dispatch anything on the main queue specifically
Use below func, it executes the func in the main thread and no other action will perform during this execution.
DispatchQueue.main.async {
self.displayDialog()
}
I going to hide a cell after half an hour time interval from populating time. If app is in not running state then also performs the same operation. Please Help me. Thank's in advance.
Here is the function delay that can help you to process delays even in background state (delay will be processed immediately after app becomes active if time is ok). And with this code you can easy cancel this delay if needed But this solution will not work for the situation when the app is not running (for this case I will give another solution):
import Foundation
import UIKit
typealias dispatch_cancelable_closure = (_ cancel : Bool) -> Void
#discardableResult
func delay(_ time:TimeInterval, closure: #escaping ()->Void) -> dispatch_cancelable_closure? {
// DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(time * 1000))) {
// closure()
// }
//
// return nil
func dispatch_later(_ clsr:#escaping ()->Void) {
DispatchQueue.main.asyncAfter(
deadline: DispatchTime.now() + Double(Int64(time * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: clsr)
}
var closure:(()->Void)? = closure
var cancelableClosure:dispatch_cancelable_closure?
let delayedClosure:dispatch_cancelable_closure = { cancel in
if closure != nil {
if (cancel == false) {
// DispatchQueue.main.async {
// closure?()
// }
DispatchQueue.main.async(execute: closure!)
// DispatchQueue.main.async(execute: closure as! #convention(block) () -> Void);
}
}
closure = nil
cancelableClosure = nil
}
cancelableClosure = delayedClosure
dispatch_later {
if let delayedClosure = cancelableClosure {
delayedClosure(false)
}
}
return cancelableClosure;
}
func cancel_delay(_ closureToCancel:dispatch_cancelable_closure?) {
if closureToCancel != nil {
closureToCancel!(true)
}
}
But for case when your application is not running, you need to save the time when you want to remove the cell in NSDefaults before application comes into background, and when application becomes active you can use this delay function to set the rest of the time (or if time expired you can remove the cell immidiatly)