How to detect that speech recogntion is in progress - ios

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.

Related

How to set a default keyboard across an entire iOS app?

I have an iPad app that uses thousands of UITextFields and UITextViews throughout it. When text is being entered into these elements, the iOS default keyboard is used. However, because my app does not provide dictation support, it crashes whenever the user attempts to dictate. Some research into the subject has shown that it isn't possible to disable the dictation feature on the default keyboard.
Instead, I found that it is recommended to use a change the type of keyboard to one that doesn't offer a dictation option. The problem is that I have thousands of UITextFields and UITextViews throughout my app, so manually going in and changing the keyboard type of each of these is unfeasible. My question is this: is there a way to change the default keyboard used within an app?
One way to go about doing this is to use a category and set the value during init. This requires swizzling, but should be safe.
Keep in mind that this will do what you are requesting, so ALL UITextFields will now present using the defined keyboard type (even if you didn't instantiate the text field yourself).
This category will default all UITextFields to display the number pad.
#import "UITextField+KeyboardType.h"
#import <objc/runtime.h>
#implementation UITextField (KeyboardType)
+(void)initialize {
Method originalMethod = class_getInstanceMethod([self class], #selector(init));
Method swizzledMethod = class_getInstanceMethod([self class], #selector(init_swizzled));
method_exchangeImplementations(originalMethod, swizzledMethod);
}
- (instancetype)init_swizzled {
self = [self init_swizzled];
if (self) {
self.keyboardType = UIKeyboardTypeNumberPad;
}
return self;
}
#end
I would recommend that you instead create subclasses of UITextField/UITextView and replace all existing instances of UITextField/UITextView with their new subclasses.

Detecting multiple touches WKInterfaceTable

Is there a way to detect if a table row has already been selected, right now I am protecting against multiple pushes with a boolean like so:
- (void)willActivate {
// This method is called when watch view controller is about to be visible to user
pushed=NO;
}
- (void)table:(WKInterfaceTable *)table didSelectRowAtIndex:(NSInteger)rowIndex {
// if pushed, just return else continue and set pushed to true
if (pushed) {
return;
}
pushed=YES;
[self pushControllerWithName:rowData[#"controllerIdentifier"] context:nil];
}
There is no built-in method to detect multiple touches in a WKInterfaceTable. The technique that you're using is the same technique that I use in my Watch app. In my case, I maintain a BOOL for each row indicating whether it's enabled or not. Based on that BOOL, I updated my cell to show a "disabled"-looking state, though of course, it isn't technically disabled.

Using IF statement to test the ON/OFF setting of iOS Switch Control

I am new to Objective-C, but use JavaScript and a lot of VB.NET and some C for firmware development. I am writing beginner level Apps and in this case, have a SWITCH control on my View. I want to know how to use an IF statement to test it's ON/OFF state. Sample code is best as I am legally blind and have difficulty reading lots of text online.
Same issue for the Stepper control (The control with the +/_ buttons used to increment / decrement values.)
Thanks.
UISwitch does have a property on (check the docu here: UISwitch class reference) so check the property (note that the getter is named differently (isOn)):
#property(nonatomic, getter=isOn) BOOL on
So one would check the switch like this:
if ([switch isOn])
{}
else
{}
first add a target to the switch for event UIControlEventValueChanged after adding the switch to your view, then use the switch's property to determine its state,
[mySwitch addTarget:self action:#selector(switchToggled:) forControlEvents:UIControlEventValueChanged];
- (void) switchToggled:(id)sender {
UISwitch *mySwitch = (UISwitch *)sender;
if ([mySwitch isOn]) {
NSLog(#"its on!");
} else {
NSLog(#"its off!");
}
}

Objective-C: UIDatePicker UIControlEventValueChanged only fired on second selection

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..

UIWebView canPerformAction do not disable some menu items

In a UIWebView, I want a certain class div element to display only one custom contextual menu entry. So that I implemented the canPerformAction:: method in the UIWebView delegate like this:
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
if (self.webView.superview != nil) {
BOOL isMyClass=[[self.webView stringByEvaluatingJavaScriptFromString:#"window.getSelection().getRangeAt(0).startContainer.parentNode.className;"] isEqualToString:#"myClass"];
if (isMyClass) {
if (action == #selector(myAction:)) {
return YES;
} else {
return NO; // should disable any other menu items
}
}
}
return [super canPerformAction:action withSender:sender];
}
The result is quite strange: when the user selects such a myclass div, most menuItems are not displayed (cut: copy: past:...) but select: and selectAll: are still displayed (along with myAction). Under debugger, I notice that these two select/selectAll methods do not fire canPerformAction:: in the delegate... Where are these two method fired?
I think I know why you may be having problems.
I had the same question and similar frustration:
"Why are select: and selectAll: not appearing when stepping through calls to canPerformAction::?"
I then realized that the firstResponder when displaying the UIMenuController was just a container, and that this class had a member that was actually extending the UITextView class. Since the sharedMenuController interacts with the first responder in the Responder chain, implementing canPerformAction in the container skipped select and selectAll because they had already been handled by the textView member (the REAL firstResponder in this situation).
What you should do is find which object is your firstResponder when displaying the UIMenuController, find any responder objects it might own until you find the highest responder on the stack, and implement canPerformAction there.
Good Luck!
Sometimes, when application is used on the iPad device, with no connection to Xcode, the menu correctly displays only the authorized item... Sometimes not... Very erratic behaviour indeed

Resources