Timer and dependancy injection - ios

I've just started with Swift and using MVVM with dependency injection.
In my ViewModel I have Timer that handles refreshing the data. I've simplified the code a little for clarity.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let viewModel = ViewModel()
}
}
class ViewModel: NSObject {
private var timer: Timer?
override init() {
super.init()
setUpTimer()
}
func setUpTimer() {
timer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true){_ in
self.refreshData()
}
}
func refreshData() {
//refresh data
print("refresh data")
}
}
I want to use dependency injection to pass the Timer into the ViewModel so that I can control the timer when doing unit tests and make it call immediately.
So passing the Timer is pretty simple. How can I pass a Timer in to ViewModel that has the ability to call the refreshData() belonging to ViewModel. Is there a trick in Swift that allows this?
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let timer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true){_ in
// call refreshData() from the class ViewModel
}
var viewModel = ViewModel(myTimer:timer)
}
}
class ViewModel: NSObject {
private var timer: Timer?
init(myTimer:Timer) {
super.init()
//setUpTimer()
timer = myTimer
}
func refreshData() {
//refresh data
print("refresh data")
}
}
I thought it might be possible using the scheduelTimer that takes a selector instead of a block but that would require using a #objc before the func refreshData() which seems clunky since I am using an Objective C feature in Swift.
Is there a nice way to achieve this?
Many Thanks,
Code

Conceptually, you want to decouple the implementation. So instead of having to pass Timer to the view model, you pass some other "control" object, which guarantees to perform the operation (of calling back after a delay)
If that doesn't shout protocol, I don't know what does...
typealias Ticker = () -> Void
protocol Refresher {
var isRunning: Bool { get }
func register(_ ticker: #escaping Ticker)
func start();
func stop();
}
So, pretty basic concept. It can start, stop and an observer can register itself to it and be notified when a "tick" occurs. The observer doesn't care "how" it works, so long as it guarantees to perform the specified operation.
A Timer based implementation then might look something like...
class TimerRefresher: Refresher {
private var timer: Timer? = nil
private var ticker: Ticker? = nil
var isRunning: Bool = false
func register(_ ticker: #escaping Ticker) {
self.ticker = ticker
guard timer == nil else {
return
}
}
func start() {
guard ticker != nil else {
return
}
stop()
isRunning = true
timer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true, block: { (timer) in
self.tick()
})
}
func stop() {
guard let timer = timer else {
return
}
isRunning = false
timer.invalidate()
self.timer = nil
}
private func tick() {
guard let ticker = ticker else {
stop()
return
}
ticker()
}
}
This provides you the entry point for mocking the dependency injection, by replacing the implementation of the Refresher with one you can control manually (or use a different "delaying" action, depending on your needs)
This is just a conceptual example, your implementation/needs may differ and lead you to a slightly different design, but the idea remains the same, decouple the physical implementation in some way.
An alternative would require you to rethink your design, and instead of the view model performing it's own refresh, the view/controller would take over that responsibility instead. Since that's a significant design decision, you're really only the person who can make that decision, but it's another idea

If I understand you correctly, you want the model to refresh every 30 seconds when running in the app, but faster for test. If so, don't inject the Timer. Inject the refresh frequency.
class ViewModel: NSObject {
// We need something to observe and confirm that the data is fresh
#objc dynamic var lastRefreshed: Date?
private var timer: Timer!
// The default frequency is 30 seconds but users can adjust that
// The unit test uses it to inject dependency
init(refreshFrequency: TimeInterval = 30) {
super.init()
timer = Timer.scheduledTimer(timeInterval: refreshFrequency, target: self, selector: #selector(refreshData), userInfo: nil, repeats: true)
}
#objc func refreshData() {
lastRefreshed = Date()
print("refreshed on: \(lastRefreshed!)")
}
}
And your unit test:
func testModel() {
let startTime = Date()
let model = ViewModel(refreshFrequency: 5)
// Test first refresh: must be within 5 - 6 seconds from startTime
keyValueObservingExpectation(for: model, keyPath: #keyPath(ViewModel.lastRefreshed)) { (_, _) -> Bool in
if let duration = model.lastRefreshed?.timeIntervalSince(startTime), 5...6 ~= duration {
return true
} else {
return false
}
}
// Test second refresh: must be within 10 - 12 seconds from startTime
keyValueObservingExpectation(for: model, keyPath: #keyPath(ViewModel.lastRefreshed)) { (_, _) -> Bool in
if let duration = model.lastRefreshed?.timeIntervalSince(startTime), 10...12 ~= duration {
return true
} else {
return false
}
}
// Wait 12 seconds for both expectations to be fulfilled
waitForExpectations(timeout: 12, handler: nil)
}
Timer is not exact: it does not fire exactly every 5 seconds like you asked. Apple say Timer is accurate to about 50 - 100ms. Hence we cannot expect that the first refresh will happen 5 seconds from now. We must allow for some tolerances. The further out you go, the bigger this tolerance have to become.

