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)
Related
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.
I have a UIButton within an app that will allow the user to send a request to an API.
How can I prevent the user from pressing this button more than X times per second?
I assume you want to just ignore taps on the button if they are too frequent, but you don't need to set the button to appear disabled while taps are ignored. (If you want to change the button appearance, you'll need to use an NSTimer or other delayed action to re-enable the button, which is a significant complication.)
If you change the requirement to “at least 1/X seconds between taps”, it's a little simpler. For example, instead of allowing at most 4 taps per second, we'll ignore a tap if it comes less than 1/4 seconds after the prior tap.
To implement this requirement, store the time of the last tap. When a tap arrives, see if 1/X seconds have elapsed since the last tap. If not, ignore the tap.
private let minimumTapInterval = 1 / CFTimeInterval(4)
private var lastTapTime = CFAbsoluteTime(0)
#IBAction func buttonWasTapped(sender: AnyObject?) {
let now = CFAbsoluteTimeGetCurrent()
guard now >= lastTapTime + minimumTapInterval else { return }
lastTapTime = now
sendAPIRequest()
}
If you really want to implement a “no more than X taps per second” requirement, you can store the times of the accepted taps. When a new tap comes in, throw away any stored times older than one second. If there are still at least X stored times, ignore the new tap.
private let maxTapsPerSecond = 4
private var tapTimes = [CFAbsoluteTime]()
#IBAction func buttonWasTapped(sender: AnyObject?) {
let now = CFAbsoluteTimeGetCurrent()
let oneSecondAgo = now - 1
tapTimes = tapTimes.filter { $0 >= oneSecondAgo }
// All elements of tapTimes are now within the last second.
guard tapTimes.count < maxTapsPerSecond else { return }
tapTimes.append(now)
sendAPIRequest
}
You can use the isEnabled property of your UIButton.
For the target of your button, add the following at the end of your #objc selector function:
#objc func handleMyButtonPress() {
self.someOtherFunction()
myButton.isEnabled = false
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
myButton.isEnabled = true
}
}
The .now() + 1 deadline is basically saying, after 1 second the button will become enabled again and your users can interact with it.
For a 5 minute delay, you could use .now() + (60 * 5)
I use this myself and it works reliably in almost any situation.
Hopefully, this answer can help you or others with the same problem.
So I've just started trying out coding in swift and I'm creating an extremely basic app (purely to experiment and learn) where you click a button and two playing cards appear on the screen.
I'm trying to get it so that when the two playing cards are the same, the button to play again disables, the program pauses for a few seconds, then the button re-enables (so later I can add some 'win' text during the pause time).
Now the button and pausing fully works besides one problem. When testing, the program pauses and then when it finishes pausing the display then updates to show the two cards being equal. But while it pauses, it shows two random non equal cards.
I'm not sure why, seeing as the cards update before I check if they're equal, but I'm new to swift (literally last few days) so not sure how it works.
Any ideas? :)
#IBAction func playRoundTapped(sender: UIButton) {
// Change the card image each time the play button is pressed using a random number generator
self.firstCardImageView.image = UIImage(named: String(format: "card%i", arc4random_uniform(13)+1))
self.secondCardImageView.image = UIImage(named: String(format: "card%i", arc4random_uniform(13)+1))
// Check if the cards are equal
if firstCardImageView.image == secondCardImageView.image && firstCardImageView.image != "card" {
playRoundButton.userInteractionEnabled=false;
NSThread.sleepForTimeInterval(4)
playRoundButton.userInteractionEnabled=true;
}
}
Don't sleep in the main thread as this will stop all interactions with your app. You need to replace:
NSThread.sleepForTimeInterval(4)
playRoundButton.userInteractionEnabled=true;
with:
let enableTime = dispatch_time(DISPATCH_TIME_NOW, Int64(4 * Double(NSEC_PER_SEC)))
dispatch_after(enableTime, dispatch_get_main_queue()) {
playRoundButton.userInteractionEnabled=true;
}
First off, a better solution is not pausing at all and using dispatch_after to change the playRoundButton button state after 4 seconds.
If you want to stick with pausing, then you should give time for the UI to update itself before pausing. E.g.,
dispatch_async(dispatch_get_main_thread(), {
//Check if both cards are equal
if firstCardImageView.image == secondCardImageView.image && firstCardImageView.image != "card" {
playRoundButton.userInteractionEnabled=false;
NSThread.sleepForTimeInterval(4)
playRoundButton.userInteractionEnabled=true;
}
});
The fact is, when you assign a new image to your buttons, the button is actually redrawn on screen only at the next run-loop cycle, so if you pause before that is run, no visual change can be seen...
Keep in mind that pausing on the main thread will make your app unresponsive during that timeframe.
You're comparing two UIImage instances, which doesn't work with == because it will only compare the pointers. In your case, it would be much easier to compare the numbers that generated those images.
Other than that, you're pausing the main thread, which takes care of updating the user interface, so it doesn't actually get a chance to do so. One way to solve this problem is by using NSTimer.
#IBAction func playRoundTapped(sender: UIButton) {
let firstNumber = arc4random_uniform(13) + 1
let secondNumber = arc4random_uniform(13) + 1
firstCardImageView.image = UIImage(named: "card\(firstNumber)")
secondCardImageView.image = UIImage(named: "card\(secondNumber)")
if firstNumber == secondNumber {
playRoundButton.userInteractionEnabled = false;
NSTimer.scheduledTimerWithTimeInterval(4.0, target: self, selector: "enableButton", userInfo: nil, repeats: false)
}
}
func enableButton() {
playRoundButton.userInteractionEnabled = true;
}
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()
}
}
}
Newbie to IOS programming - learning through Swift. I'm writing a simple "slot machine / dice game".
I'm trying to show the user a flashing sequence of rolls before the "actual" roll appears.
func doFancyDiceRoll() {
for x in 1...100 {
Die1.image = PipsImg[RollOne()]
Die2.image = PipsImg[RollOne()]
Die3.image = PipsImg[RollOne()]
}
}
Die1, etc., are defined as generic UIImage views.
In any case, I'm not seeing the 100x iterations, just the images at the end of the loop. I'm assuming that either it redraws "too fast" or that IOS is trying to be smart, and only draws the last images so as to conserve resources.
I'll wildly guess that I need to either implement some kind of delay here, or, IOS needs to be told to explicitly draw out my images, and not try to outthink my intent.
For the delay, I've seen something about NSTimer, but nothing I saw seems to simply say something like "pause for .05" second, and the whole construct was unclear as they were ObjC examples/conversions.
(Note: I've simplified things here --- normally, I would store the value of RollOne() so I can use it later. I also would like to make an array (or collection?) like Die[1].image, but that is another question.)
========== Edit =======
OK, so I'm following up with more of my original code, merged in with that of #doctordoder so we can discuss a bit better. Hopefully that is kosher. (If this appended question is too long, please advise me on the best way to post a lengthy follow-up directly.)
import UIKit
class ViewController: UIViewController {
//( stripping out stuff unneeded for discussion )
// refers to same label below - works but kosher??
#IBOutlet var btnRoll_x: UIView
#IBAction func btnRoll(sender: AnyObject) {
triggerRoll()
}
var timer : NSTimer? = nil
var rolls : Int = 0
func triggerRoll() {
//hiding is bad UI, but until i know how to "disable & dim"
btnRoll_x.hidden = true
timer = NSTimer.scheduledTimerWithTimeInterval(0.10, target: self, selector: "doFancyDiceRoll", userInfo: nil, repeats: true);
}
func doFancyDiceRoll() {
Die1.image = PipsImg[randomInt(6)]
Die2.image = PipsImg[randomInt(6)]
Die3.image = PipsImg[randomInt(6)]
if (++rolls > 10)
{
timer?.invalidate()
timer = nil
rolls = 0 // DCB added this presumed missing line
btnRoll_x.hidden = false //again, need to do this better
}
}
}
Hopefully, my implementation of the code is what would have been intended. I made some minor adjustments for (hopeful) clarity.
Good news is I have working code. I have just enough understanding to get in place, but I'm fuzzy on some details.
Here is what I (think I) know...
We declare an NSTImer object, and a roll counter at the main level of the class. I note that in my original version, I had the roll counter scoped within the rolling function itself. Took me a while to understand why it could not live in the DiceRoll loop itself, but now I do. I'm going to express it poorly, but since the timer is going to call DiceRoll multiple instances, it needs to live outside the function.
The button btnRoll gets touched, and invokes triggerRoll().
To prevent the user from touching the button while we are in progress, which put us into a state where the roll counter never got to zero, I hide the button. (I'll figure how to properly put in in disabled state later.)
The timer is set. It fires every .1 second (within limits), and is set to repeat. (until .invalidate ). And it "calls" the function doFancyDiceRoll via the selector: attribute.
So, the big change as previously noted is that doFancy..Roll() no longer loops. It excites a single instance up updating the images. It checks the counter, and if we reach the limit, we kill the timer, which stops the timer (invalidate). (And I unhide the button, making it available again.)
So, a few things I am concerned/wondering about: I get the value of timers for other things that need to happen periodically (take health away every second, check a GPS position every 10 seconds, etc.). It's seems a odd construct to force a screen refresh.
Frankly, I would have expected to see see something like this:
func doFancyDiceRoll() {
for x in 1...100 {
Die1.image = PipsImg[RollOne()] // and 2 and 3 of course.....
VIewController.forceRedraw <<=== something like this, or maybe
ViewController.wait(.05) <<== this?? I dunno ;-)
}
}
instead we end up with about 20 extra lines or so. I'd be interested in knowing if there other approaches that could work keeping the loop intact.
Anyway, assuming this is the one true way to go, I guess my followup to this is how do I pass parameters, since this is not a "real" function call. Trying
selector: "doFancyDiceRoll(40)"
was not objected to by the IDE, but failed in execution.
I had exactly same problem back in days, entire loop is finished before the view is refreshed as #doctordoder mentioned. Solved with using NSTimer
var rollCount = 0
let arrayOfImages = ["image01", "image02", "image03"]
var timer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: Selector("doFancyDiceRoll"), userInfo: nil, repeats: true)
func doFancyDiceRoll() {
if rollCount == 100 {
timer.invalidate
rollCount = 0
{
else {
//get images from array arrayOfImages[rollCount]
rollCount++
}
}
there could be typos, since I have no Xcode right now.
I have basically the same answer as above :(, but I thought I'd post it anyway.
var timer : NSTimer? = nil
var rolls : Int = 0
func doFancyDiceRoll() {
timer = NSTimer.scheduledTimerWithTimeInterval(0.10, target: self, selector: "roll", userInfo: nil, repeats: true);
}
func roll() {
println("rolling")
Die1.image = PipsImg[RollOne()]
Die2.image = PipsImg[RollOne()]
Die3.image = PipsImg[RollOne()]
if (++rolls > 100)
{
timer?.invalidate()
timer = nil
}
}
Rather than NSTimer and invalidating, you can use dispatch_after to do the work for you.
func rollDice(howManyTimes: Int) {
die1.image = PipsImg[RollOne()]
die2.image = PipsImg[RollOne()]
die3.image = PipsImg[RollOne()]
if howManyTimes > 0 {
let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(Double(NSEC_PER_SEC) / 10.0))
dispatch_after(delayTime, dispatch_get_main_queue()) {
self.rollDice(howManyTimes - 1)
}
}
}
This will run the code for the number of times specified, delaying each time by 0.1 seconds. It works like this: First it sets the images on each die, then, if there are more iterations, it does a dispatch_after to call itself with rollDice(howManyTimes - 1)
With this, you don't need to maintain a NSTimer and it is pretty self contained.