I am calling one API, but that data is not changing frequently and I am storing data in core data. I want that API should call only once in 30 min. is there any better approach of calling API only if it exceed 30 min from last API call. I can think of Timer based, but like to know if there is any other better way to do same?
If you are calling the api randomly then holding a variable in memory for the last successful call might be good enough. If you want the API to be called automatically you'll best approach would be to set up a timer.
If you want your app to prohibit to make a new call in 30 min, then this would be a quick example:
(wrote this for a playground)
var lastCheck: Date?
let minimumMinutes = 60.0
func makeNetworkCall() {
if let lastCheckDate = lastCheck, lastCheckDate.timeIntervalSinceNow < (30 * minimumMinutes) {
debugPrint("Not making call, Didn't go 30 min yet")
return
}
lastCheck = Date()
debugPrint("Making network call!")
// ... make call
}
makeNetworkCall() // Should make call
makeNetworkCall() // Should not make call
makeNetworkCall() // Should not make call
I haven't tested the code above, but it should work.
To just limit service calling for a specific time (ex 30 minutes), you can store last service called date and use it to decide to do a call or not. You can store the date in memory or persistent storage depend on you need.
UserDefaults can be an option to store last date for persistance. There is a sample implementation below;
func saveLastServiceCalledDate() {
UserDefaults.standard.set(Date(), forKey: "lastServiceCallDate")
}
func isCalledInLast30Min() -> Bool {
guard let lastDate = UserDefaults.standard.value(forKey: "lastServiceCallDate") as? Date else { return false }
let timeElapsed: Int = Int(Date().timeIntervalSince(lastDate))
return timeElapsed < 30 * 60 // 30 minutes
}
func serviceCall() {
// ignore if called in last 30 minutes
if isCalledInLast30Min() { return }
// save current date
saveLastServiceCalledDate()
// do service call
}
My suggestion is to use DispatchSourceTimer because it can be restarted at any time.
Call startTimer() in viewDidLoad and in applicationWillBecomeActive to get the most recent data when the application becomes active
var timer : DispatchSourceTimer!
func startTimer()
{
if timer == nil {
timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global())
timer.schedule(deadline: .now(), repeating: 30.0)
timer.setEventHandler {
self.callAPI()
}
timer.activate()
} else {
timer.schedule(deadline:.now(, repeating: 30.0)
}
}
There is no way but timer
1- Create a Timer with 1 minute schedule
2- Timer function checks current timeStamp against a stored 1 say in defaults
3- If stored value is nil or exceeded 30 minutes gap between the current call the api
4- When you call the api update the stored value with the current 1
The reason behind making it a stored value not global is freguently opening and closing the app won't cause non-new api calls
let current = Date().timeIntervalSince1970
let stored = UserDefaults.standard.double(forKey:"stored")
if stored == 0 || current - stored >= 30.0 {
// call the api && update stored value
}
You haven't mentioned whether this should happen in background or foreground? Because based on that only we need to go for the solution. In case if u are wondering about update the data in the background, you should check apples BGTaskBackground. But the problem with this is, you can't decide the time to trigger. You can only give minimumFetchingInterval, which is not guaranteed but will be decided by the system/is.
Incase if you are looking to update in the foreground, just go with the timer approach you are talking about. Use any background queues to do that job. Queues will help you out in dispatching specific task at specific time with delay method.
Related
I am making an app where the user can have multiple timers going at once and see them in a list view.
I am aware that there are 2 main options for working out time:
Subtract the date started from current date (current date-start date)
OR
Use an NSTimer and take away 1 second every second from each active timer.
I have previously been using the latter, but having looked around the internet I am starting to think that the data one may be better.
Please could you let me know which you think is best to use, and if you chose the first one (dates), please could you provide some sample code on how to use it.
You can Use an NSTimer and take away 1 second every second from each active timer. You can use this class.
class CustomTimer {
typealias Update = (Int)->Void
var timer:Timer?
var count: Int = 0
var update: Update?
init(update:#escaping Update){
self.update = update
}
func start(){
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(timerUpdate), userInfo: nil, repeats: true)
}
func stop(){
if let timer = timer {
timer.invalidate()
}
}
/**
* This method must be in the public or scope
*/
#objc func timerUpdate() {
count += 1;
if let update = update {
update(count)
}
}
}
To use multiple timer you can create multiple instance of CustomTimer, Example Code:
let timer1 = CustomTimer { (seconds) in
// do whatever you want
}
timer1.start()
let timer2 = CustomTimer { (seconds) in
// do whatever you want
}
timer2.start()
NOTE:
timerUpdate method will be called exactly at 1 second interval. to keep some space for function execution we can set interval to 0.9 or 0.95 according to time taken by execution.
You use both. You have one Timer that repeats every second. The handler for the Timer then iterates through your list of start dates for each of the user's timers and you update the display for each based on the current date.
I have a project that has Json in it - when a user come back to the home page because of view didLoad method the app will start getting son again and I want this But I want the app detect that if the user has came back to the home page in the last 2 minutes the app doesn't get the json - simply I want to run a task when user go to a view controller but if user has came back to the view controller in the last 2 minutes the app doesn't execute task and for example if the user open the app and go to the another page after 3 minutes when he came back to the home page the task start - as you see here I can use timer but the timer will run the task every minutes I want to limit this as I said
weak var timer: Timer?
func startTimer() {
timer = Timer.scheduledTimer(withTimeInterval: 60.0, repeats: true) { [weak self] _ in
// do something here
}
}
func stopTimer() {
timer?.invalidate()
}
// if appropriate, make sure to stop your timer in `deinit`
deinit {
stopTimer()
}
You would need to save the Date(time) object when the task gets completed in the completion handler and then next time when you are about to start the task you would need to check the time elapsed.
Set a Date in UserDefaults in the completion handler of your task.
Before proceeding to start task check whether this Date exists and if exists, then the elapsed time is greater than 120 seconds(2 minutes) or not.
func startTaskIfPossible() {
let date = UserDefaults.standard.object(forKey: "taskCompletionDate") as? Date
guard let prevCompletionDate = date else {
startTask()
return
}
guard Date().timeIntervalSince(prevCompletionDate) > 120 else {
return
}
startTask()
}
func startTask() {
//Set Date in userdefaults in completion handler of task
// UserDefaults.standard.setValue(Date(), forKey: "taskCompletionDate")
}
Save the last json request execution time in one of the keys in your NSUserdefaults.
let userDefaults = NSUserDefaults.standardUserDefaults()
// save to user defaults
userDefaults.setObject(NSDate(), forKey: "LastExecutionDate")
Everytime your user come to home page and you want to fire json request, just compare it with last execution time. If its more than 2 mins fire it otherwise don't.
// retrieve from user defaults
let lastExecutionDate = userDefaults.objectForKey("LimitReachedOnDate") as? NSDate
Then your currentDate - lastExecutionDate > 180 seconds. This is just a algo and not the exact code for date comparison but i guess you will get it. this
I want to measure how long (in seconds) it takes users to do certain things in my app. Some examples are logging in, pressing a button on a certain page, etc.
I am using an NSTimer for that. I am starting it in the viewDidLoad of a specific page, and stopping it at the point that I want to measure.
I also want to measure cumulative time for certain things. I would like to start the timer on the log-in screen, and then continue the timer until the user gets to the next view controller and clicks on a certain button.
I'm not sure how to do this. Should create a global variable in my app delegate? Or is there some other better way?
No need for an NSTimer, you just need to record the start times and compare them to the stop times. Try using a little helper class such as:
class MyTimer {
static let shared = MyTimer()
var startTimes = [String : Date]()
func start(withKey key: String) {
startTimes[key] = Date()
}
func measure(key: String) -> TimeInterval? {
if let start = startTimes[key] {
return Date().timeIntervalSince(start)
}
return nil
}
}
To use this, just call start(withKey:) right before you start a long-running task.
MyTimer.shared.start(withKey: "login")
Do something that takes a while and then call measure(key:) when you're done. Because MyTimer is a singleton, it can be called from anywhere in your code.
if let interval = MyTimer.shared.measure("login") {
print("Logging in time: \(interval)")
}
If you're using multiple threads, you may to to add some thread safety to this, but it should work as is in simple scenarios.
I am making an app where people can request two orders per hour maximum. I would like to disable the "Order" UIButton for a full 30 minutes once it is pressed. Can anybody suggest the most efficient way to do this but that fully prevents the user from ordering twice even if the app is killed? Thanks.
At a high level you need to do two things:
Calculate the NSDate for when the button should be enabled again and store that date in NSUserDefaults.
Start an NSTimer that goes off in 30 minutes. Enable the disabled button when the timer goes off and remove the date from NSUserDefaults.
More than likely the app will go into the background and the timer will stop long before the 30 minutes. This means that your app needs to stop the timer when it goes into the background. And when it returns to the foreground, you look at the date in NSUserDefaults and see how much time is left. If the time is already past, enable the button and delete the date from NSUserDefaults. If not, start another timer to go off after the needed amount of time as in step 2 above.
Here's the approach I thought of earlier to your problem. The three things you'll use are NSDate, NSTimeInterval, and NSUserDefaults
// I threw this in Xcode to aide me in typing this solution.
// You probably dragged a button from storyboard...leave it like that.
let orderButton: UIButton?
// Put toggleOrderButtonAvailability() in viewDidLoad and viewDidAppear
func toggleOrderButtonAvailability() {
// get current date
let currentDate = NSDate()
// we're not sure if there's a value for this, but we're creating a variable for it
// it will nil if the user hasn't placed an order
var lastOrderDate: NSDate?
// we're creating a variable to check
var timeSinceLastOrder: NSTimeInterval?
// if a value for the lastOrderDate saved in NSUserDefaults, then...
if NSUserDefaults.standardUserDefaults().objectForKey("lastOrderDate") != nil {
lastOrderDate = NSUserDefaults.standardUserDefaults().valueForKey("lastOrderDate") as? NSDate
// calculate minutes since last order
// 1800 seconds = 60 seconds per minute X 30 minutes
timeSinceLastOrder = (currentDate.timeIntervalSinceDate(lastOrderDate!)) / 1800
if timeSinceLastOrder < 30 {
orderButton?.enabled = false
// Some alert to let the user know they can't order for another X minutes
// TODO: you could create a class variable like "timeUntilButtonReenabled"
// and set it here, then the timer would run and call this method when it's
// done to re-enable the button. Set the timer in viewDidAppear()
} else {
orderButton?.enabled = true
}
}
}
You'll also want to set the lastOrderDate when you place an order and you can call the method we just created to disable the button when you place an order.
#IBAction func orderButtonAction(sender: UIButton) {
// Whatever you do when you send an order
// Set the lastOrderDate & disable the button
let currentDate = NSDate()
NSUserDefaults.standardUserDefaults().setObject(currentDate, forKey: "lastOrderDate")
toggleOrderButtonAvailability()
}
You should save the order date in NSUserdefaults.Once app launched,check the last order date and make an count down timer for that.
When the button is pressed, disable the button and log the current time using an NSDate object. To ensure it persists even if the app is killed, make sure you write it-- if you're app isn't already using a data system, NSUserDefaults is probably the easiest way to get about this.
Next, you need to create a mechanism for the button to enable again. The easiest reliable method to do so is by creating an NSTimer that checks whether or not the logged date is over 30 minutes ago, and if so, enable the button.
Here's an example of how to do this in Swift:
class ViewController: UIViewController {
#IBOutlet weak var btn: UIButton!
var enableTimer: NSTimer!
let defaults = NSUserDefaults.standardUserDefaults()
//Because this is a computed property, it auto-saves and persists
var lastPushed: NSDate {
get {
if let unwrappedDate = defaults.objectForKey("lastPushed") as? NSDate {
return unwrappedDate
} else { //If date not yet set
return NSDate(timeIntervalSince1970: 0)
}
} set { //NSDate is already compatible with NSUserDefaults
defaults.setObject(newValue, forKey: "lastPushed")
}
}
override func viewDidLoad() {
startTimer()
}
func startTimer() {
enableTimer = NSTimer.scheduledTimerWithTimeInterval(30, self, Selector("enableTim:"), nil, true)
}
#IBAction btnPressed() {
lastPushed = NSDate() //NSDate with current time
startTimer()
btn.enabled = false
}
func enableTim(timer: NSTimer) {
if (lastPushed.timeIntervalSinceNow < -1800) { //If last pressed more than 30 minutes ago
btn.enabled = true
enableTimer.stop()
}
}
}
I am using a Particle Core to get the temperature from my room. The temperature is accessed through the cloud, which is being constantly updated in a variable. This is how I access the variable and display it:
func updateTemp(){
let seconds = 3.0
let delay = seconds * Double(NSEC_PER_SEC) // nanoseconds per seconds
let dispatchTime = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(dispatchTime, dispatch_get_main_queue(), {
self.myPhoton?.getVariable("tempF", completion: { (result:AnyObject!, error:NSError!) -> Void in
if let _ = error {
print("Failed reading temperature from device")
}
else {
if let larry = result as? Int {
self.temp.text="\(larry)˚"
self.truth++ //Once a value has been found, update the count.
}
}
})
})
}
override func viewDidLoad() {
sparkStart()
}
override func viewDidLayoutSubviews() {
updateTemp()
NSTimer.scheduledTimerWithTimeInterval(100.0, target: self, selector: "updateTemp", userInfo: nil, repeats: true) //Gaurantees that the app is updated every 100 seconds. That way we have a fresh temperature often.
//Stop the spinning once a value has been found
if truth == 1{
activity.stopAnimating()
activity.removeFromSuperview()
}
}
Since this is my Particle Core detecting the temperature from environment, the temperature variable is constantly changing. However, when I use NSTimer, the code does not get updated in the time specified. Instead, it begins by updating based on the specified time, but then the time starts decreases exponentially and the variable is updated every 0.001 seconds or so. Any thoughts?
Im assuming what we see is not the full code. In your viewDidLayoutSubviews function, you call updateTemp twice. Once explicitly and once via timer callback.
Your updateTemp function schedules the network call in the main run loop, that's where the timer is also running. The dispatch_after function queues the execution of the readout updates one after the other. I am now assuming, that something in your display code causes repeated triggers of viewDidLayoutSubviews, each of which schedules two new updates etc. Even if the assumption is false (there are a couple of other possibilities due to network code being slow and the timer also running in the main run loop), I am guessing if you drop the explicit call to updateTemp you'll lose the "exponential" and should be fine.
In general, as the web call is largely asynchronous, you could just use the timer and call your sensor directly or if you feel GCD has an important performance advantage switch to dispatch_async and apply for the next available queue with each call via calling dispatch_get_global_queue