Wait 50 run loop ticks in iOS - 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()

Related

Swift - Pass inout parameter to scheduledTimer

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.

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() }
}
}

iOS equivalent of Androids ValueAnimator

I would like something that mimics the behaviour in iOS of the ValueAnimator in Android. Under normal circumstances I would use a UIView animation, but unfortunately the objects value im trying to interpolate over time isnt working with a normal animation.
In my particular case im using a Lottie animation, and trying to change the progress of the animation via a UIView animation, but instead of the animation changing the value of the progress over time, it just jumps to the final value. eg:
let lottieAnim = ...
lottieAnim.animationProgress = 0
UIView.animate(withDuration: 2) {
lottieAnim.animationProgress = 1
}
This example does not animate the lottie animation over time, but simply jumps to the end. I know lottie has methods to play the animation, but Im trying to use a custom animation curve to set the progress (its a progress animation that has no finite duration) which is why I need to use a UIView animation to interpolate the value.
I need something that updates intermittently to mimic an animation, the first thing that comes to mind is Android ValueAnimator class.
I put together a class that will mimic a ValueAnimator from android somewhat, and will allow you to specify your own animation curve if need be (default is just linear)
Simple usage
let valueAnimator = ValueAnimator(duration: 2) { value in
animationView.animationProgress = value
}
valueAnimator.start()
Advanced usage
let valueAnimator = ValueAnimator(from: 0, to: 100, duration: 60, animationCurveFunction: { time, duration in
return atan(time)*2/Double.pi
}, valueUpdater: { value in
animationView.animationProgress = value
})
valueAnimator.start()
You can cancel it at any point as well using:
valueAnimator.cancel()
Value Animator class in Swift 5
// swiftlint:disable:next private_over_fileprivate
fileprivate var defaultFunction: (TimeInterval, TimeInterval) -> (Double) = { time, duration in
return time / duration
}
class ValueAnimator {
let from: Double
let to: Double
var duration: TimeInterval = 0
var startTime: Date!
var displayLink: CADisplayLink?
var animationCurveFunction: (TimeInterval, TimeInterval) -> (Double)
var valueUpdater: (Double) -> Void
init (from: Double = 0, to: Double = 1, duration: TimeInterval, animationCurveFunction: #escaping (TimeInterval, TimeInterval) -> (Double) = defaultFunction, valueUpdater: #escaping (Double) -> Void) {
self.from = from
self.to = to
self.duration = duration
self.animationCurveFunction = animationCurveFunction
self.valueUpdater = valueUpdater
}
func start() {
displayLink = CADisplayLink(target: self, selector: #selector(update))
displayLink?.add(to: .current, forMode: .default)
}
#objc
private func update() {
if startTime == nil {
startTime = Date()
valueUpdater(from + (to - from) * animationCurveFunction(0, duration))
return
}
var timeElapsed = Date().timeIntervalSince(startTime)
var stop = false
if timeElapsed > duration {
timeElapsed = duration
stop = true
}
valueUpdater(from + (to - from) * animationCurveFunction(timeElapsed, duration))
if stop {
cancel()
}
}
func cancel() {
self.displayLink?.remove(from: .current, forMode: .default)
self.displayLink = nil
}
}
You could probably improve this to be more generic but this serves my purpose.

Swift 3 - How to make timer work in background

i am trying to do an application which can make a timer run in background.
here's my code:
let taskManager = Timer.scheduledTimer(timeInterval: 10, target: self, selector: #selector(self.scheduleNotification), userInfo: nil, repeats: true)
RunLoop.main.add(taskManager, forMode: RunLoopMode.commonModes)
above code will perform a function that will invoke a local notification.
this works when the app is in foreground, how can i make it work in the background?
i tried to put several print lines and i saw that when i minimize (pressed the home button) the app, the timer stops, when i go back to the app, it resumes.
i wanted the timer to still run in the background. is there a way to do it?
here's what i want to happen:
run app -> wait 10 secs -> notification received -> wait 10 secs -> notification received -> and back to wait and received again
that happens when in foreground. but not in background. pls help.
you can go to Capabilities and turn on background mode and active Audio. AirPlay, and picture and picture.
It really works . you don't need to set DispatchQueue .
you can use of Timer.
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { (t) in
print("time")
}
Swift 4, Swift 5
I prefer to not run timer on background task, just compare a Date seconds between applicationDidEnterBackground and applicationWillEnterForeground.
func setup() {
NotificationCenter.default.addObserver(self, selector: #selector(applicationDidEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(applicationWillEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
}
#objc func applicationDidEnterBackground(_ notification: NotificationCenter) {
appDidEnterBackgroundDate = Date()
}
#objc func applicationWillEnterForeground(_ notification: NotificationCenter) {
guard let previousDate = appDidEnterBackgroundDate else { return }
let calendar = Calendar.current
let difference = calendar.dateComponents([.second], from: previousDate, to: Date())
let seconds = difference.second!
countTimer -= seconds
}
This works. It uses while loop inside async task, as suggested in another answer, but it is also enclosed within a background task
func executeAfterDelay(delay: TimeInterval, completion: #escaping(()->Void)){
backgroundTaskId = UIApplication.shared.beginBackgroundTask(
withName: "BackgroundSound",
expirationHandler: {[weak self] in
if let taskId = self?.backgroundTaskId{
UIApplication.shared.endBackgroundTask(taskId)
}
})
let startTime = Date()
DispatchQueue.global(qos: .background).async {
while Date().timeIntervalSince(startTime) < delay{
Thread.sleep(forTimeInterval: 0.01)
}
DispatchQueue.main.async {[weak self] in
completion()
if let taskId = self?.backgroundTaskId{
UIApplication.shared.endBackgroundTask(taskId)
}
}
}
}
A timer can run in the background only if both the following are true:
Your app for some other reason runs in the background. (Most apps don't; most apps are suspended when they go into the background.) And:
The timer was running already when the app went into the background.
Timer won't work in background. For background task you can check this link below...
https://www.raywenderlich.com/143128/background-modes-tutorial-getting-started
============== For Objective c ================
create Global uibackground task identifier.
UIBackgroundTaskIdentifier bgRideTimerTask;
now create your timer and add BGTaskIdentifier With it, Dont forget to remove old BGTaskIdentifier while creating new Timer Object.
[timerForRideTime invalidate];
timerForRideTime = nil;
bgRideTimerTask = UIBackgroundTaskInvalid;
UIApplication *sharedApp = [UIApplication sharedApplication];
bgRideTimerTask = [sharedApp beginBackgroundTaskWithExpirationHandler:^{
}];
timerForRideTime = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:#selector(timerTicked:)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop]addTimer:timerForRideTime forMode: UITrackingRunLoopMode];
Here this will work for me even when app goes in background.ask me if you found new problems.
You can achieve this by getting the time-lapse between background and foreground state of the app, here is the code snippet.
import Foundation
import UIKit
class CustomTimer {
let timeInterval: TimeInterval
var backgroundTime : Date?
var background_forground_timelaps : Int?
init(timeInterval: TimeInterval) {
self.timeInterval = timeInterval
}
private lazy var timer: DispatchSourceTimer = {
let t = DispatchSource.makeTimerSource()
t.schedule(deadline: .now() + self.timeInterval, repeating: self.timeInterval)
t.setEventHandler(handler: { [weak self] in
self?.eventHandler?()
})
return t
}()
var eventHandler: (() -> Void)?
private enum State {
case suspended
case resumed
}
private var state: State = .suspended
deinit {
NotificationCenter.default.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil)
timer.setEventHandler {}
timer.cancel()
resume()
eventHandler = nil
}
func resume() {
NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackgroundNotification), name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForegroundNotification), name: UIApplication.willEnterForegroundNotification, object: nil)
if state == .resumed {
return
}
state = .resumed
timer.resume()
}
func suspend() {
if state == .suspended {
return
}
state = .suspended
timer.suspend()
}
#objc fileprivate func didEnterBackgroundNotification() {
self.background_forground_timelaps = nil
self.backgroundTime = Date()
}
#objc fileprivate func willEnterForegroundNotification() {
// refresh the label here
self.background_forground_timelaps = Date().interval(ofComponent: .second, fromDate: self.backgroundTime ?? Date())
self.backgroundTime = nil
}
}
Use this class like;
self.timer = CustomTimer(timeInterval: 1)
self.timer?.eventHandler = {
DispatchQueue.main.sync {
var break_seconds = self.data.total_break_sec ?? 0
break_seconds += 1
if self.timer?.background_forground_timelaps != nil && self.timer?.backgroundTime == nil{
break_seconds += (self.timer?.background_forground_timelaps)!
self.timer?.background_forground_timelaps = nil
}
self.data.total_break_sec = String(break_seconds)
self.lblBreakTime.text = PRNHelper.shared.getPlainTimeString(time: TimeInterval(break_seconds))
}
}
self.timer?.resume()
This way I am able to get the timer right when resumed the app from background.
If 1 or 2 seconds threshold is acceptable this hack could be helpful.
UIApplication.didEnterBackgroundNotification
UIApplication.willEnterForegroundNotification
Stop Timer and Backup Date() on didEnterBackground.
Add Date() to the Backup date on willEnterForegraound to achieve total time.
Start Timer and Add total date to the Timer.
Notice: If user changed the date time of system it will be broken!
You dont really need to keep up with a NSTImer object. Every location update comes with its own timestamp.
Therefore you can just keep up with the last time vs current time and every so often do a task once that threshold has been reached:
if let location = locations.last {
let time = location.timestamp
guard let beginningTime = startTime else {
startTime = time // Saving time of first location time, so we could use it to compare later with subsequent location times.
return //nothing to update
}
let elapsed = time.timeIntervalSince(beginningTime) // Calculating time interval between first and second (previously saved) location timestamps.
if elapsed >= 5.0 { //If time interval is more than 5 seconds
//do something here, make an API call, whatever.
startTime = time
}
}
As others pointed out, Timer cannot make a method run in Background. What you can do instead is use while loop inside async task
DispatchQueue.global(qos: .background).async {
while (shouldCallMethod) {
self.callMethod()
sleep(1)
}
}

