Swift - Pass inout parameter to scheduledTimer - ios

Is there a way to pass an inout parameter through a scheduledTimer method? I want a specific value(disable Button until timer has run out).
func Timer(Hz:TimeInterval, Activate: inout Bool, time:Double=10){
let info = ProcessInfo.processInfo
let begin = info.systemUptime
var A=0
// do something
var diff = (info.systemUptime - begin)
print("Time runs\n")
let timer=Timer.scheduledTimer(withTimeInterval: Hz, repeats: true) { timer in
diff=(info.systemUptime - begin)
if (diff>=time){
timer.invalidate()
print("Finished")
Activate = !Activate
A=1
}
}
if (A==1){
print("Will never happen")
}
}
Var A is just there to show that I also tried a different approach but it didn't work

If the button is a reference type (ie, a class), which it will be if it's a UIKit or AppKit type, you can just pass it in, but there's a more general way of doing that is maybe better. Pass a closure in to act as a completion handler:
func Timer(Hz:TimeInterval, time:Double=10, onCompletion handler: #escaping () -> Void){
let info = ProcessInfo.processInfo
let begin = info.systemUptime
var A=0
// do something
var diff = (info.systemUptime - begin)
print("Time runs\n")
let timer=Timer.scheduledTimer(withTimeInterval: Hz, repeats: true) { timer in
diff=(info.systemUptime - begin)
if (diff>=time){
timer.invalidate()
print("Finished")
A=1
handler() // <- Call completion handler here
}
}
if (A==1){
print("Will never happen")
}
}
Then you can disable the button like this:
Timer(Hz: hz, time: time) { myButton.isEnabled = false /* or whatever else you want to do */ }

Your code seems unnecessarily complicated (though perhaps I have not understood the goal). Let's say we want to disable our button for 10 seconds, and then it should be enabled again. Then we would just say
self.myButton.isEnabled = false
self.toggleEnabled(self.myButton, after:10)
where toggleEnabled looks like this:
func toggleEnabled(_ enabled:UIButton, after t:Double) {
Timer.scheduledTimer(withTimeInterval: t, repeats: false) { _ in
enabled.isEnabled.toggle()
}
}
If you really wanted to, you could use a protocol to hide from toggleEnabled the fact that this is a UIButton. But that's hardly needed just to do something so simple.

Related

Cancel a timer / DispatchQueue.main.asyncAfter in SwiftUI Tap gesture

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

Set a timer with variable intervals in swift

I need to set a specific timer asynchronously after executing an action like this:
calling my function (sending http request)
10 seconds after, sending another request
20 seconds after 2), sending another one
40 seconds after 3), another one
then send every 60 seconds another one
At any moment, I must be able to cancel my timer. Firstable I thought using DispatchQueue, but I see several post saying that it's not possible to cancel it.
Some post suggest to use DispatchWorkItem ( how to stop a dispatchQueue in swift ) but I'm not sur it fit my need (unless adding a sleep(10,20,40,60...) in each loop but will it not impact asynchronous part?).
Another answer from this post suggest to use Timer instead ( scheduledTimerWithTimeInterval ) with repeats:false, and invalidate it after each loop, but I didn't undertand how to do the loop in this case. Actually, here's my code, that just send a request after 10 seconds:
private func start() {
timer?.invalidate()
if(self.PCount > self.Intervals.count){
self.value = self.pollingIntervals.count-1
} else {
self.Value = self.Intervals[self.pCount]
}
print("set timer with \(pollingValue) as interval")
timer = Timer.scheduledTimer(withTimeInterval: TimeInterval(pollingValue), repeats: false, block: { timer in
self.sessionManager.sendHit()
self.pollingCount+=1
})
}
The current goal is to do something like coroutine in Kotlin, like it work with this code :
private val Intervals = longArrayOf(10000,20000,40000,60000)
private var Count = 0
private fun start() {
currentJob = GlobalScope.launch {
while (true) {
delay(Intervals[if (Count > Intervals.size) Intervals.size - 1 else Count]) // 10,20,40 then every 60
session.sendHit()
pollingCount++
}
}
}
I'm not sure what solution is the most appropriate to my project
Here is a basic idea on how to approach the problem
struct RequestMananger {
var timers: [Timer] = []
mutating func startSequence() {
var delay = 10.0
sendRequest()
timers.append(scheduleTimer(delay))
delay += 20
timers.append(scheduleTimer(delay))
delay += 40
timers.append(scheduleTimer(delay))
delay += 60
timers.append(scheduleTimer(delay, repeats: true))
}
private func scheduleTimer(_ delay: TimeInterval, repeats: Bool = false) -> Timer {
return Timer.scheduledTimer(withTimeInterval: delay, repeats: false, block: { timer in
self.sendRequest()
})
}
func sendRequest() {
}
func cancelTimers() {
timers.forEach { $0.invalidate() }
}
}

Wait 50 run loop ticks in iOS

I would like to wait for the run loop to run and the screen to be rendered 50 times before performing an operation.
Is it necessary to use CAMediaTiming and a counter for that? Is there a way to hook into the NSRunLoop directly? Can I achieve this using 50 nested DispatchQueue.async calls like so?
import Dispatch
func wait(ticks: UInt, queue: DispatchQueue = DispatchQueue.main, _ handler: #escaping () -> Void) {
var ticks = ticks
func predicate() {
queue.async {
ticks -= 1
if ticks < 1 {
handler()
return
}
queue.async(execute: predicate)
}
}
predicate()
}
EDIT: if anyone is wondering, the snippet does work and it performs very well when we're talking about apps run loop.
You can use CADisplayLink, which is called every screen update.
let displayLink = CADisplayLink(target: self, selector: #selector(drawFunction(displayLink:)))
displayLink.add(to: .main, forMode: .commonModes)
In drawFunction (or predicate, etc.) we can subtract from ticks. When they reach 0, we've hit the 50th frame and invalidate displayLink.
var ticks = 50
#objc private func drawFunction(displayLink: CADisplayLink) {
doSomething()
ticks -= 1
if ticks == 0 {
displayLink.invalidate()
displayLink = nil
return
}
}
CADisplayLink can also provide the amount of time between frames. A similar discussion can be found here. If you're concerned about absolute accuracy here, you could calculate the time between frames. From the docs:
The duration property provides the amount of time between frames at the maximumFramesPerSecond. To calculate the actual frame duration, use targetTimestamp - timestamp. You can use this value in your application to calculate the frame rate of the display, the approximate time that the next frame will be displayed, and to adjust the drawing behavior so that the next frame is prepared in time to be displayed.
I think better solution to use CADisplayLink instead of Dispatch. Here is an example:
class Waiter {
let handler: () -> Void
let ticksLimit: UInt
var ticks: UInt = 0
var displayLink: CADisplayLink?
init(ticks: UInt, handler: #escaping () -> Void) {
self.handler = handler
self.ticksLimit = ticks
}
func wait() {
createDisplayLink()
}
private func createDisplayLink() {
displayLink = CADisplayLink(target: self,
selector: #selector(step))
displayLink?.add(to: .current,
forMode: RunLoop.Mode.default)
}
#objc private func step(displaylink: CADisplayLink) {
print(ticks)
if ticks >= ticksLimit {
displayLink?.invalidate()
handler()
}
ticks += 1
}
}
Here is an example of usage:
let waiter = Waiter(ticks: 50) {
print("Handled")
}
waiter.wait()

How to stop a Function from Running in Swift 3.0

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

How to hide UICollectionView Cell in ios after specific time. eg. Half Hour, 7 week etc

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)

Resources