Objective-C: UIDatePicker UIControlEventValueChanged only fired on second selection - ios

Working with UIDatePickers for the first time. To start, I'm just trying to update a text field with whatever value is selected from the datePicker (set in count down mode).
It works, but only the second time that a user selects a time. My action, durationSelected() is only called the second time, so there are no problems updating the textField.
Here's how I configured the date picker in my storyboard:
Here's the action triggered on Value Changed:
from DetailViewController.m
- (IBAction)durationSelected:(UIDatePicker *)sender
{
self.durationTextField.text = [NSString stringWithFormat:#"%f seconds", sender.countDownDuration];
}
I've tried setting a default value, which had no effect.
What's going on?

I have seen a similar bug with the iOS 7.0.3 UIDatePicker when it is 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 ;
});

This seems to be a bug in the iOS 7 implementation of UIDatePicker. I'd suggest filing a radar on it.
Building on the iOS 6 SDK and running on an iOS 6 device will work, while running on an iOS 7 device will not.
You can fix it by adding this line of code to your - (void)viewDidLoad method
[self.datePicker setDate:[NSDate date] animated:YES];

I'm not sure, it's an educated guess :
It might be that iOS is keeping track of value changes for the Date Picker only when they are caused by animating the wheel. So it sets the value on your first "roll" but only detects that has changed on the second.
As I said, I cannot be sure of the reason, but the fix should be simple: just set the staring date programmatically after the view loads using setDate: animated: :
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[self.picker setDate:[NSDate dateWithTimeIntervalSinceNow:0] animated:true ];
}
It seems to be working for me.

There is also a similar bug where it doesn't fire on the first change after you spin it to all 0's and it slides to the first minute.
Given both of these, I just use a brute force method to account for all this nonsense (assuming Apple is simply not interested in fixing this).
in viewDidLoad():
NSTimer scheduledTimerWithTimeInterval:0.750 repeats:YES block:^(NSTimer * _Nonnull timer) {
[self timePickerDidChange:nil];
}];
Just keep calling the change event yourself. so long as it goes off of the current value (i.e. self.timePicker.countDownDuration then it works smoothly. Make sure your didChange code doesn't do anything big like a network call :)

You can try to set countDownTimer value (if you are using datePicker on countdowntimer mode). But make sure you set this property inside completion block after presenting the picker.

The following code after presenting the picker worked for me in Xcode 9.4 beta, iOS 11.4.
picker.countDownDuration = 0
picker.countDownDuration = 3600 // 1 Hour

I've had success with the following in Xcode 10, iOS 12.0.1 using Swift 4.0:
Using the other answers as a guide, I ended up making the UITextFields delegate respond to:
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
if let _ = textField.inputView as? UIDatePicker {
//In my case I had more than one text field who's delegate would fire this method
//So I checked to see if the input view was a date picker before continuing
//If you have only one "delegated" text field then there's probably
//no need for a check
perform(#selector(setDatePicker), with: nil, afterDelay: 0)
}
return true
}
#objc func setDatePicker() {
Thread.doBlockOnMainThread {
self.theTimePicker.countDownDuration = 1000 //or whatever
}
}
where Thread.doBlockOnMainThread(_) is:
extension Thread {
class func doBlockOnMainThread(_ block: () -> Void) {
if !Thread.isMainThread {
DispatchQueue.main.sync(execute: {
block()
})
} else {
block()
}
}
}
As an aside, I found that it's not only when the UIDatePicker first appears but also when the user mistakenly tries to set the countdown value to zero. The DatePicker automatically moves to 1 as should be expected but then the very next value change will also not fire the requred target.

None of the above worked for me on iOS 13 / XCode 11.4.1. My solution was to use the "textFieldDidBeginEditing" delegate method for the textfield UITextFieldDelegate, as the datepicker needs to be in view and animated to simulate a "first" selection, so on second selection (perceived first selection to the user) the UIControlEventValueChanged method is triggered. So, something like this:
- (void)textFieldDidBeginEditing:(UITextField *)textField {
[self.datePicker setDate: date]; //date is whatever date you want to set
}
On my method, on first run, I set the date variable to a minute interval of 5 using NSCalendar and NSDateComponents, as that's what I needed, and then after that I set date = self.datePicker.date.
This works except for the case where the user manually selects 0 minutes..

Related

Label taking a really long time to change