Related

Creating a simple timer in WatchOS with Swift 4

Seemingly simple but I'm struggling...the code below crashes on the line setting the date of the workoutTimer. Also my WKInterfaceTimer isn't hooked up to an IBOutlet, does it need to be? I wanted to use it just for purposes of the time.
class InterfaceController {
var workoutTimer: WKInterfaceTimer!
var workoutStartTime: NSDate? = nil
func startWorkOutTimer() {
self.startWorkout()
if let test = self.workoutSecondsElapsed() {
print("timer seconds = \(test)")
}
}
func startWorkout() {
// To count up use 0.0 or less, otherwise the timer counts down.
workoutTimer.setDate(NSDate(timeIntervalSinceNow: 0.0) as Date)
workoutTimer.start()
self.workoutStartTime = NSDate()
}
func stopWorkout() {
workoutTimer.stop()
}
func workoutSecondsElapsed() -> TimeInterval? {
// If the timer hasn't been started then return nil
guard let startTime = self.workoutStartTime else {
return nil
}
// Time intervals from past dates are negative, so
// multiply by -1 to get the elapsed time.
return -1.0 * (self.workoutStartTime?.timeIntervalSinceNow)!
}
}
From Apple doc:
Do not subclass or create instances of this class yourself. Instead, define outlets in your interface controller class and connect them to the corresponding objects in your storyboard file.
Your app probably is crashing because your timer is nil, but for what you need you can use Timer class instead of WKInterfaceTimer.

How implement optimized multiple timer in swift?

I just wonder what is the best implementation of memory optimized versatile multi Timers in swift.
The timers which are concurrent and have weak reference with Dispatch?
I've tried to implement two timers in one view controller and I got an error.
one of my timer was like this:
func startOnPlayingTimer() {
let queue = DispatchQueue(label: "com.app.timer")
onPlayTimer = DispatchSource.makeTimerSource(queue: queue)
onPlayTimer!.scheduleRepeating(deadline: .now(), interval: .seconds(4))
onPlayTimer!.setEventHandler { [weak self] in
print("onPlayTimer has triggered")
}
onPlayTimer!.resume()
}
another one was:
carouselTimer = Timer.scheduledTimer(timeInterval: 3, target: self,selector: #selector(scrollCarousel), userInfo: nil, repeats: true)
I dont think there is need for multiple timer for any application.
If from before hand you know which methods you are going to fire, Keep boolean for each method you need to fire and also an Int to save the occurrence of the method. You can invoke the timer once, with a method that checks for the required boolean and its respective method.
A pseudo code referencing the above logic is below :
class ViewController: UIViewController {
var myTimer : Timer!
var methodOneBool : Bool!
var methodTwoBool : Bool!
var mainTimerOn : Bool!
var mainTimerLoop : Int!
var methodOneInvocation : Int!
var methodTwoInvocation : Int!
override func viewDidLoad() {
super.viewDidLoad()
configure()
}
func configure(){
methodOneBool = false
methodTwoBool = false
methodOneInvocation = 5 // every 5 seconds
methodTwoInvocation = 3 //every 3 seconds
mainTimerOn = true // for disable and enable timer
mainTimerLoop = 0 // count for timer main
}
func invokeTimer(){
myTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(checkTimerMethod), userInfo: nil, repeats: true)
}
func checkTimerMethod(){
if(mainTimerOn){
if(mainTimerLoop % methodOneInvocation == 0 && methodOneBool){
// perform first method
// will only get inside this when
// methodOneBool = true and every methodOneInvocation seconds
}
if(mainTimerLoop % methodTwoInvocation == 0 && methodTwoBool){
// perform second method
// will only get inside this when
// methodTwoBool = true and every methodTwoInvocation seconds
}
mainTimerLoop = mainTimerLoop + 1
}
}
}
I hope this clears up the problem, also if I didnt understand your requirement please comment below, so that I can edit the answer accordingly

