How do I detect that text is entered into UITextField using dictation on iOS 16?
Before iOS 16, we could detect the user is using dictation to enter text by checking primaryLanguage variable of the textInputMode being dictation. Simple enough. But this method doesn't work no more. In iOS 16, primaryLanguage is the language selected for dictation.
In my Remote Mouse & Keyboard for Mac / PC app (https://remote4mac.app), I need to detect whether the user is using dictation to wait for the whole phrase before sending it to the computer. So I started looking for alternative methods and spent a good couple of hours to no avail. But then I remembered method swizzling. iOS using UIDictationController to control dictation interactions, but it's a private class. So I had to figure out how to swizzle its methods, and fortunately, you can do that by adding a category and using category methods for it like this
#interface NSObject(PrivateSwizzleCategory)
- (void)swizzled_setupForDictationStart;
- (void)swizzled_stopDictation;
#end
#implementation NSObject(PrivateSwizzleCategory)
- (void)swizzled_setupForDictationStart {
[(NSObject *)self swizzled_setupForDictationStart];
[[NSNotificationCenter defaultCenter] postNotificationName:#"UIDictationControllerWillStart" object:nil];
}
- (void)swizzled_stopDictation {
[(NSObject *)self swizzled_stopDictation];
[[NSNotificationCenter defaultCenter] postNotificationName:#"UIDictationControllerDidStop" object:nil];
}
#end
Then you just need to run a code that will swizzle those methods with the UIDictationController like this
Method original, swizzled;
original = class_getInstanceMethod(objc_getClass("UIDictationController"), NSSelectorFromString(#"setupForDictationStart"));
swizzled = class_getInstanceMethod(objc_getClass("NSObject"), #selector(swizzled_setupForDictationStart));
method_exchangeImplementations(original, swizzled);
original = class_getInstanceMethod(objc_getClass("UIDictationController"), NSSelectorFromString(#"stopDictation"));
swizzled = class_getInstanceMethod(objc_getClass("NSObject"), #selector(swizzled_stopDictation));
method_exchangeImplementations(original, swizzled);
and you are done. Just listen to the new notifications and act accordingly in your code.
So now its time for me to see if the app will pass Apple review.
Related
An app we made utilizes keyboardWillShow notifications from NSNotificationCenter
Everything was working as expected until we tried to use the app with a device that had the Sogou custom keyboard installed. When this keyboard is used (which oddly enough it doesn't seem to always come up -- for instance secure text entries ignore the Sogou keyboard) the keyboardWillShow notification is not firing.
Does anyone know of this issue or how to disable the use of custom keyboards?
The only way I found to get this to work was to disable all custom keyboards using the app delegate:
- (BOOL)application:(UIApplication *)application shouldAllowExtensionPointIdentifier:(NSString *)extensionPointIdentifier {
if ([extensionPointIdentifier isEqualToString: UIApplicationKeyboardExtensionPointIdentifier]) {
return NO;
}
return YES;
}
I am just coding my first iOS app using a today widget (using Swift). I was wondering if there is a function that is called whenever my app comes back to the foreground after dismissing the notification center.
I know I can use an Observer to check for UIApplicationWillEnterForegroundNotification but my function does not get called when pulling down the notification center while using my app and dismissing it again.
My problem is simple:
It is quite unlikely users will pull down the notification center to manipulate data I am using in the app, but I still have to consider what happens if they do. The user is supposed to be able to save his current location by pressing the today widget button.
If that happens while using my app, the app won't check for new data.
I used the following code for determining if the notification center was opened during the application's run time:
#import "ViewController.h"
#interface ViewController ()
#end
#implementation ViewController
{
BOOL notificationCenterCurrentlyDisplayed;
}
- (void) viewDidLoad
{
[super viewDidLoad];
notificationCenterCurrentlyDisplayed = false;
NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
[defaultCenter addObserver:self selector:#selector(onNotificationCenterDisplayed) name:UIApplicationWillResignActiveNotification object:nil];
[defaultCenter addObserver:self selector:#selector(onNotificationCenterDismissed) name:UIApplicationDidBecomeActiveNotification object:nil];
}
- (void) onNotificationCenterDisplayed
{
notificationCenterCurrentlyDisplayed = true;
NSLog(#"Notification center has been displayed!");
}
- (void) onNotificationCenterDismissed
{
// Reason for this check is because once the app is launched the UIApplucationDidBecomeActiveNotification is called.
if (notificationCenterCurrentlyDisplayed)
{
notificationCenterCurrentlyDisplayed = false;
NSLog(#"Notification center has been dismissed!");
}
}
#end
Also the notification center was displayed method will also be called when the user decides to close the application into the background.
I have referred the following links for this issue, but neither of the solutions worked for me:
Link 1
Link 2
I think these solutions don't work for iOS7.
So now how would I be able to find out whether there is any UIAlertView open, when my application enters in background state.
I want to dismiss the UIAlertView before going into the background.
Remove alert in applicationDidEnterBackground
Add this line in your class
[[NSNotificationCenter defaultCenter] addObserver: self
selector: #selector(enteredBackground:)
name:UIApplicationDidEnterBackgroundNotification
object: nil];
And implement method as well
- (void)enteredBackground:(UIApplication *)application
{
if (mainAlertView && mainAlertView.isVisible)
[mainAlertView dismissWithClickedButtonIndex:0 animated:NO];
}
You get a notification when the app is send to background, so detect that notification in the class that displays the alert view and remove it, that's all
have you checked UIAlertView property #property(nonatomic, readonly, getter=isVisible) BOOL visible Also while going in the background you get a notification in - (void )applicationDidEnterBackground: you can check there and remove all alertviews if any
I’m getting multiple UIKeyboardDidShowNotification and UIKeyboardDidHideNotification notifications when I rotate the device and I can’t figure out why.
I have an app that lets you edit the text for each picture. (It’s in a UITextView.) Most of the time I need to slide the text up so you can see it above the keyboard, then I slide it down when you are done editing. Then I do a database update to save the new text. I use notifications to tell me when the keyboard is displayed and when it goes away. It works fine on iPad when the user taps the keyboard close icon on the keyboard. It also works fine if the user swipes to the next page and iOS closes the keyboard. Since iPhones and iPods don’t have a keyboard close key, I wrote a method to close the keyboard when the picture or background is tapped. It works fine there too. However, when I rotate the device, I get multiple hide and show notifications. And I don’t know why.
Instead of getting one UIKeyboardDidHideNotification notification, I get a hide, a show, a hide, and then a show.
2:39:44.200 Picts for SLPs[16533:907] keyboardDidHide called. Keyboard showing flag is YES.
2:39:51.751 Picts for SLPs[16533:907] keyboardDidShow called. Keyboard showing flag is NO.
2:39:55.224 Picts for SLPs[16533:907] keyboardDidHide called. Keyboard showing flag is YES.
2:39:56.124 Picts for SLPs[16533:907] keyboardDidShow called. Keyboard showing flag is NO.
I posted the relevant code below. It is taken mostly from StackOverflow posts (Thanks guys).
In my class that displays the pictures I start notifications when it is initialized.
- (id)initWithParentView:(UIView *)parentview {
self = [super init];
if (self) {
_parentView = parentview;
if (ALLOW_DATABASE_EDITING) [self startNotifications];
}
return self;
}
- (void)startNotifications {
// Listen for keyboard appearances and disappearances
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardDidShow:)
name:UIKeyboardDidShowNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardDidHide:)
name:UIKeyboardDidHideNotification
object:nil];
}
The View Controller calls the hideKeyboard method in the View when the user taps on the picture.
- (void)dismissKeyboard {
if (self.showArtic.keyBoardIsShowing) {
[self.showArtic hideTheKeyboard];
}
}
resignFirstResponder sends a notification that closes the keyboard
- (void)hideTheKeyboard {
id <ShowArticDelegate> SA_delegate = _delegate;
// Don't update the database when there is no text.
if ( ![self.editableTextView.text isEqualToString:#""] ) {
[SA_delegate updateTextInDatabase:self.editableTextView.text];
}
[self.editableTextView resignFirstResponder];
}
These methods respond to the notifications.
- (void)keyboardDidHide:(NSNotification *)notification {
NSLog(#"keyboardDidHide called. Keyboard showing flag is %#.", self.keyBoardIsShowing ? #"YES" : #"NO");
self.keyBoardIsShowing = NO;
// Move the text, update the database
}
- (void)keyboardDidShow:(NSNotification *)notification {
NSLog(#"keyboardDidShow called. Keyboard showing flag is %#.", self.keyBoardIsShowing ? #"YES" : #"NO");
self.keyBoardIsShowing = YES;
// Move the text
}
any chance you could manually dismiss the keyboard and clean up your database from within this method:
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
and then if you want the keyboard brought back up once the rotation completes, manually call your method to display it from within:
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
That may or may not fit your needs, but it's a pretty common and clean technique to "get out of the way" while Apple handles the rotation, and then get back to business once everything is back on solid ground again.
How to respond to starting dictation?
Known ways of responding to dictation:
dictationRecordingDidEnd - respond to the completion of the recognition of a dictated
phrase.
dictationRecognitionFailed - respond to failed dictation recognition.
Reference: UITextInput Protocol Reference
Starting in iOS 5.1, when the user chooses dictation input on a supported device, the system automatically inserts recognized phrases into the current text view. Methods in the UITextInput protocol allow your app to respond to the completion of dictation, as described in “Using Dictation.” You can use an object of the UIDictationPhrase class to obtain a string representing a phrase a user has dictated. In the case of ambiguous dictation results, a dictation phrase object provides an array containing alternative strings.
http://developer.apple.com/library/ios/#documentation/uikit/reference/UITextInput_Protocol/Reference/Reference.html
As far as I can tell, there's no public API for detecting when dictation has started.
If you really want to do it, and you want to be in the App Store, you can probably get away with the following approach, but it is totally unsupported, it might get you rejected anyway, and it is likely to break in a future version of iOS.
The text system posts some undocumented notifications after changing to or from the dictation “keyboard”. Two of them are posted both on a change to it and a change from it, with these names:
UIKeyboardCandidateCorrectionDidChangeNotification
UIKeyboardLayoutDidChangedNotification
Note that the second one has a strange verb conjugation. That is not a typo. (Well, it's not my typo.)
These notices are also posted at other times, so you can't just observe them and assume the dictation state has changed. You'll need to do more checking when you receive the notification. So, add yourself as an observer of one of those notifications. The first one seems less likely to go away or be renamed in the future.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(checkForDictationKeyboard:)
name:#"UIKeyboardCandidateCorrectionDidChangeNotification"
object:nil];
...
When you receive the notification, you'll want to see whether the dictation view is showing:
- (void)checkForDictationKeyboard:(NSNotification *)note {
if ([self isShowingDictationView]) {
NSLog(#"showing dictation view");
} else {
NSLog(#"not showing dictation view");
}
}
To see whether it's showing, check each window except your own application window. Normally, the only other window is the text system's window.
- (BOOL)isShowingDictationView {
for (UIWindow *window in [UIApplication sharedApplication].windows) {
if (window == self.window)
continue;
if (containsDictationView(window))
return YES;
}
return NO;
}
Recursively walk the view hierarchy checking for a view whose class name contains the string “DictationView”. The actual class name is UIDictationView but by not using the whole name you're less likely to be rejected from the App Store.
static BOOL containsDictationView(UIView *view) {
if (strstr(class_getName(view.class), "DictationView") != NULL)
return YES;
for (UIView *subview in view.subviews) {
if (containsDictationView(subview))
return YES;
}
return NO;
}
Although this question is answered, I still want to add my solution and wish to be helpful to someone else.
When you tap on the MIC button on the keyboard, primaryLanguage will change to dictation. You can detect that like this:
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:#selector(handleCurrentInputModeDidChange:)
name:UITextInputCurrentInputModeDidChangeNotification
object:nil];
- (void) handleCurrentInputModeDidChange:(NSNotification *)notification
{
NSString *primaryLanguage = [UITextInputMode currentInputMode].primaryLanguage;
NSLog(#"current primaryLanguage is: %#", primaryLanguage);
}
UPDATE:
Just as what #user1686700 said, currentInputMode already been deprecated. This isn't a solution any more.
Please note - UITextInputMode's currentInputMode is deprecated as of iOS7.
We may just have to wait until Apple decides to publish the dictation API so we can make the calls that make sense as per our intentions.