I'm learning to develop an IOS app. I'm having the following problem. I want to use a label to display a string. It takes a really long time for this string to be displayed (10-15 sec). Is this normal? The following code is inside the viewDidLoad function
NSLog(self.example); //displays almost immediately
_labelOutput.text= [NSString stringWithFormat:#"%#", self.example;//takes 15 seconds
The entire viewDidLoad function:
- (void)viewDidLoad
{
[super viewDidLoad];
double lat = 43.7000;
double lon = -79.4000;
NSArray *users = [[NSArray alloc] initWithObjects:#"user_1",#"user_2",#"user_3", nil];
id prediction = [[Prediction alloc] initWithUsers:users Lat:lat Lon:lon];
[prediction populate:^{
self.resName= [prediction generateRandom][#"id"];
NSLog([NSString stringWithFormat:#"%#", self.resName]);
_labelOutput.text= [NSString stringWithFormat:#"%#", self.resName];
}];
}
What does -[Prediction populate:] do with the block? My guess is it runs the block on a background thread or queue. You aren't allowed to modify the UI from a background thread or queue. Your app might crash or just act unpredictably. Your mysterious delay in updating the screen is a common symptom of this mistake.
You must only modify the UI from the main thread or queue. Try this:
[prediction populate:^{
self.resName= [prediction generateRandom][#"id"];
NSLog([NSString stringWithFormat:#"%#", self.resName]);
dispatch_async(dispatch_get_main_queue(), ^{
_labelOutput.text= [NSString stringWithFormat:#"%#", self.resName];
});
}];
ETA: Rob's answer is probably spot-on...if you'd posted that code initially I would have caught it as well.
Try setting the label's text in viewWillAppear or perhaps viewDidAppear instead.
Setting the "text" property of a label will normally trigger a [setNeedsDisplay] call automatically via key-value observing, and this notifies the system that the label's view needs to be redrawn on the next run loop. However, viewDidLoad is called before your view is actually visible. It's likely that because of this, either [setNeedsDisplay] is not being called, or is being ignored because the label is not yet visible...and thus, you have to wait for some other event to trigger re-drawing of subviews.
You could test this theory by adding a [self.labelOutput setNeedsDisplay] call yourself in viewDidAppear.
For swift 3 you'll want to use
DispatchQueue.main.async(execute: {
_labelOutput.LabelName.text = "Something"
})

How to detect that speech recogntion is in progress

Problem:
I have UITextField side by side with UIButton with send functionality. When user presses send button I'm performing simple action:
- (IBAction)sendMessage: (id)sender {
[self.chatService sendMessage: self.messageTextField.text];
self.messageTextField.text = #""; // here I get exception
}
Now when user starts using dictation from keyboard, then presses done on dictation view (keyboard) and immediately presses send button, I've got exception "Range or index out of bounds".
Possible solution:
I've noticed that other applications disable this "send" button when speech recognition server is processing data. This is exactly between two events: user presses "done" and results are appearing in text field. I wish to solve it in the same manner.
I've problem finding in documentation where this notification can be received. I've found UITextInput protocol, but this is not what I need.
Similar topics:
Using Dictation - iOS 6 - DidStart - solution not acceptable (might be rejected by apple)
Disable Dictation button on the keyboard of iPhone 4S / new iPad - similar approach as above
What have I tried:
simply catch and ignore exception. Crash didn't acured, but virtual keyboard become completely unresponsive
Disabling send button when [UITextInputMode currentInputMode].primaryLanguage is equal #"dictation". Notification UITextInputCurrentInputModeDidChangeNotification which reports end of dictation mode arrives before dictation service commits new value and I'm still able to click send button to cause exception. I could add delay when primaryLanguage losses #"dictation" value, but I don't like this approach. Most probably this required delay depends how much speech recognition service is responsive.
I've added bunch of actions on different events (this evets was looking processing: UIControlEventEditingDidBegin, UIControlEventEditingChanged, UIControlEventEditingDidEnd, UIControlEventEditingDidEndOnExit). The good thing is that it looks like UIControlEventEditingChanged is fired exactly at desired moments: when user presses "Done" on dictation view and when service is committing or ending dictation. So this is my best concept so far. The bad thing is that this is fired in other cases too and there is no information to distinguish in which case this control event was fired, so I don't know should I disable or enable the button or do nothing.
I finally found ultimate solution.
It is simple elegant will pass apple review and it Always work. Just react on UIControlEventEditingChanged and detect existance of replacemnt characterlike this:
-(void)viewDidLoad {
[super viewDidLoad];
[self.textField addTarget: self
action: #selector(eventEditingChanged:)
forControlEvents: UIControlEventEditingChanged];
}
-(IBAction)eventEditingChanged:(UITextField *)sender {
NSRange range = [sender.text rangeOfString: #"\uFFFC"];
self.sendButton.enabled = range.location==NSNotFound;
}
Old approach
Finlay I've found some solution. This is improved concept nr 3 with mix of concept nr 2 (based on that answer).
-(void)viewDidLoad {
[super viewDidLoad];
[self.textField addTarget: self
action: #selector(eventEditingChanged:)
forControlEvents: UIControlEventEditingChanged];
}
-(IBAction)eventEditingChanged:(UITextField *)sender {
NSString *primaryLanguage = [UITextInputMode currentInputMode].primaryLanguage;
if ([primaryLanguage isEqualToString: #"dictation"]) {
self.sendButton.enabled = NO;
} else {
// restore normal text field state
self.sendButton.enabled = self.textField.text.length>0;
}
}
- (IBAction)sendMessage: (id)sender {
[self.chatService sendMessage: self.messageTextField.text];
self.messageTextField.text = #"";
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
if (self.textField.text.length==0 || !self.sendButton.enabled) {
return NO;
}
[self sendMessage: textField];
return YES;
}
// other UITextFieldDelegate methods ...
Now problem doesn't appears since user is blocked when it could happen (exactly between user presses "Done" button on dictation view and when results are coming from speech recognition service.
The good thing is that public API is used (only #"dictation" can be a problem, but I thin it should be accepted by Apple).
In iOS 7 Apple introduced TextKit so there are new information for this question:
NSAttachmentCharacter = 0xfffc
Used to denote an attachment as documentation says.
So, if your version is more or equal to 7.0, better approach is to check attributedString for attachments.

Call custom Class' obj of type NStimer into viewController

In the sample tutorial I found, I noticed a timer made using NStimer class: it was implemented directly into viewController.m
I tried to make it as "separate" obj class in it's own timer.m and relative header.
This is what i got
#import "Timer.h"
#implementation Timer
-(void) startTimer{
seconds = 31;
myTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(subtractTime:) userInfo:nil repeats:YES];
NSLog(#"%i",seconds);
}
-(void)subtractTime: (Timer*)myTimer{
seconds--;
NSLog(#"seconds %i",seconds);
if (seconds == 0) {
[myTimer invalidate];
}
}
#end
According to the output in NSLog, the countdown itself works perfectly; my issues start when i try to display it into a label using the form label.text = into the viewController.m
(here's just the method implementation part)
(Xcode gives me no error bout #implementation part both in Timer.h and viewController.h, also project build runs ok but the countdown into the label is locked to 0)
-(void)setupGame{
count = 0;
scoreLabel.text = [NSString stringWithFormat:#"Score:\n%i",count];//these strings are to make a score label increasing of 1 every time a button's pressed
Timer *newTimer = [[Timer alloc]init]; //i create a new obj of my Timer class
[newTimer startTimer];
[newTimer subtractTime:(Timer*) myTimer]; //i set the methods i created
timerLabel.text = [NSString stringWithFormat:#"%i",seconds]; //??? i don't know which var put here, actually, i cannot get here the refreshing seconds to make timerLabel changing
NSLog(#"newTimer %#",myTimer);//just a try to see this output, it's not what i need of course
}
What am i doing wrong?
There are a couple of issues here. Firstly make Timer an instance variable of the view controller as when you do this:
-(void)setupGame{
// blah
Timer *newTimer = [[Timer alloc]init];
// blah
}
The Timer object will be destroyed when this method returns, as the object goes out of scope.
The second problem is that you need to update the UI control when the time changes, so perhaps pass a reference to the UI control (UILabel or whatever it is), and store this as an instance variable of the Timer class, so the timer object can update it itself.
In this particular case I don't see the value of a separate Timer object as it would be easier to implement what you want just within the view controller class.
As Trojanfoe says, you are over-complicating this. Make your view controller create the timer and be done with it.
If you are bound and determined to create a custom class to manage a timer then you have a bunch of work to do.
I would NOT pass the label to the Timer object, as that breaks the encapsulation of the view controller. You should always treat a view controller's views as private to that view controller.
Instead, here's what I would do:
You need to add a Timer instance variable to your view controller, as Trojanfoe says.
You need to define a TimerProtocol. In that protocol, define a "timerFired" method, that would include the remaining time value as a parameter. You probably also want a "timerFinished" method
When you create a Timer object, set yourself up as the delegate of that timer.
Rewrite your Timer object's subtractTime method to send a message to the delegate with the remaining time value. Then in the view controller's timerFired method, update the label.
In your view controller's timerFinished method, nil out the instance var to the Timer object so it will be deallocated, since you then done with it.
Again, this is an over-complicated solution that does not add any value. The only reason to do it this way is as a learning exercise, but it is not a good design decision.

What is a more efficient way to prevent actions from firing twice?

I'm having an issue when someone taps the rightBarButtonItem on the navigation bar and taps it again quickly that the program crashes. It's understandable that is crashes because the new view hasn't finished loading yet and the button is still visible to tap again so it will attempt to push the view again crashing the program. I've tried a couple of methods to try and prevent this and my current implementation somewhat works, but I know there is a better solution, perhaps a solution built into the framwork?
- (void) loadView
{
[super loadView];
self.navigationItem.rightBarButtonItem = [[[UIBarButtonItem alloc]
initWithTitle:#"Members"
style:UIBarButtonItemStylePlain
target:self
action:#selector(showRoster)] autorelease];
}
-(void) showRoster {
if (seconds + 3 < [[NSDate date] timeIntervalSince1970]) { //This is where I am trying to prevent the button from being activated twice.
seconds = [[NSDate date] timeIntervalSince1970];
vcRoster = [[RosterDataViewController alloc] init];
vcRoster.rosterDataModel.group_id = self.tweetsByGroupIdModel.group_id;
[self.navigationController pushViewController:vcRoster animated:YES];
}
}
I've also tried this, but it's not doing what I am thinking it should be doing.
if (!self.navigationController.isBeingPresented)
I've tried solving the problem and Googling my way to an answer, but I have yet to find a good solution to this problem. I know the above is not a good way to do things, I'm open to suggestions for any improvements at all as I am extremely new to the IOS world. Thanks in advance for any help.
Try disabling the button as soon as the action method is run:
-(void) showRoster:(UIButton*)sender {
sender.enabled = NO;
...
}
You can enable it later, to make it responsive again. How you can best do that depends on the view you are showing: if it is full screen, you might simply re-enable the button in viewDidAppear: (i.e., after the view has disappeared and your button is visible again).
You should use UIViewController's isViewLoaded method to check if the view has been loaded:
if (self.navigationController.topViewController.isViewLoaded && seconds + 3 < [[NSDate date] timeIntervalSince1970]) {...}

How do I create a GLKViewController that starts in a paused state?

I create a GLKViewController like this:
// Create a GLK View Controller to handle animation timings
_glkVC = [[GLKViewController alloc] initWithNibName:nil bundle:nil];
_glkVC.preferredFramesPerSecond = 60;
_glkVC.view = self.glkView;
_glkVC.delegate = self;
_glkVC.paused = YES;
NSLog(#"initial state: %#", _glkVC.paused ? #"paused" : #"running");
but it immediately starts calling the delegate update method and the output from the NSLog above is: initial state: running
I am managing my view updates with setNeedsDisplay but I want the GLKViewController to handle animations from time to time so I want to unpause it only when needed. Is there a way to start the controller in a paused state?
have you tried pausing in the viewDidAppear method instead of the viewDidLoad method? It should look something like this:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// self.paused automatically set to NO in super's implementation
self.paused = YES;
}
Boom, done! If this works then you save an "if" check thousands of times a minute just to pause on launch!
The viewDidAppear method works for me, but not optimally. A few frames of visible animation occur before the pause takes effect. Using viewWillAppear worked much better:
- (void) viewWillAppear: (BOOL) animated
{
[ super viewDidAppear: animated ];
self.paused = YES;
}
In lieu of any answers I'm using this work-around:
I set .preferredFramesPerSecond = 1 initially and then in the update method I check if(preferredFramesPerSecond == 1) and set .paused = YES (and also set my real desired value for preferredFramesPerSecond). I can then allow the rest of the update method to run once after initialisation, or return immediately if I don't want it to run yet.
I then trigger redraws manually as needed with setNeedsDisplay and unpause it when I need it to animate.
If anyone has a better solution please answer as usual.
Have you tried overriding resumeOnDidBecomeActive to return NO? This should keep the animation paused on any activation, including the first.

Resources