Singleton SharedInstance Function Called Twice

All singleton functions are twice calls.
The "Prova" function, and the selector of the timer are twice calls.
class Timer {
static let sharedInstanceTimer = Timer()
var TimerCounter : NSTimer = NSTimer()
var Counter : Int = Int()
var TimerGameOver : Int = Int()
var TimerBonusMultipleCircle : Int = Int()
var TimerBonusBigCircle : Int = Int()
var TimerCounterInterval : NSTimeInterval = 1
init()
{
self.Counter = 60
self.TimerGameOver = 10
self.TimerBonusMultipleCircle = 5
self.TimerBonusBigCircle = 5
self.TimerCounterInterval = 1
}
func StartTimerCounter()
{
self.TimerCounter = NSTimer.scheduledTimerWithTimeInterval(self.TimerCounterInterval, target: Game.sharedInstanceGame, selector: #selector(Game.sharedInstanceGame.UpdateAllCounter), userInfo: nil, repeats: true)
}
func StopTimerCounter()
{
self.TimerCounter.invalidate()
}
}
And... In another file I call StartTimerCounter()
import UIKit
class FirstViewController: UIViewController {
static let sharedInstanceFirstViewController = FirstViewController()
override func viewDidLoad() {
super.viewDidLoad()
let backButton = UIBarButtonItem(title: "00:00:60", style: UIBarButtonItemStyle.Plain, target: self, action: nil)
backButton.tintColor = UIColor.whiteColor()
navigationItem.leftBarButtonItem = backButton
Circle.sharedInstanceCircle.CreateCircle()
view.layer.addSublayer(Circle.sharedInstanceCircle.PrincipalCircle)
Game.sharedInstanceGame.Play()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func prefersStatusBarHidden() -> Bool {
return true
}
func ReturnToMenu()
{
navigationController?.popViewControllerAnimated(true)
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
}
Leaving aside the various errors that I corrected as suggested below I have tried several solutions but I can not find the solution to this problem.
A couple of thoughts:
Your sharedInstanceTimer is instantiating Timer (which is, itself, a NSTimer). It should not subclass NSTimer.
You then initialize TimerCounter to a second timer which you never use.
The above will instantiate two timers. You then have StartTimerCounter that instantiates another NSTimer every time you call it. Assuming you want only one timer, you should should have StartTimerCounter invalidate any prior timer before starting a new one.
I'd call this class TimerManager, or something like that, to avoid clashes with Swift 3 type, Timer.
Method names should start with lowercase letters, as should properties. In the spirit of the Swift 3 API guidelines, you might want to shorten the method names, too.
Also, if you're going to define a singleton, I'd declare a private init() initializer, to prevent other classes from accidentally ever instantiating another TimerManager object.
So, that yields something like:
class TimerManager {
static let shared = TimerManager()
private var timer: NSTimer? // this certainly should be private
// whether you make these private or not, or constants vs properties, is up to you
private var tounter : Int = 60
private var timerGameOver : Int = 10
private var timerBonusMultipleCircle : Int = 5
private var timerBonusBigCircle : Int = 5
private var timerCounterInterval : NSTimeInterval = 1
// make sure no one else accidentally instantiates another TimerManager
private init() {
}
/// Start timer.
func startTimer() {
stopTimer() // cancel prior timer, if any
timer = NSTimer.scheduledTimerWithTimeInterval(timerCounterInterval, target: Game.sharedInstanceGame, selector: #selector(Game.sharedInstanceGame.updateAllCounter), userInfo: nil, repeats: true)
Game.sharedInstanceGame.prova()
}
/// Stop timer.
func stopTimer() {
timer?.invalidate()
timer = nil
}
}
An unrelated, deeper observation here: I'd suggest that you keep the timer object loosely coupled with respect to the Game object. So, I'd excise all "game" related stuff from this class:
class TimerManager {
static let shared = TimerManager()
private var timer: NSTimer? // this certainly should be private
private var timerHandler: (() -> ())?
// whether you make these private or not, or constants vs properties, is up to you
private var timerCounterInterval: NSTimeInterval = 1
// make sure no one else accidentally instantiates another TimerManager
private init() {
}
/// Start timer.
///
/// - parameter timerHandler: The closure that will be called once per second.
func startTimer(timerHandler: () -> ()) {
stopTimer() // cancel prior timer, if any
self.timerHandler = timerHandler
timer = NSTimer.scheduledTimerWithTimeInterval(timerCounterInterval, target: self, selector: #selector(handleTimer(_:)), userInfo: nil, repeats: true)
}
#objc func handleTimer(timer: NSTimer) {
timerHandler?()
}
/// Stop timer.
func stopTimer() {
timer?.invalidate()
timerHandler = nil
timer = nil
}
}
Then, when the Game object wants to start a timer, it might do:
TimerManager.shared.startTimer {
self.updateAllCounter()
}
prova()
Now, perhaps you simplified your timer object for the purpose of this question and perhaps there's more that's needed in this TimerManager object (as suggested by all these other properties that aren't otherwise referenced in your code snippet), but hopefully this illustrates the basic idea: The TimerManager shouldn't be involved in the business of calling any specific Game methods or the like. It should simply provide a mechanism by which the caller can simply supply a block of code that the timer should periodically invoke.

How to throttle search (based on typing speed) in iOS UISearchBar?

I have a UISearchBar part of a UISearchDisplayController that is used to display search results from both local CoreData and remote API.
What I want to achieve is the "delaying" of the search on the remote API. Currently, for each character typed by the user, a request is sent. But if the user types particularly fast, it does not make sense to send many requests: it would help to wait until he has stopped typing.
Is there a way to achieve that?
Reading the documentation suggests to wait until the users explicitly taps on search, but I don't find it ideal in my case.
Performance issues. If search operations can be carried out very
rapidly, it is possible to update the search results as the user is
typing by implementing the searchBar:textDidChange: method on the
delegate object. However, if a search operation takes more time, you
should wait until the user taps the Search button before beginning the
search in the searchBarSearchButtonClicked: method. Always perform
search operations a background thread to avoid blocking the main
thread. This keeps your app responsive to the user while the search is
running and provides a better user experience.
Sending many requests to the API is not a problem of local performance but only of avoiding too high request rate on the remote server.
Thanks
Try this magic:
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText{
// to limit network activity, reload half a second after last key press.
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(reload) object:nil];
[self performSelector:#selector(reload) withObject:nil afterDelay:0.5];
}
Swift version:
func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {
// to limit network activity, reload half a second after last key press.
NSObject.cancelPreviousPerformRequestsWithTarget(self, selector: "reload", object: nil)
self.performSelector("reload", withObject: nil, afterDelay: 0.5)
}
Note this example calls a method called reload but you can make it call whatever method you like!
For people who need this in Swift 4 onwards:
Keep it simple with a DispatchWorkItem like here.
or use the old Obj-C way:
func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {
// to limit network activity, reload half a second after last key press.
NSObject.cancelPreviousPerformRequestsWithTarget(self, selector: "reload", object: nil)
self.performSelector("reload", withObject: nil, afterDelay: 0.5)
}
EDIT: SWIFT 3 Version
func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {
// to limit network activity, reload half a second after last key press.
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(self.reload), object: nil)
self.perform(#selector(self.reload), with: nil, afterDelay: 0.5)
}
#objc func reload() {
print("Doing things")
}
Improved Swift 4+:
Assuming that you are already conforming to UISearchBarDelegate, this is an improved Swift 4 version of VivienG's answer:
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(self.reload(_:)), object: searchBar)
perform(#selector(self.reload(_:)), with: searchBar, afterDelay: 0.75)
}
#objc func reload(_ searchBar: UISearchBar) {
guard let query = searchBar.text, query.trimmingCharacters(in: .whitespaces) != "" else {
print("nothing to search")
return
}
print(query)
}
The purpose of implementing cancelPreviousPerformRequests(withTarget:) is to prevent the continuous calling to the reload() for each change to the search bar (without adding it, if you typed "abc", reload() will be called three times based on the number of the added characters).
The improvement is: in reload() method has the sender parameter which is the search bar; Thus accessing its text -or any of its method/properties- would be accessible with declaring it as a global property in the class.
Thanks to this link, I found a very quick and clean approach. Compared to Nirmit's answer it lacks the "loading indicator", however it wins in terms of number of lines of code and does not require additional controls. I first added the dispatch_cancelable_block.h file to my project (from this repo), then defined the following class variable: __block dispatch_cancelable_block_t searchBlock;.
My search code now looks like this:
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
if (searchBlock != nil) {
//We cancel the currently scheduled block
cancel_block(searchBlock);
}
searchBlock = dispatch_after_delay(searchBlockDelay, ^{
//We "enqueue" this block with a certain delay. It will be canceled if the user types faster than the delay, otherwise it will be executed after the specified delay
[self loadPlacesAutocompleteForInput:searchText];
});
}
Notes:
The loadPlacesAutocompleteForInput is part of the LPGoogleFunctions library
searchBlockDelay is defined as follows outside of the #implementation:
static CGFloat searchBlockDelay = 0.2;
A quick hack would be like so:
- (void)textViewDidChange:(UITextView *)textView
{
static NSTimer *timer;
[timer invalidate];
timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:#selector(requestNewDataFromServer) userInfo:nil repeats:NO];
}
Every time the text view changes, the timer is invalidated, causing it not to fire. A new timer is created and set to fire after 1 second. The search is only updated after the user stops typing for 1 second.
Swift 4 solution, plus some general comments:
These are all reasonable approaches, but if you want exemplary autosearch behavior, you really need two separate timers or dispatches.
The ideal behavior is that 1) autosearch is triggered periodically, but 2) not too frequently (because of server load, cellular bandwidth, and the potential to cause UI stutters), and 3) it triggers rapidly as soon as there is a pause in the user's typing.
You can achieve this behavior with one longer-term timer that triggers as soon as editing begins (I suggest 2 seconds) and is allowed to run regardless of later activity, plus one short-term timer (~0.75 seconds) that is reset on every change. The expiration of either timer triggers autosearch and resets both timers.
The net effect is that continuous typing yields autosearches every long-period seconds, but a pause is guaranteed to trigger an autosearch within short-period seconds.
You can implement this behavior very simply with the AutosearchTimer class below. Here's how to use it:
// The closure specifies how to actually do the autosearch
lazy var timer = AutosearchTimer { [weak self] in self?.performSearch() }
// Just call activate() after all user activity
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
timer.activate()
}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
performSearch()
}
func performSearch() {
timer.cancel()
// Actual search procedure goes here...
}
The AutosearchTimer handles its own cleanup when freed, so there's no need to worry about that in your own code. But don't give the timer a strong reference to self or you'll create a reference cycle.
The implementation below uses timers, but you can recast it in terms of dispatch operations if you prefer.
// Manage two timers to implement a standard autosearch in the background.
// Firing happens after the short interval if there are no further activations.
// If there is an ongoing stream of activations, firing happens at least
// every long interval.
class AutosearchTimer {
let shortInterval: TimeInterval
let longInterval: TimeInterval
let callback: () -> Void
var shortTimer: Timer?
var longTimer: Timer?
enum Const {
// Auto-search at least this frequently while typing
static let longAutosearchDelay: TimeInterval = 2.0
// Trigger automatically after a pause of this length
static let shortAutosearchDelay: TimeInterval = 0.75
}
init(short: TimeInterval = Const.shortAutosearchDelay,
long: TimeInterval = Const.longAutosearchDelay,
callback: #escaping () -> Void)
{
shortInterval = short
longInterval = long
self.callback = callback
}
func activate() {
shortTimer?.invalidate()
shortTimer = Timer.scheduledTimer(withTimeInterval: shortInterval, repeats: false)
{ [weak self] _ in self?.fire() }
if longTimer == nil {
longTimer = Timer.scheduledTimer(withTimeInterval: longInterval, repeats: false)
{ [weak self] _ in self?.fire() }
}
}
func cancel() {
shortTimer?.invalidate()
longTimer?.invalidate()
shortTimer = nil; longTimer = nil
}
private func fire() {
cancel()
callback()
}
}
Swift 2.0 version of the NSTimer solution:
private var searchTimer: NSTimer?
func doMyFilter() {
//perform filter here
}
func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {
if let searchTimer = searchTimer {
searchTimer.invalidate()
}
searchTimer = NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: #selector(MySearchViewController.doMyFilter), userInfo: nil, repeats: false)
}
Please see the following code which i've found on cocoa controls. They are sending request asynchronously to fetch the data. May be they are getting data from local but you can try it with the remote API. Send async request on remote API in background thread. Follow below link:
https://www.cocoacontrols.com/controls/jcautocompletingsearch
We can use dispatch_source
+ (void)runBlock:(void (^)())block withIdentifier:(NSString *)identifier throttle:(CFTimeInterval)bufferTime {
if (block == NULL || identifier == nil) {
NSAssert(NO, #"Block or identifier must not be nil");
}
dispatch_source_t source = self.mappingsDictionary[identifier];
if (source != nil) {
dispatch_source_cancel(source);
}
source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(source, dispatch_time(DISPATCH_TIME_NOW, bufferTime * NSEC_PER_SEC), DISPATCH_TIME_FOREVER, 0);
dispatch_source_set_event_handler(source, ^{
block();
dispatch_source_cancel(source);
[self.mappingsDictionary removeObjectForKey:identifier];
});
dispatch_resume(source);
self.mappingsDictionary[identifier] = source;
}
More on Throttling a block execution using GCD
If you're using ReactiveCocoa, consider throttle method on RACSignal
Here is ThrottleHandler in Swift in you're interested
You can use DispatchWorkItem with Swift 4.0 or above. It's a lot easier and makes sense.
We can execute the API call when the user hasn't typed for 0.25 second.
class SearchViewController: UIViewController, UISearchBarDelegate {
// We keep track of the pending work item as a property
private var pendingRequestWorkItem: DispatchWorkItem?
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
// Cancel the currently pending item
pendingRequestWorkItem?.cancel()
// Wrap our request in a work item
let requestWorkItem = DispatchWorkItem { [weak self] in
self?.resultsLoader.loadResults(forQuery: searchText)
}
// Save the new work item and execute it after 250 ms
pendingRequestWorkItem = requestWorkItem
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250),
execute: requestWorkItem)
}
}
You can read the full article about it from here
Disclamer: I am the author.
If you need vanilla Foundation based throttling feature,
If you want just one liner API without going into reactive, combine, timer, NSObject cancel and anything complex,
Throttler can be the right tool to get your job done.
You can use throttling without going reactive as below:
import Throttler
for i in 1...1000 {
Throttler.go {
print("throttle! > \(i)")
}
}
// throttle! > 1000
import UIKit
import Throttler
class ViewController: UIViewController {
#IBOutlet var button: UIButton!
var index = 0
/********
Assuming your users will tap the button, and
request asyncronous network call 10 times(maybe more?) in a row within very short time nonstop.
*********/
#IBAction func click(_ sender: Any) {
print("click1!")
Throttler.go {
// Imaging this is a time-consuming and resource-heavy task that takes an unknown amount of time!
let url = URL(string: "https://jsonplaceholder.typicode.com/todos/1")!
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
guard let data = data else { return }
self.index += 1
print("click1 : \(self.index) : \(String(data: data, encoding: .utf8)!)")
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}
click1!
click1!
click1!
click1!
click1!
click1!
click1!
click1!
click1!
click1!
2021-02-20 23:16:50.255273-0500 iOSThrottleTest[24776:813744]
click1 : 1 : {
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": false
}
if you want some specific delay seconds:
import Throttler
for i in 1...1000 {
Throttler.go(delay:1.5) {
print("throttle! > \(i)")
}
}
// throttle! > 1000
Swift 5.0
Based on GSnyder response
//
// AutoSearchManager.swift
// BTGBankingCommons
//
// Created by Matheus Gois on 01/10/21.
//
import Foundation
/// Manage two timers to implement a standard auto search in the background.
/// Firing happens after the short interval if there are no further activations.
/// If there is an ongoing stream of activations, firing happens at least every long interval.
public class AutoSearchManager {
// MARK: - Properties
private let shortInterval: TimeInterval
private let longInterval: TimeInterval
private let callback: (Any?) -> Void
private var shortTimer: Timer?
private var longTimer: Timer?
// MARK: - Lifecycle
public init(
short: TimeInterval = Constants.shortAutoSearchDelay,
long: TimeInterval = Constants.longAutoSearchDelay,
callback: #escaping (Any?) -> Void
) {
shortInterval = short
longInterval = long
self.callback = callback
}
// MARK: - Methods
public func activate(_ object: Any? = nil) {
shortTimer?.invalidate()
shortTimer = Timer.scheduledTimer(
withTimeInterval: shortInterval,
repeats: false
) { [weak self] _ in self?.fire(object) }
if longTimer == nil {
longTimer = Timer.scheduledTimer(
withTimeInterval: longInterval,
repeats: false
) { [weak self] _ in self?.fire(object) }
}
}
public func cancel() {
shortTimer?.invalidate()
longTimer?.invalidate()
shortTimer = nil
longTimer = nil
}
// MARK: - Private methods
private func fire(_ object: Any? = nil) {
cancel()
callback(object)
}
}
// MARK: - Constants
extension AutoSearchManager {
public enum Constants {
/// Auto-search at least this frequently while typing
public static let longAutoSearchDelay: TimeInterval = 2.0
/// Trigger automatically after a pause of this length
public static let shortAutoSearchDelay: TimeInterval = 0.75
}
}

How to synchronize different actions in Swift Language for IOS

I created a timer in one class, and tried to do something else in another class while timer works, and do other thing while timer stops. For example, show every second when timer works. I simplified the code as below. How to realize that?
import Foundation
import UIView
class TimerCount {
var timer: NSTimer!
var time: Int!
init(){
time = 5
timer = NSTimer.scheduledTimerWithTimeInterval( 1.0 , target: self, selector: Selector("update"), userInfo: nil, repeats: true)
}
func update(){
if(time > 0) {
time = time - 1
// do something while timer works
}
else{
timer.invalidate()
timer = nil
time = 5
}
}
}
class Main: UIView {
var Clock: TimerCount!
override func viewDidLoad() {
Clock = TimerCount()
//? do something else while clock works
// ? do other things while clock stops
// FOR EXAMPLE: show every second when timer works
if(Clock.time > 0){
println(Clock.time)
}else{
println("clocker stops")
}
}
}
viewDidLoad is most likely only going to be called once. You could simply make the update method be in your Main object and then pass that instance of main and that update method into the scheduledTimerWithTimerInterval call. Otherwise you need a new method in the Main class to call from the timerCount class and pass in the int for the current time.
This is in your Main class:
func updateMethodInMain(timeAsInt: Int){
//do stuff in Main instance based on timeAsInt
}
This is what you have in your timer class:
func update(){
if(time > 0) {
time = time - 1
instanceNameForMain.updateMethodInMain(time)
}
else{
timer.invalidate()
timer = nil
time = 5
}
}
}

Resources