iOS disable UIButton for 30 minutes - ios

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

Related

How to call API only once in 30min in swift5?

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.

Multiple timers at once ios

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.

how to detect if a task has execute in the last 2 minutes in swift 3?

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

NSTimer stops when view controller is not the selected tab or not showing

I have a strange problem with my countdown timer. It fires off normally when my start button is hit, and is reinstantiated correctly when I close the app and relaunch it again. However, when I select a different tab and stay there for a while, it stops counting down, then resumes counting down from where it left off when I show the countdown tab again.
For example, if the timer is now at 00:28:00 (format is HH:MM:SS), select some other tab, stay there for 5 minutes, and then go back to the timer tab, it's only at the 27:52 mark. When I close the app (double tap the home button, swipe up my app) and reopen it, it starts off at a more reasonable 22:50 mark.
I've posted the relevant code from the class to show how I'm setting up the timer, but a summary of what it does:
I have plus (+) and minus (-) buttons somewhere that, when tapped, call recalculate().
recalculate() fires off a CalculateOperation.
A CalculateOperation computes for the starting HH:MM:ss based on the addition/removal of a new record. The successBlock of a CalculateOperation executes in the main thread.
A CalculateOperation creates the NSTimer in the successBlock if the countdownTimer hasn't been created yet.
The NSTimer executes decayCalculation() every 1 second. It reduces the calculation.timer by 1 second by calling tick().
Code:
class CalculatorViewController: MQLoadableViewController {
let calculationQueue: NSOperationQueue // Initialized in init()
var calculation: Calculation?
var countdownTimer: NSTimer?
func recalculate() {
if let profile = AppState.sharedState.currentProfile {
// Cancel all calculation operations.
self.calculationQueue.cancelAllOperations()
let calculateOperation = self.createCalculateOperation(profile)
self.calculationQueue.addOperation(calculateOperation)
}
}
func decayCalculation() {
if let calculation = self.calculation {
// tick() subtracts 1 second from the timer and adjusts the
// hours and minutes accordingly. Returns true when the timer
// goes down to 00:00:00.
let timerFinished = calculation.timer.tick()
// Pass the calculation object to update the timer label
// and other things.
if let mainView = self.primaryView as? CalculatorView {
mainView.calculation = calculation
}
// Invalidate the timer when it hits 00:00:00.
if timerFinished == true {
if let countdownTimer = self.countdownTimer {
countdownTimer.invalidate()
}
}
}
}
func createCalculateOperation(profile: Profile) -> CalculateOperation {
let calculateOperation = CalculateOperation(profile: profile)
calculateOperation.successBlock = {[unowned self] result in
if let calculation = result as? Calculation {
self.calculation = calculation
/* Hide the loading screen, show the calculation results, etc. */
// Create the NSTimer.
if self.countdownTimer == nil {
self.countdownTimer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("decayCalculation"), userInfo: nil, repeats: true)
}
}
}
return calculateOperation
}
}
Well, if I leave the app in some other tab and not touch the phone for a while, it eventually goes to sleep, the app resigns active, and enters the background, which stops the timer.
The solution was to set my view controller as a listener to the UIApplicationWillEnterForegroundNotification and call recalculate to correct my timer's countdown value.

UIDatePicker bug? UIControlEventValueChanged after hitting minimum internal

I've run into a weird effect that sure looks like a bug in iOS7 -- but often in the past, when I have thought I found a bug in Apple's APIs, it has turned out to be my own misunderstanding.
I have a UIDatePicker with datePickerMode = UIDatePickerModeCountDownTimer and minuteInterval = 5. I initialize the duration to 1 hour, and present it to the user, where it appears as a two-column picker with hours and minutes. (So far so good.)
The user is thinking "20 minutes," and so scrolls the Hour column to 0. At this point the picker reads 0 hours and 0 minutes, and iOS7 is not cool with that, so it automatically scrolls the minute wheel to 5. My UIControlEventValueChanged handler gets invoked, and the countDownDuration reads 5 minutes. (Still good.)
Now the user grabs the minute wheel and drags it to 20. AND... my UIControlEventValueChanged handler does not get called. (Bad.)
If I have some other event in the UI check the date picker at this point, I do see the countDownDuration is set to 20. But I had no way of knowing that the user changed it, at the moment it was changed. This is very repeatable: it always happens on the first change AFTER the picker refuses to be set to 0 (advancing itself to 5 minutes).
Note that this is in iOS7; it does not occur in iOS6 (perhaps because the picker there is perfectly content to be set to 0 minutes).
So... am I missing something here? Or is this a genuine bug in iOS7? And in the latter case, does anybody know a work-around better than having some timer periodically check the current interval?
I can also confirm that the iOS 7.0.3 UIDatePicker has a bug in it when used in UIDatePickerModeCountDownTimer mode. The picker does not fire the target-action associated with the UIControlEventValueChanged event the first time the user changes the value by scrolling the wheels. It works fine for subsequent changes.
Below is an efficient workaround. Simply enclose the code that sets the initial value of the countDownDuration in a dispatch block to the main loop. Your target-action method will fire every time the wheels are rotated to a new value. This approach has almost no overhead and works quite well on an iPhone 4 and iPad 4.
dispatch_async(dispatch_get_main_queue(), ^{
self.myDatePicker.countDownDuration = (NSTimeInterval) aNewDuration ;
});
Swift 5:
DispatchQueue.main.async {
self.myDatePicker.countDownDuration = aNewDuration
}
I was still hitting this issue in 7.1 but adding the following to the UIControlEventValueChanged handler fixed it for me.
// Value Changed Event is not firing if minimum value hit
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.3 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[self.myDatePicker setCountDownDuration: self.myDatePicker.countDownDuration];
});
If someone still having problems with datepicker...
I'm using Swift / iOS 8 / Xcode 6.3
So solve the problem you should no use
picker.countDownDuration = NSTimeInterval
instead, use setDate
picker.setDate(NSDate, animated: true)
it works direct on viewDidLoad(), don't need to use `queues
For Swift 3:
DispatchQueue.main.async(execute: {
yourPicker.countDownDuration = TimeInterval()
})
Since no answer has been accepted yet. The simplest solution that worked for me in swift 3 is to simply do
datePicker.countDownDuration = seconds
in viewDidAppear instead of viewDidLoad
If someone still having problems with datepicker...
I'm using Swift / iOS 8 / Xcode 6.3
To solve the problem you should not use picker.countDownDuration = NSTimeInterval. Use .setDate(NSDate, animated: true).
it works direct on viewDidLoad(), don't need to use queues
The complete snippet:
override func viewDidLoad() {
super.viewDidLoad()
picker.setDate(setDateFromSeconds(seconds), animated: true)
}
func setDateFromSeconds(seconds: Double) -> (NSDate) {
let intSeconds = Int(seconds)
let minutes = (intSeconds / 60) % 60
let hours = intSeconds / 3600
let dateString = NSString(format: "%0.2d:%0.2d", hours, minutes)
let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "hh:mm"
return dateFormatter.dateFromString(dateString as String) as NSDate!
}
Swift 4
#IBOutlet weak var fromPickerView: UIDatePicker!
#objc func toPickerViewDateChanged() {
fromPickerView.minimumDate = toPickerView.date
}
override func viewDidLoad() {
super.viewDidLoad()
toPickerView.backgroundColor = UIColor.white
toPickerView.tintColor = .black
toPickerView.maximumDate = Date()
toPickerView.addTarget(self, action:
#selector(toPickerViewDateChanged), for: UIControlEvents.valueChanged)

Resources