How to create flash LED in SOS on swift 2?

I tried write small program base on torch/flash in iPhones. Now i want add SOS signal but i have no idea how should do this. In this code when i start program will switch on and off my LED every 0.2 sec. But i don't know how to do this in SOS signal. And when user click SOS ON and click SOS OFF led should be off immediately. Do i need running some Thread ? or on NSTimer ?
class Sos {
var timer1 = NSTimer()
var timer2 = NSTimer()
var volume: Float = 0.1
let flashLight = FlashLight()
func start() {
self.timer1 = NSTimer.scheduledTimerWithTimeInterval(0.2,
target: self,
selector: Selector("switchON"),
userInfo: nil,
repeats: true)
self.timer2 = NSTimer.scheduledTimerWithTimeInterval(0.4,
target: self,
selector: Selector("switchOFF"),
userInfo: nil,
repeats: true)
}
func stop() {
timer1.invalidate()
timer2.invalidate()
flashLight.switchOFF()
}
#objc func switchON() {
flashLight.switchON(self.volume)
}
#objc func switchOFF() {
flashLight.switchOFF()
}
deinit {
self.timer1.invalidate()
self.timer2.invalidate()
}
}
Many ways to achieve it. Create sequence of time intervals and alternate between on/off for example. Comments in code.
///
/// SOS sequence: ...---...
///
/// . short
/// - long
///
class SOS {
/// Short signal duration (LED on)
private static let shortInterval = 0.2
/// Long signal duration (LED on)
private static let longInterval = 0.4
/// Pause between signals (LED off)
private static let pauseInterval = 0.2
/// Pause between the whole SOS sequences (LED off)
private static let sequencePauseInterval = 2.0
/**
When the SOS sequence is started flashlight is on. Thus
the first time interval is for the short signal. Then pause,
then short, ...
See `timerTick()`, it alternates flashlight status (on/off) based
on the current index in this sequence.
*/
private let sequenceIntervals = [
shortInterval, pauseInterval, shortInterval, pauseInterval, shortInterval, pauseInterval,
longInterval, pauseInterval, longInterval, pauseInterval, longInterval, pauseInterval,
shortInterval, pauseInterval, shortInterval, pauseInterval, shortInterval, sequencePauseInterval
]
/// Current index in the SOS `sequence`
private var index: Int = 0
/// Non repeatable timer, because time interval varies
private weak var timer: NSTimer?
/**
Put your `Flashlight()` calls inside this function.
- parameter on: pass `true` to turn it on or `false` to turn it off
*/
private func turnFlashlight(on on: Bool) {
// if on == true -> turn it on
// if on == false -> turn it off
print(on ? "ON" : "OFF")
}
private func scheduleTimer() {
timer = NSTimer.scheduledTimerWithTimeInterval(sequenceIntervals[index],
target: self, selector: "timerTick",
userInfo: nil, repeats: false)
}
#objc private func timerTick() {
// Increase sequence index, at the end?
if ++index == sequenceIntervals.count {
// Start from the beginning
index = 0
}
// Alternate flashlight status based on current index
// index % 2 == 0 -> is index even number? 0, 2, 4, 6, ...
turnFlashlight(on: index % 2 == 0)
scheduleTimer()
}
func start() {
index = 0
turnFlashlight(on: true)
scheduleTimer()
}
func stop() {
timer?.invalidate()
turnFlashlight(on: false)
}
deinit {
timer?.invalidate()
}
}

Resources