class viewcontroller1: UIViewController {
I have these two images. When the next button is clicked, image1 changes to image2 after 10 seconds. However, I have no idea how to cancel/reset the delayed action if the next button is clicked again before the 10 seconds has expired. This is the code I have so far...
var image1: UIImageView!
var image2: UIImageView!
func delay(_ delay:Double, closure:#escaping ()->()) {
let when = DispatchTime.now() + delay
DispatchQueue.main.asyncAfter(deadline: when, execute: closure)
}
#IBAction func nextbutton(_ sender: Any) {
image1.image = UIImage(named: "image1")
delay(10) { self.image2.image = UIImage(named: "image2")
}
}
}
I would really appreciate any help. Thanks
anyncAfter can not be canceled
As per comment you have to use Timer following is example
Create global variable
var timer:Timer?
and On action
#IBAction func btnImageChangedTapped(_ sender: UIButton) {
if (self.timer != nil) {
self.timer?.invalidate()
self.timer = nil;
}
timer = Timer.scheduledTimer(timeInterval: yourTime, target: self, selector: #selector(changeImage:), userInfo: sender, repeats: false)
}
u can try this one
var timer : Timer?
func startTimer(){
timer = Timer.scheduledTimer(timeInterval: 10.0, target: self, selector: #selector(self.YourImageChangeAction), userInfo: nil, repeats: false)
}
func resetTimer(){
timer?.invalidate()
startTimer()
}
add func resetTimer to your UiButton sender.
You could use a click counter in your VC and test it upon execution of the delayed code.
for example:
var clickCount = 0
#IBAction func nextbutton(_ sender: Any)
{
clickCount += 1
let wantsClickCount = clickCount
image1.image = UIImage(named: "image1")
delay(10)
{
guard self.clickCount == wantsClickCount
else { return }
self.image2.image = UIImage(named: "image2")
}
}
if the button was clicked again before the closure executes, the clickCount will no longer match and the timed code will do nothing.
This would even work with very fast clicking. Note that it will push off the image change by 10 seconds each time so the image change will always happen 10 seconds after the last click.
all
I am a new developer on IOS. nowdays I study Swift by books.
there are some trouble. I use swift3 but the book consists of swift2.
so I don't know what is wrong code.
could you help me?
thanks you for reading and helping me.
this is datacode.
import UIKit
class ViewController: UIViewController {
let timeSelector: Selector = #selector(ViewController.updateTime)
let interval = 1.0
var count = 0
#IBOutlet weak var IbICurrentTime: UILabel!
#IBOutlet weak var IbIPickTime: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
Timer.scheduledTimer(withTimeInterval: interval, repeats: true, block: timeSelector)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func changeDatePicker(_ sender: UIDatePicker) {
let datePickerView = sender
let formatter = DateFormatter()
formatter.dateFormat = "YYYY-MM-dd HH:mm:ss EEE"
IbIPickTime.text = "선택시간: " + formatter.string(from: datePickerView.date)
}
func updateTime() {
IbICurrentTime.text = String(count)
count = count+1
}
}
there are problem
Timer.scheduledTimer(withTimeInterval: interval, repeats: true, block: timeSelector)
}
i don't know what i have to input at "block"?
"Block" is the Objective-C term of Swift's closure. The "Blocks" variant works like this:
Timer.scheduledTimer(timeInterval: interval, repeats: true, block: { timer in
self.IbICurrentTime.text = String(self.count)
self.count += 1
})
You don't need to define a separate updateTime() function with the block syntax.
In Swift the above can be written more naturally as
Timer.scheduledTimer(timeInterval: interval, repeats: true) { _ in
self.IbICurrentTime.text = String(self.count)
self.count += 1
}
If you want to use selectors, use scheduledTimer(timeInterval:target:selector:userInfo:repeats:) instead:
Timer.scheduledTimer(timeInterval: interval,
target: self,
selector: #selector(updateTime),
userInfo: nil,
repeats: true)
I'm trying to use a UISearchView to query google places. In doing so, on text change calls for my UISearchBar, I'm making a request to google places. The problem is I'd rather debounce this call to only request once per 250 ms in order to avoid unnecessary network traffic. I'd rather not write this functionality myself, but I will if I need to.
I found: https://gist.github.com/ShamylZakariya/54ee03228d955f458389 , but I'm not quite sure how to use it:
func debounce( delay:NSTimeInterval, #queue:dispatch_queue_t, action: (()->()) ) -> ()->() {
var lastFireTime:dispatch_time_t = 0
let dispatchDelay = Int64(delay * Double(NSEC_PER_SEC))
return {
lastFireTime = dispatch_time(DISPATCH_TIME_NOW,0)
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
dispatchDelay
),
queue) {
let now = dispatch_time(DISPATCH_TIME_NOW,0)
let when = dispatch_time(lastFireTime, dispatchDelay)
if now >= when {
action()
}
}
}
}
Here is one thing I've tried using the above code:
let searchDebounceInterval: NSTimeInterval = NSTimeInterval(0.25)
func findPlaces() {
// ...
}
func searchBar(searchBar: UISearchBar!, textDidChange searchText: String!) {
debounce(
searchDebounceInterval,
dispatch_get_main_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT),
self.findPlaces
)
}
The resulting error is Cannot invoke function with an argument list of type '(NSTimeInterval, $T5, () -> ())
How do I use this method, or is there a better way to do this in iOS/Swift.
Here's an option for those not wanting to create classes/extensions:
Somewhere in your code:
var debounce_timer:Timer?
And in places you want to do the debounce:
debounce_timer?.invalidate()
debounce_timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { _ in
print ("Debounce this...")
}
If you like to keep things clean, here's a GCD based solution that can do what you need using familiar GCD based syntax:
https://gist.github.com/staminajim/b5e89c6611eef81910502db2a01f1a83
DispatchQueue.main.asyncDeduped(target: self, after: 0.25) { [weak self] in
self?.findPlaces()
}
findPlaces() will only get called one time, 0.25 seconds after the last call to asyncDuped.
Swift 3 version
1. Basic debounce function
func debounce(interval: Int, queue: DispatchQueue, action: #escaping (() -> Void)) -> () -> Void {
var lastFireTime = DispatchTime.now()
let dispatchDelay = DispatchTimeInterval.milliseconds(interval)
return {
lastFireTime = DispatchTime.now()
let dispatchTime: DispatchTime = DispatchTime.now() + dispatchDelay
queue.asyncAfter(deadline: dispatchTime) {
let when: DispatchTime = lastFireTime + dispatchDelay
let now = DispatchTime.now()
if now.rawValue >= when.rawValue {
action()
}
}
}
}
2. Parameterized debounce function
Sometimes it's useful to be have the debounce function take a parameter.
typealias Debounce<T> = (_ : T) -> Void
func debounce<T>(interval: Int, queue: DispatchQueue, action: #escaping Debounce<T>) -> Debounce<T> {
var lastFireTime = DispatchTime.now()
let dispatchDelay = DispatchTimeInterval.milliseconds(interval)
return { param in
lastFireTime = DispatchTime.now()
let dispatchTime: DispatchTime = DispatchTime.now() + dispatchDelay
queue.asyncAfter(deadline: dispatchTime) {
let when: DispatchTime = lastFireTime + dispatchDelay
let now = DispatchTime.now()
if now.rawValue >= when.rawValue {
action(param)
}
}
}
}
3. Example
In the following example you can see, how the debouncing works, using a string parameter to identify the calls.
let debouncedFunction = debounce(interval: 200, queue: DispatchQueue.main, action: { (identifier: String) in
print("called: \(identifier)")
})
DispatchQueue.global(qos: .background).async {
debouncedFunction("1")
usleep(100 * 1000)
debouncedFunction("2")
usleep(100 * 1000)
debouncedFunction("3")
usleep(100 * 1000)
debouncedFunction("4")
usleep(300 * 1000) // waiting a bit longer than the interval
debouncedFunction("5")
usleep(100 * 1000)
debouncedFunction("6")
usleep(100 * 1000)
debouncedFunction("7")
usleep(300 * 1000) // waiting a bit longer than the interval
debouncedFunction("8")
usleep(100 * 1000)
debouncedFunction("9")
usleep(100 * 1000)
debouncedFunction("10")
usleep(100 * 1000)
debouncedFunction("11")
usleep(100 * 1000)
debouncedFunction("12")
}
Note: The usleep() function is only used for demo purposes and may not be the most elegant solution for a real app.
Result
You always get a callback, when there is an interval of at least 200ms since the last call.
called: 4
called: 7
called: 12
Despite several great answers here, I thought I'd share my favorite (pure Swift) approach for debouncing user entered searches...
1) Add this simple class (Debounce.swift):
import Dispatch
class Debounce<T: Equatable> {
private init() {}
static func input(_ input: T,
comparedAgainst current: #escaping #autoclosure () -> (T),
perform: #escaping (T) -> ()) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if input == current() { perform(input) }
}
}
}
2) Optionally include this unit test (DebounceTests.swift):
import XCTest
class DebounceTests: XCTestCase {
func test_entering_text_delays_processing_until_settled() {
let expect = expectation(description: "processing completed")
var finalString: String = ""
var timesCalled: Int = 0
let process: (String) -> () = {
finalString = $0
timesCalled += 1
expect.fulfill()
}
Debounce<String>.input("A", comparedAgainst: "AB", perform: process)
Debounce<String>.input("AB", comparedAgainst: "ABCD", perform: process)
Debounce<String>.input("ABCD", comparedAgainst: "ABC", perform: process)
Debounce<String>.input("ABC", comparedAgainst: "ABC", perform: process)
wait(for: [expect], timeout: 2.0)
XCTAssertEqual(finalString, "ABC")
XCTAssertEqual(timesCalled, 1)
}
}
3) Use it wherever you want to delay processing (e.g. UISearchBarDelegate):
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
Debounce<String>.input(searchText, comparedAgainst: searchBar.text ?? "") {
self.filterResults($0)
}
}
Basic premise is that we are just delaying the processing of the input text by 0.5 seconds. At that time, we compare the string we got from the event with the current value of the search bar. If they match, we assume that the user has paused entering text, and we proceed with the filtering operation.
As it is generic, it works with any type of equatable value.
Since the Dispatch module has been included in the Swift core library since version 3, this class is safe to use with non-Apple platforms as well.
Put this at the top level of your file so as not to confuse yourself with Swift's funny parameter name rules. Notice that I've deleted the # so that now none of the parameters have names:
func debounce( delay:NSTimeInterval, queue:dispatch_queue_t, action: (()->()) ) -> ()->() {
var lastFireTime:dispatch_time_t = 0
let dispatchDelay = Int64(delay * Double(NSEC_PER_SEC))
return {
lastFireTime = dispatch_time(DISPATCH_TIME_NOW,0)
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
dispatchDelay
),
queue) {
let now = dispatch_time(DISPATCH_TIME_NOW,0)
let when = dispatch_time(lastFireTime, dispatchDelay)
if now >= when {
action()
}
}
}
}
Now, in your actual class, your code will look like this:
let searchDebounceInterval: NSTimeInterval = NSTimeInterval(0.25)
let q = dispatch_get_main_queue()
func findPlaces() {
// ...
}
let debouncedFindPlaces = debounce(
searchDebounceInterval,
q,
findPlaces
)
Now debouncedFindPlaces is a function which you can call, and your findPlaces won't be executed unless delay has passed since the last time you called it.
First, create a Debouncer generic class:
//
// Debouncer.swift
//
// Created by Frédéric Adda
import UIKit
import Foundation
class Debouncer {
// MARK: - Properties
private let queue = DispatchQueue.main
private var workItem = DispatchWorkItem(block: {})
private var interval: TimeInterval
// MARK: - Initializer
init(seconds: TimeInterval) {
self.interval = seconds
}
// MARK: - Debouncing function
func debounce(action: #escaping (() -> Void)) {
workItem.cancel()
workItem = DispatchWorkItem(block: { action() })
queue.asyncAfter(deadline: .now() + interval, execute: workItem)
}
}
Then create a subclass of UISearchBar that uses the debounce mechanism:
//
// DebounceSearchBar.swift
//
// Created by Frédéric ADDA on 28/06/2018.
//
import UIKit
/// Subclass of UISearchBar with a debouncer on text edit
class DebounceSearchBar: UISearchBar, UISearchBarDelegate {
// MARK: - Properties
/// Debounce engine
private var debouncer: Debouncer?
/// Debounce interval
var debounceInterval: TimeInterval = 0 {
didSet {
guard debounceInterval > 0 else {
self.debouncer = nil
return
}
self.debouncer = Debouncer(seconds: debounceInterval)
}
}
/// Event received when the search textField began editing
var onSearchTextDidBeginEditing: (() -> Void)?
/// Event received when the search textField content changes
var onSearchTextUpdate: ((String) -> Void)?
/// Event received when the search button is clicked
var onSearchClicked: (() -> Void)?
/// Event received when cancel is pressed
var onCancel: (() -> Void)?
// MARK: - Initializers
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
delegate = self
}
override init(frame: CGRect) {
super.init(frame: frame)
delegate = self
}
override func awakeFromNib() {
super.awakeFromNib()
delegate = self
}
// MARK: - UISearchBarDelegate
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
onCancel?()
}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
onSearchClicked?()
}
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
onSearchTextDidBeginEditing?()
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
guard let debouncer = self.debouncer else {
onSearchTextUpdate?(searchText)
return
}
debouncer.debounce {
DispatchQueue.main.async {
self.onSearchTextUpdate?(self.text ?? "")
}
}
}
}
Note that this class is set as the UISearchBarDelegate. Actions will be passed to this class as closures.
Finally, you can use it like so:
class MyViewController: UIViewController {
// Create the searchBar as a DebounceSearchBar
// in code or as an IBOutlet
private var searchBar: DebounceSearchBar?
override func viewDidLoad() {
super.viewDidLoad()
self.searchBar = createSearchBar()
}
private func createSearchBar() -> DebounceSearchBar {
let searchFrame = CGRect(x: 0, y: 0, width: 375, height: 44)
let searchBar = DebounceSearchBar(frame: searchFrame)
searchBar.debounceInterval = 0.5
searchBar.onSearchTextUpdate = { [weak self] searchText in
// call a function to look for contacts, like:
// searchContacts(with: searchText)
}
searchBar.placeholder = "Enter name or email"
return searchBar
}
}
Note that in that case, the DebounceSearchBar is already the searchBar delegate. You should NOT set this UIViewController subclass as the searchBar delegate! Nor use delegate functions.
Use the provided closures instead!
I used this good old Objective-C inspired method:
override func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
// Debounce: wait until the user stops typing to send search requests
NSObject.cancelPreviousPerformRequests(withTarget: self)
perform(#selector(updateSearch(with:)), with: searchText, afterDelay: 0.5)
}
Note that the called method updateSearch must be marked #objc !
#objc private func updateSearch(with text: String) {
// Do stuff here
}
The big advantage of this method is that I can pass parameters (here: the search string). With most of Debouncers presented here, that is not the case ...
The following is working for me:
Add the below to some file within your project (I maintain a 'SwiftExtensions.swift' file for things like this):
// Encapsulate a callback in a way that we can use it with NSTimer.
class Callback {
let handler:()->()
init(_ handler:()->()) {
self.handler = handler
}
#objc func go() {
handler()
}
}
// Return a function which debounces a callback,
// to be called at most once within `delay` seconds.
// If called again within that time, cancels the original call and reschedules.
func debounce(delay:NSTimeInterval, action:()->()) -> ()->() {
let callback = Callback(action)
var timer: NSTimer?
return {
// if calling again, invalidate the last timer
if let timer = timer {
timer.invalidate()
}
timer = NSTimer(timeInterval: delay, target: callback, selector: "go", userInfo: nil, repeats: false)
NSRunLoop.currentRunLoop().addTimer(timer!, forMode: NSDefaultRunLoopMode)
}
}
Then set it up in your classes:
class SomeClass {
...
// set up the debounced save method
private var lazy debouncedSave: () -> () = debounce(1, self.save)
private func save() {
// ... actual save code here ...
}
...
func doSomething() {
...
debouncedSave()
}
}
You can now call someClass.doSomething() repeatedly and it will only save once per second.
The general solution as provided by the question and built upon in several of the answers, has a logic mistake that causes problems with short debounce thresholds.
Starting with the provided implementation:
typealias Debounce<T> = (T) -> Void
func debounce<T>(interval: Int, queue: DispatchQueue, action: #escaping (T) -> Void) -> Debounce<T> {
var lastFireTime = DispatchTime.now()
let dispatchDelay = DispatchTimeInterval.milliseconds(interval)
return { param in
lastFireTime = DispatchTime.now()
let dispatchTime: DispatchTime = DispatchTime.now() + dispatchDelay
queue.asyncAfter(deadline: dispatchTime) {
let when: DispatchTime = lastFireTime + dispatchDelay
let now = DispatchTime.now()
if now.rawValue >= when.rawValue {
action(param)
}
}
}
}
Testing with an interval of 30 milliseconds, we can create a relatively trivial example that demonstrates the weakness.
let oldDebouncerDebouncedFunction = debounce(interval: 30, queue: .main, action: exampleFunction)
DispatchQueue.global(qos: .background).async {
oldDebouncerDebouncedFunction("1")
oldDebouncerDebouncedFunction("2")
sleep(.seconds(2))
oldDebouncerDebouncedFunction("3")
}
This prints
called: 1
called: 2
called: 3
This is clearly incorrect, because the first call should be debounced. Using a longer debounce threshold (such as 300 milliseconds) will fix the problem. The root of the problem is a false expectation that the value of DispatchTime.now() will be equal to the deadline passed to asyncAfter(deadline: DispatchTime). The intention of the comparison now.rawValue >= when.rawValue is to actually compare the expected deadline to the "most recent" deadline. With small debounce thresholds, the latency of asyncAfter becomes a very important problem to think about.
It's easy to fix though, and the code can be made more concise on top of it. By carefully choosing when to call .now(), and ensuring the comparison of the actual deadline with most recently scheduled deadline, I arrived at this solution. Which is correct for all values of threshold. Pay special attention to #1 and #2 as they are the same syntactically, but will be different if multiple calls are made before the work is dispatched.
typealias DebouncedFunction<T> = (T) -> Void
func makeDebouncedFunction<T>(threshold: DispatchTimeInterval = .milliseconds(30), queue: DispatchQueue = .main, action: #escaping (T) -> Void) -> DebouncedFunction<T> {
// Debounced function's state, initial value doesn't matter
// By declaring it outside of the returned function, it becomes state that persists across
// calls to the returned function
var lastCallTime: DispatchTime = .distantFuture
return { param in
lastCallTime = .now()
let scheduledDeadline = lastCallTime + threshold // 1
queue.asyncAfter(deadline: scheduledDeadline) {
let latestDeadline = lastCallTime + threshold // 2
// If there have been no other calls, these will be equal
if scheduledDeadline == latestDeadline {
action(param)
}
}
}
}
Utilities
func exampleFunction(identifier: String) {
print("called: \(identifier)")
}
func sleep(_ dispatchTimeInterval: DispatchTimeInterval) {
switch dispatchTimeInterval {
case .seconds(let seconds):
Foundation.sleep(UInt32(seconds))
case .milliseconds(let milliseconds):
usleep(useconds_t(milliseconds * 1000))
case .microseconds(let microseconds):
usleep(useconds_t(microseconds))
case .nanoseconds(let nanoseconds):
let (sec, nsec) = nanoseconds.quotientAndRemainder(dividingBy: 1_000_000_000)
var timeSpec = timespec(tv_sec: sec, tv_nsec: nsec)
withUnsafePointer(to: &timeSpec) {
_ = nanosleep($0, nil)
}
case .never:
return
}
}
Hopefully, this answer will help someone else that has encountered unexpected behavior with the function currying solution.
Here you have totally Swift 5 friendly and smooth solution 👌🏻
You can use it for example when detecting tableView scrolls to bottom.
NSObject.cancelPreviousPerformRequests(withTarget: self,
selector: #selector(didScrollToBottom),
object: nil)
perform(#selector(didScrollToBottom), with: nil, afterDelay: TimeInterval(0.1))
#objc private func didScrollToBottom() {
print("finally called once!")
}
A couple subtle improvements on quickthyme's excellent answer:
Add a delay parameter, perhaps with a default value.
Make Debounce an enum instead of a class, so you can skip having to declare a private init.
enum Debounce<T: Equatable> {
static func input(_ input: T, delay: TimeInterval = 0.3, current: #escaping #autoclosure () -> T, perform: #escaping (T) -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
guard input == current() else { return }
perform(input)
}
}
}
It's also not necessary to explicitly declare the generic type at the call site — it can be inferred. For example, if you want to use Debounce with a UISearchController, in updateSearchResults(for:) (required method of UISearchResultsUpdating), you would do this:
func updateSearchResults(for searchController: UISearchController) {
guard let text = searchController.searchBar.text else { return }
Debounce.input(text, current: searchController.searchBar.text ?? "") {
// ...
}
}
Swift 5.7
Note that it is only available in iOS 16.0 or newer.
var task: Task<(), Never>?
func debounce(interval: Duration = .nanoseconds(10000),
operation: #escaping () -> Void) {
task?.cancel()
task = Task {
do {
try await Task.sleep(for: interval)
operation()
} catch {
// TODO
}
}
}
You can use it like
for i in 0...1000 {
debounce {
print(i)
}
}
// 0
// 25
// 81
// 1000
Prior to iOS 16
import Foundation
// < iOS 16
var task: Task<(), Never>?
func debounce(seconds: Double = 1.0,
operation: #escaping () -> Void) {
task?.cancel()
task = Task {
do {
try await Task.sleep(seconds: seconds)
operation()
} catch {
// TODO
}
}
}
for i in 0...1000 {
debounce(seconds: 1.0 * 0.00001) {
print(i)
}
}
// 0
// 123
// 1000
extension Task where Success == Never, Failure == Never {
static func sleep(seconds: Double) async throws {
let duration = UInt64(seconds * 1_000_000_000)
try await Task.sleep(nanoseconds: duration)
}
}
Here is a debounce implementation for Swift 3.
https://gist.github.com/bradfol/541c010a6540404eca0f4a5da009c761
import Foundation
class Debouncer {
// Callback to be debounced
// Perform the work you would like to be debounced in this callback.
var callback: (() -> Void)?
private let interval: TimeInterval // Time interval of the debounce window
init(interval: TimeInterval) {
self.interval = interval
}
private var timer: Timer?
// Indicate that the callback should be called. Begins the debounce window.
func call() {
// Invalidate existing timer if there is one
timer?.invalidate()
// Begin a new timer from now
timer = Timer.scheduledTimer(timeInterval: interval, target: self, selector: #selector(handleTimer), userInfo: nil, repeats: false)
}
#objc private func handleTimer(_ timer: Timer) {
if callback == nil {
NSLog("Debouncer timer fired, but callback was nil")
} else {
NSLog("Debouncer timer fired")
}
callback?()
callback = nil
}
}
owenoak's solution works for me. I changed it a little bit to fit my project:
I created a swift file Dispatcher.swift:
import Cocoa
// Encapsulate an action so that we can use it with NSTimer.
class Handler {
let action: ()->()
init(_ action: ()->()) {
self.action = action
}
#objc func handle() {
action()
}
}
// Creates and returns a new debounced version of the passed function
// which will postpone its execution until after delay seconds have elapsed
// since the last time it was invoked.
func debounce(delay: NSTimeInterval, action: ()->()) -> ()->() {
let handler = Handler(action)
var timer: NSTimer?
return {
if let timer = timer {
timer.invalidate() // if calling again, invalidate the last timer
}
timer = NSTimer(timeInterval: delay, target: handler, selector: "handle", userInfo: nil, repeats: false)
NSRunLoop.currentRunLoop().addTimer(timer!, forMode: NSDefaultRunLoopMode)
NSRunLoop.currentRunLoop().addTimer(timer!, forMode: NSEventTrackingRunLoopMode)
}
}
Then I added the following in my UI class:
class func changed() {
print("changed")
}
let debouncedChanged = debounce(0.5, action: MainWindowController.changed)
The key difference from owenoak's anwer is this line:
NSRunLoop.currentRunLoop().addTimer(timer!, forMode: NSEventTrackingRunLoopMode)
Without this line, the timer never triggers if the UI loses focus.
Scenario: User taps on button continuously but only last one is accepted and all previous request is cancelled.To keep it simple fetchMethod() prints the counter value.
1: Using Perform Selector After a delay:
working example Swift 5
import UIKit
class ViewController: UIViewController {
var stepper = 1
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func StepperBtnTapped() {
stepper = stepper + 1
NSObject.cancelPreviousPerformRequests(withTarget: self)
perform(#selector(updateRecord), with: self, afterDelay: 0.5)
}
#objc func updateRecord() {
print("final Count \(stepper)")
}
}
2:Using DispatchWorkItem:
class ViewController: UIViewController {
private var pendingRequestWorkItem: DispatchWorkItem?
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func tapButton(sender: UIButton) {
counter += 1
pendingRequestWorkItem?.cancel()
let requestWorkItem = DispatchWorkItem { [weak self] in self?.fetchMethod()
}
pendingRequestWorkItem = requestWorkItem
DispatchQueue.main.asyncAfter(deadline: .now() +.milliseconds(250),execute: requestWorkItem)
}
func fetchMethod() {
print("fetchMethod:\(counter)")
}
}
//Output:
fetchMethod:1 //clicked once
fetchMethod:4 //clicked 4 times ,
//but previous triggers are cancelled by
// pendingRequestWorkItem?.cancel()
reference link
I'm building an app and I need a timer to run if the user sends the screen to the background, or if they put the phone in sleep and open it again. I need the timer to still be going.
I tried recording the time when I exit the and enter it again, subtracting the two and adding that to the running count, and it seems to work fine on the Xcode simulator but when I run it on my phone it doesn't work. Any ideas?
Here is the code for reference.
And the timer starts with a button I didn't include that part but it's just a simple IBAction that calls the timer.fire() function.
var time = 0.0
var timer = Timer()
var exitTime : Double = 0
var resumeTime : Double = 0
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(true)
exitTime = Date().timeIntervalSinceNow
}
override func awakeFromNib() {
super.awakeFromNib()
resumeTime = Date().timeIntervalSinceNow
time += (resumeTime-exitTime)
timer.fire()
}
func startTimer() {
if !isTimeRunning {
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector:
#selector(WorkoutStartedViewController.action), userInfo: nil, repeats: true)
isTimeRunning = true
}
}
func pauseTimer() {
timer.invalidate()
isTimeRunning = false
}
#objc func action()
{
time += 0.1
timerLabel.text = String(time)
let floorCounter = Int(floor(time))
let hour = floorCounter/3600
let minute = (floorCounter % 3600)/60
var minuteString = "\(minute)"
if minute < 10 {
minuteString = "0\(minute)"
}
let second = (floorCounter % 3600) % 60
var secondString = "\(second)"
if second < 10 {
secondString = "0\(second)"
}
if time < 3600.0 {
timerLabel.text = "\(minuteString):\(secondString)"
} else {
timerLabel.text = "\(hour):\(minuteString):\(secondString)"
}
}
You do have the right idea but the first problem I see is that viewWillDissapear is only called when you leave a view controller to go to a new viewController - It is not called when the app leaves the view to enter background (home button press)
I believe the callback functions you are looking for are UIApplication.willResignActive (going to background) and UIApplication.didBecomeActive (app re-opened)
You can access these methods in the AppDelegate or you can set them up on a view controller heres a mix of your code and some changes to produce a working sample on one initial VC:
import UIKit
import CoreData
class ViewController: UIViewController {
#IBOutlet weak var timerLabel: UILabel!
var time = 0.0
var timer = Timer()
var exitTime : Date? // Change to Date
var resumeTime : Date? // Change to Date
var isTimeRunning = false
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
startTimer()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(self,
selector: #selector(applicationDidBecomeActive),
name: UIApplication.didBecomeActiveNotification,
object: nil)
// Add willResign observer
NotificationCenter.default.addObserver(self,
selector: #selector(applicationWillResign),
name: UIApplication.willResignActiveNotification,
object: nil)
}
override func viewWillDisappear(_ animated: Bool) {
// Remove becomeActive observer
NotificationCenter.default.removeObserver(self,
name: UIApplication.didBecomeActiveNotification,
object: nil)
// Remove becomeActive observer
NotificationCenter.default.removeObserver(self,
name: UIApplication.willResignActiveNotification,
object: nil)
}
func startTimer() {
if !isTimeRunning {
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector:
#selector(self.action), userInfo: nil, repeats: true)
isTimeRunning = true
}
}
#objc func action() {
time += 0.1
timerLabel.text = String(time)
let floorCounter = Int(floor(time))
let hour = floorCounter/3600
let minute = (floorCounter % 3600)/60
var minuteString = "\(minute)"
if minute < 10 {
minuteString = "0\(minute)"
}
let second = (floorCounter % 3600) % 60
var secondString = "\(second)"
if second < 10 {
secondString = "0\(second)"
}
if time < 3600.0 {
timerLabel.text = "\(minuteString):\(secondString)"
} else {
timerLabel.text = "\(hour):\(minuteString):\(secondString)"
}
}
#objc func applicationDidBecomeActive() {
// handle event
lookForActiveTimers()
}
func lookForActiveTimers() {
var timers = [NSManagedObject]()
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext = appDelegate.persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Timers")
//3
do {
timers = try managedContext.fetch(fetchRequest)
print("timers: \(timers)")
var activeTimer: NSManagedObject?
for timer in timers {
if let active = timer.value(forKey: "active") as? Bool {
if active {
activeTimer = timer
}
}
}
if let activeTimer = activeTimer {
// Handle active timer (may need to go to a new view)
if let closeDate = activeTimer.value(forKey: "appCloseTime") as? Date {
if let alreadyTimed = activeTimer.value(forKey: "alreadyTimed") as? Double {
let now = Date()
let difference = now.timeIntervalSince(closeDate)
// Handle set up again here
print("App opened with a difference of \(difference) and already ran for a total of \(alreadyTimed) seconds before close")
time = alreadyTimed + difference
startTimer()
}
}
} else {
print("We dont have any active timers")
}
// Remove active timers because we reset them up
for timer in timers {
managedContext.delete(timer)
}
do {
print("deleted")
try managedContext.save() // <- remember to put this :)
} catch {
// Do something... fatalerror
}
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}
}
#objc func applicationWillResign() {
// handle event
saveActiveTimer()
}
func saveActiveTimer() {
if isTimeRunning {
// Create a new alarm object
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
let context = appDelegate.persistentContainer.viewContext
if let entity = NSEntityDescription.entity(forEntityName: "Timers", in: context) {
let newTimer = NSManagedObject(entity: entity, insertInto: context)
newTimer.setValue(true, forKey: "active")
let now = Date()
newTimer.setValue(now, forKey: "appCloseTime")
newTimer.setValue(self.time, forKey: "alreadyTimed")
do {
try context.save()
print("object saved success")
} catch {
print("Failed saving")
}
}
}
}
}
EDIT - Here is the full tested and working code on xCode 11.3 and a physical device iOS 13.2 - You have to figure out how to start and stop the timer according to your buttons - but this example simply starts the timer when the app is first opened and never stops or resets it.
You can reproduce this by creating a new single-view xCode project and replacing the code in the first view controller that it creates for you with the code above. Then create a label to attach to the outlet timerLabel on the VC
Also make sure to enable CoreData in your project while creating your new project * Then set up the entities and attributes in the xcdatamodel file:
Hope this helps