In ios 8.3 and later, UIAlertView cause keyboardWillShow & keyboardWillHide called twice - ios

While working on project, i came across this problem.
One of the controller implement thekeyboardWillShow & keyboardWillHide (Standard code from Apple Managing the Keyboard).
On Background tap,UIAlertView Appears(based on some validation), there is only one button in UIAlertView that simply close the UIAlertView.
Problem occurs here, on close of UIAlertView, keyboardWillShow & keyboardWillHide called again.
Below is the code i am having problem,
#import "ViewController.h"
#interface ViewController () <UITextFieldDelegate>
{
int timeCalledShow;
int timeCalledHide;
}
#property (weak, nonatomic) IBOutlet UITextField *textField;
#property (weak, nonatomic) IBOutlet UIScrollView *scrollView;
- (IBAction)backgroundTapped:(id)sender;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWillShow:)
name:UIKeyboardWillShowNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWillHide:)
name:UIKeyboardDidHideNotification
object:nil];
}
- (void)keyboardWillShow:(NSNotification *)notification {
timeCalledShow+=1;
NSLog(#"show Time Called %d", timeCalledShow);
CGSize keyboardSize = [[[notification userInfo] objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
UIEdgeInsets contentInsets;
if (UIInterfaceOrientationIsPortrait([[UIApplication sharedApplication] statusBarOrientation])) {
contentInsets = UIEdgeInsetsMake(0.0, 0.0, (keyboardSize.height), 0.0);
} else {
contentInsets = UIEdgeInsetsMake(0.0, 0.0, (keyboardSize.width), 0.0);
}
self.scrollView.contentInset = contentInsets;
self.scrollView.scrollIndicatorInsets = contentInsets;
}
- (void)keyboardWillHide:(NSNotification *)notification {
timeCalledHide+=1;
NSLog(#"Hide Time Called %d", timeCalledShow);
self.scrollView.contentInset = UIEdgeInsetsZero;
self.scrollView.scrollIndicatorInsets = UIEdgeInsetsZero;
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (IBAction)backgroundTapped:(id)sender {
[[[UIAlertView alloc] initWithTitle:#"Testing" message:#"Keyboard hide & show, due to alert view" delegate:nil cancelButtonTitle:#"Ok" otherButtonTitles:nil, nil] show];
[self.view endEditing:YES];
}
#end
Notes
I have already check keyboardWillShow called twice and similar questions here, but could not found answer
It works fine with iOS 7.0
Here is link of Test Code
Edit
I already know the work around codes. but the real question is, how aUIAlertView can fire akeyboardWillShow notification
Edit Code
I have tried Below code also suggested by #Chonch, but with this code keyboard never close even. Means keyboard appear again after closing Alert.
- (IBAction)backgroundTapped:(id)sender {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:#"testing" message:#"Keyboard" preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:#"OK" style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {}];
[alert addAction:defaultAction];
[self presentViewController:alert animated:YES completion:nil];
[self.textField resignFirstResponder];
}
Question Posted at Apple Developer Forums

UIAlertView is buggy and probably hasn't been since it was deprecated. Replace it with UIAlertController and your problem should go away

Not sure why it happens. It has probably something to do with UIAlertView trying to get the keyboard state back to what it was before. But note that there is no harm done with the additional show/hide calls. And in general, you need to be prepared for multiple show calls anyways, because they also come when the keyboard style is changed.
If you want to get rid of them, then use a UIAlertController instead, as suggested by Chonch already, and make sure the keyboard is dismissed before the Alert comes up, then it will work OK:
- (IBAction)backgroundTapped:(id)sender {
[self.textField resignFirstResponder];
alert = [UIAlertController alertControllerWithTitle:#"testing" message:#"Keyboard" preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:#"OK" style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {}];
[alert addAction:defaultAction];
[self presentViewController:alert animated:YES completion:nil];
}
Note that with UIAlertController, you also need to keep a reference to the alert in the viewcontroller, otherwise it will be deallocated too soon.

I just fixed a similar issue. The keyboard keep popup after the alert dismisses
It seems like a bug of apple.
There is a simple solution:
If you are using AlertController, you can just set the animated to NO
[self presentViewController:alert animated:NO completion:nil];
Let me know if it fixed your problem

Related

Keyboard does not disappear after viewDidDisappear on iOS 7

In our app, there's a situation where the user enters something in a textbox and then presses the back button to go back to the master screen.
If we run that on iOS 7, the keyboard does not disappear, it just stays there. The user can still navigate through the app, but all text fields are disabled, meaning you can't enter text anywhere. The only option the user has is killing the app and starting fresh.
We tried to add resignFirstResponder messages, but that didn't help anything.
There's much code involved, and we're actively working on the issue. Meantime, has anyone experienced that problem too, and maybe found a way to make it go away?
I had the same issue like you when I compiled the app for iOS 7 and I did the following changes:
Make sure you add [textfield resignFirstResponder] before dismissing the viewController for example:
[_passwordInput resignFirstResponder];
[_emailInput resignFirstResponder];
[self performSegueWithIdentifier:#"forgotPassword" sender:self];
Just to be sure the keyboard disappears add [textfield resignFirstResponder] in viewWillDisappear for example :
- (void) viewWillDisappear:(BOOL)animated
{
[_passwordInput resignFirstResponder];
[_emailInput resignFirstResponder];
}
If your viewController is presented using UIModalPresentationFormSheet add this to your viewController just to make sure the textfields will respond resignFirstResponder:
- (BOOL)disablesAutomaticKeyboardDismissal
{
return NO;
}
In your case, override the back button action or just use viewWillDisappear to check when the user pressed the back button and then call resignFirstResponder before [super viewWillDisappear] something like this:
-(void) viewWillDisappear:(BOOL)animated
{
[_passwordInput resignFirstResponder];
[_emailInput resignFirstResponder];
[super viewWillDisappear:animated];
}
Try [self.view resignFirstResponder], instead of [textfield resignFirstResponder] on viewWillDisappear.
[textfield resignFirstResponder] should do the job, but to make sure and for not to loop through all your textFields you can use:
[self.view endEditing:YES];
From the doc:
use to make the view or any subview that is the first responder resign
(optionally force).
in general I find this useful
[[UIApplication sharedApplication] sendAction:#selector(resignFirstResponder) to:nil from:nil forEvent:nil];
you can add this in viewWillDisappear: or viewDidDisappear:
this will hide the keyboard without a reference to the currently focused text field
I had the same problem only with MoreViewController in UITabBarController (iOS 8.3). Maybe this solution not very 'nice' and little complicated, but seems like it works, hope it will help you too.
#interface ViewController ()
#property (nonatomic) BOOL needToHideKeyboard;
#property (nonatomic, strong) IBOutlet UITextField *txtField;
#property (nonatomic, strong) IBOutlet UIScrollView *scrollView;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(keyboardWasShown:) name:UIKeyboardDidShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(keyboardWillBeHidden:) name:UIKeyboardWillHideNotification object:nil];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
self.needToHideKeyboard = NO;
}
- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
[self hideKeayboard];
}
- (void)hideKeayboard
{
if (self.needToHideKeyboard) {
[self.txtField resignFirstResponder];
}
}
- (void)keyboardWasShown:(NSNotification *)notification
{
self.needToHideKeyboard = YES;
NSDictionary *info = [notification userInfo];
CGSize keyboardSize = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
// Shift scroll view content insets on the keyboard height
UIEdgeInsets contentInsets = self.scrollView.contentInset;
contentInsets.bottom = keyboardSize.height;
self.scrollView.contentInset = contentInsets;
}
- (void)keyboardWillBeHidden:(NSNotification *)notification
{
self.needToHideKeyboard = NO;
// Reset keyboard content insets
UIEdgeInsets contentInsets = self.scrollView.contentInset;
contentInsets.bottom = [self.bottomLayoutGuide length];
self.scrollView.contentInset = contentInsets;
}
#end
If your view controller implements textFieldDidEndEditing, make sure you don't set another view to be the first responder if the view is disappearing. textFieldDidEndEditing will get invoked when you call resignFirstResponder, or [self.view endEditing:YES].
[self.view endEditing:YES];
stopped working on my device iOS9.x
We can also do this in the viewWillDisappear method
for (UIView *subview in self.view.subviews) {
if ([subview canPerformAction:#selector(endEditing:) withSender:nil]) {
[subview endEditing:YES];
}
if ([subview canResignFirstResponder]) {
[subview resignFirstResponder];
}
}
This will loop through the responders and resign the responder status.

removeObserver not working

I have next code:
#implementation SplashViewVC
- (void)viewDidLoad
{
[super viewDidLoad];
self.splashView.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:#"Default.png"]];
self.activityIndicator.originY = 355.f;
[[NSNotificationCenter defaultCenter] addObserverForName:NCDownloadComplete object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *n){
NSInteger errorCode = [n.userInfo[#"errorCode"] integerValue];
[self.activityIndicator stopAnimating];
if (errorCode == ERROR_CODE_NO_CONNECTION) {
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:#"Error" message:#"Some problem with server" delegate:self cancelButtonTitle:#"try again" otherButtonTitles:nil];
[alertView show];
} else if (errorCode == 0) {
[self dismissViewControllerAnimated:YES completion:nil];
}
}];
[self downloadData];
}
- (void)downloadData
{
[self.activityIndicator startAnimating];
[[Server sharedServer] getMovieData];
}
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
[self downloadData];
}
- (void)viewDidDisappear:(BOOL)animated
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
[super viewDidDisappear:animated];
}
#end
So I put breakpoints in begin of viewDidLoad method, in viewDidDisappear. When I launch app that first go to viewDidload, after downloading it is go to viewDidDisappear.
But during my app I again download data and post notification: NSDownloadComplete. And in this VC it is work, but I removed later using:
[[NSNotificationCenter defaultCenter] removeObserver:self]
This VC use viewDidLoad once in the beginning & can not again addObserver.
What is wrong?
EDIT
I try put addObserver method to viewWillAppear or viewWillDisappear - no results.
I add NSLog(#"addObserver"); before
[[NSNotificationCenter defaultCenter] addObserverForName...
in viewDidLoad
and write
- (void)viewDidDisappear:(BOOL)animated
{
NSLog(#"removeObserver");
[[NSNotificationCenter defaultCenter] removeObserver:self];
[super viewDidDisappear:animated];
}
In log I see:
2013-06-10 14:32:05.646 myApp[9390:c07] addObserver
2013-06-10 14:32:06.780 myApp[9390:c07] removeObserver
What wrong?
EDIT 2
you can see that observer must be removed but it again run block in addObserver method
Apart from add/remove observer calls not properly being balanced, at noted in the other answers, there is another problem.
Your code to remove the observer is wrong. For a block-based observer, the return value of addObserver must be given as argument to removeObserver. So you should add a property
#property(nonatomic, strong) id observer;
to the class. Then you add the observer with
self.observer = [[NSNotificationCenter defaultCenter] addObserverForName:NCDownloadComplete object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *n){
// ...
}];
and remove it with
[[NSNotificationCenter defaultCenter] removeObserver:self.observer];
What e1985 is trying to expose is that your addObserver and removeObserver calls are not properly balanced. viewDidLoad is called only once after the VC initialization, but viewDidDisappear is called each time the view controller is moved off screen.
To resolve your issue you must balance your addObserver and removeObserver calls, either by making them in viewDidLoad and the other in dealloc, or - as e1985 suggested - in viewDidAppear: and viewDidDisappear:.
EDIT: Ok, so your problem comes from the fact that you are using addObserverForName:object:queue:usingBlock: which do not register self as observer (as addObserver:selector:name:object: would do if you pass self as first argument).
So in your case, [[NSNotificationCenter defaultCenter] removeObserver:self]; does nothing because self is not an observer. You should instead call removeObserver: on the return value of addObserverForName:object:queue:usingBlock:, as shown in the doc:
Return Value
An opaque object to act as the observer.
So your code should looks something like:
// header file .h
#interface SplashViewVC : UIViewController
#property (strong, nonatomic) id downloadCompleteObserver;
#end
// implementation file .m
#implementation SplashViewVC
- (void)viewDidLoad
{
[super viewDidLoad];
// [...] snip
self.downloadCompleteObserver = [[NSNotificationCenter defaultCenter] addObserverForName:NCDownloadComplete object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *n){
NSInteger errorCode = [n.userInfo[#"errorCode"] integerValue];
[self.activityIndicator stopAnimating];
if (errorCode == ERROR_CODE_NO_CONNECTION) {
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:#"Error" message:#"Some problem with server" delegate:self cancelButtonTitle:#"try again" otherButtonTitles:nil];
[alertView show];
} else if (errorCode == 0) {
[self dismissViewControllerAnimated:YES completion:nil];
}
}];
[self downloadData];
}
// [...] snip
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self.downloadCompleteObserver];
[super dealloc];
}
#end
The pattern you are using is not correct. You should add the observer in viewDidAppear: and remove it in viewDidDisappear:.

Shake no longer works after a UIAlertview

I have tested this on both an iPad and the iPad Simulator and after one successful shake, the motionBegan method is never called. The following is a snippet of my program.
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
{
if(event.type == UIEventSubtypeMotionShake)
{
NSLog(#"Shake event occurred.");
UIAlertView * alert = [[UIAlertView alloc] initWithTitle:#"Information" message:#"Some message here" delegate:self cancelButtonTitle:#"Cancel" otherButtonTitles:#"OK", nil];
alert.tag = shakeTag;
[alert show];
}
}
- (BOOL)canBecomeFirstResponder
{
return YES;
}
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:NO];
[self becomeFirstResponder];
}
-(void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:NO];
}
-(void)viewDidDisappear:(BOOL)animated {
[self resignFirstResponder];
[super viewDidDisappear:NO];
}
What exactly is preventing the motionBegan method from occurring again? I would like the UIAlertView to be presented exactly once for the first shake and dismissed for all subsequent shakes. I have a hunch that the First Responder is still attached to the UIAlertView which is preventing the motionBegan method from being called again.
Update: In my corresponding UIView, there is an UIActionSheet that is created. which is called and implemented in my UIView) and I trigger the motionBegan method (which is in my UIViewController) at the same time which the UIActionSheet is displayed on the screen, the problem where the motionBegan method no longer being able to be called exists.
Afterwards, the UIAlertView is dismissed from any button selection, the motionBegan method no longer is called but the UIActionSheet works perfectly fine. There is no firstResponder assignment in UIView and only the "canBecomeFirstResponder" exists in the UIViewController. Any ideas?
Since that code is in your viewController, remove all the stuff related to responder chain. You don't need it actually. It is made automatically for you. More precisely you can remove:
- (BOOL)canBecomeFirstResponder // remove all that method
{
return YES;
}
[self becomeFirstResponder]; // remove this line
...
[self resignFirstResponder]; // remove this line
...
and remove this as well all that method
-(void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
}
Call your alertView stuff in motionEnd instead motionBegan. It could be better.

editable webView getting scrolled to top by itself

I have some text displayed in an editable webView. As soon as I scroll it down and touch somewhere to edit the rendered text, it scrolls to the top itself and the keyboard appears and hence I have to scroll it down again for editing. Is there a way to prevent webView from doing that?
Got the same problem and still looking for normal solution of this weird behavior.
We still cannot prevent UIWebView from doing this, and if you look at Evernote application on iPad, you'll see the same issue there, unfortunately :(
The only thing we could do on this is to save contentOffset of UIWebView when keyboard is shown and restore if after keyboard is opened.
This will look like:
//register your controller for keyboard notifications
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(keyboardWasShown:) UIKeyboardDidShowNotification object:nil];
Then you will need to handle keyboard notification like:
- (void)keyboardWillShow:(NSNotification *)aNotification {
// scroll view will scroll to beginning, but we save current offset
[_yourViewWithWebView saveOffset];
...
}
After that you will need to handle event when keyboard was shown:
- (void)keyboardWasShown:(NSNotification*)aNotification{
...
// scroll view scrolled to beginning, but we restore previous offset
[_yourViewWithWebView restoreOffset];
}
Accordingly in your view which contains UIWebView you'll need to implement:
static CGPoint editableWebViewOffsetPoint;
- (void) saveOffset{
editableWebViewOffsetPoint = yourWebView.scrollView.contentOffset;
}
- (void) restoreOffset{
//just use animation block to have scroll animated after jumping to top and back to old position
[UIView animateWithDuration:.2
delay:0
options:UIViewAnimationOptionCurveEaseIn
animations:^{
yourWebView.scrollView.contentOffset = editableWebViewOffsetPoint;
}
completion:nil];
}
Hope in general this will help you to solve your problem at least partially.
If someone will help us to prevent UIWebView scrolling to top each time keyboard is displayed, I'd appreciate this deeply.
UIWebView.scrollView.scrollsToTop = NO; does not help.
Disabling scrolling before showing keyboard and enabling it after keyboard is displayed also didn't work.
Also in future you will face problem with editing text when cursor is not in visible area of UIWebView - and it does not scroll itself automatically to make cursor visible. We have solved that problem, but I am in progress of creating detailed and readable tutorial of how we've done this. If you already solved this problem, I'd appreciate to look at your solution :)
PS: http://www.cocoanetics.com/2011/01/uiwebview-must-die/
Thank you,
Sergey N.
A method that works rather well is temporarily disabling setContentOffset: on UIScrollView, while the keyboard is being shown. This is a little hackish though, so it may cause other issues instead, in some situations.
As in #Sergey N.'s response, register for the keyboard notifications, but instead of storing/restoring contentOffset, use these:
- (void)keyboardWillShow:(NSNotification *)aNotification {
[self disableMethod:#selector(setContentOffset:) onClass:[UIScrollView class]];
}
- (void)keyboardWasShown:(NSNotification *)aNotification {
[self enableMethod:#selector(setContentOffset:) onClass:[UIScrollView class]];
}
Somewhere else in the class (or in another class, as long as you replace self in above calls), place these:
-(void)swizzleMethod:(SEL)origSel from:(Class)origClass toMethod:(SEL)toSel from:(Class)toClass{
Method origMethod = class_getInstanceMethod(origClass, origSel);
Method newMethod = class_getInstanceMethod(toClass, toSel);
method_exchangeImplementations(origMethod, newMethod);
}
-(void)disableMethod:(SEL)sel onClass:(Class)cl{
[self swizzleMethod:sel from:cl toMethod:#selector(doNothing) from:[self class]];
}
-(void)enableMethod:(SEL)method onClass:(Class)cl{
[self swizzleMethod:#selector(doNothing) from:[self class] toMethod:method from:cl];
}
-(void)doNothing{
}
This prevents the webview from scrolling to top in the first place, so it won't show that bad animation, however, in some situations it may cause some problems (e.g have more input controls in the view holding the webview). Tested this successfully in iOS 5.0+.
In iOS 6.0 the scrolling to top seems to be fixed, so no workaround is necessary.
Functions for "editing text when cursor is not in visible area" problem.
- (void)keyboardWasShown:(NSNotification *)aNotification {
//if(self.navigationController.viewControllers objectAtIndex:([self.navigationController.viewControllers count]-1)==self.)
NSLog(#"keyboardshown");
if (keyboardshown)
return;
keyboardshown=YES;
NSDictionary* userInfo = [aNotification userInfo];
CGRect keyboardEndFrame;
[[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] getValue:&keyboardEndFrame];
CGRect newFrame = self.textView.frame;
CGRect keyboardFrame = [self.textView convertRect:keyboardEndFrame toView:nil];
newFrame.size.height -= keyboardFrame.size.height;
[UIView beginAnimations:#"ResizeForKeyboard" context:nil];
[UIView setAnimationBeginsFromCurrentState:YES];
[UIView setAnimationDuration:0.3];
[self.textView setFrame:newFrame];
[UIView commitAnimations];
}
- (void)keyboardWasHidden:(NSNotification *)aNotification {
if (!keyboardshown)
return;
keyboardshown=NO;
NSDictionary* userInfo = [aNotification userInfo];
CGRect keyboardEndFrame;
[[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] getValue:&keyboardEndFrame];
CGRect newFrame = self.textView.frame;
CGRect keyboardFrame = [self.textView convertRect:keyboardEndFrame toView:nil];
newFrame.size.height += keyboardFrame.size.height;
[UIView beginAnimations:#"ResizeForKeyboard" context:nil];
[UIView setAnimationDuration:0.3];
self.textView.frame = newFrame;
[UIView commitAnimations];
}
Actually keyboardWasShown will not always be invoked especially when user has BT keyboard connected and virtual one can be hidden/shown by Eject key. We have implemented our own class like:
#implementation KeyboardUtils
+ (CGRect) convertRect:(CGRect)rect toView:(UIView *)view {
UIWindow *window = [view isKindOfClass:[UIWindow class]] ? (UIWindow *) view : [view window];
return [view convertRect:[window convertRect:rect fromWindow:nil] fromView:nil];
}
/**
* This is working but deprecated solution
* Based on UIKeyboardCenterBeginUserInfoKey and UIKeyboardCenterEndUserInfoKey which are deprecated since iOS 3.2
*/
+ (BOOL)checkKeyboardOnDisplayCenterBegin:(CGRect)centerBegin centerEnd:(CGRect)centerEnd{
CGRect mainScreen = [UIApplication currentBounds];
BOOL isKeyboardOnDisplay = CGRectContainsPoint(mainScreen, centerEnd.origin);
[[NSUserDefaults standardUserDefaults] setValue:[NSNumber numberWithBool:isKeyboardOnDisplay] forKey:#"isKeyboardOnDisplay"];
[[NSUserDefaults standardUserDefaults] synchronize];
return isKeyboardOnDisplay;
}
/**
* This method allows to verify if software keyboard is currently present on screen for the application
* Allows to handle undocked, split states of keyboard, as well as connected Bluetooth keyboard.
* Needed to adjust UI - scrolling and insets for editable parts of the app, as well as avoid application be beneath open keyboard
*/
+ (BOOL)checkKeyboardOnDisplayBeginFrame:(CGRect)frameBegin endFrame:(CGRect)frameEnd{
CGRect mainScreen = [UIApplication currentBounds];
UIView *firstView = [[(AppDelegate *)[[UIApplication sharedApplication] delegate] window].subviews objectAtIndex:0];
CGRect convertedEndFrame = [KeyboardUtils convertRect:frameEnd toView:firstView];
BOOL isKeyboardOnDisplay = CGRectContainsRect(mainScreen, convertedEndFrame);
[[NSUserDefaults standardUserDefaults] setValue:[NSNumber numberWithBool:isKeyboardOnDisplay] forKey:#"isKeyboardOnDisplay"];
[[NSUserDefaults standardUserDefaults] synchronize];
return isKeyboardOnDisplay;
}
+ (BOOL)checkKeyboardOnDisplayFromNotification:(NSNotification *)aNotification{
BOOL isKeyboardOnDisplay = [KeyboardUtils checkKeyboardOnDisplayBeginFrame:[[aNotification.userInfo valueForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue]
endFrame:[[aNotification.userInfo valueForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]];
return isKeyboardOnDisplay;
}
Then you can use it like:
- (void)keyboardWillChangeFrame:(NSNotification*)aNotification{
[KeyboardUtils checkKeyboardOnDisplayFromNotification:aNotification];
}
Where keyboardWillChangeFrame is selector-observer for:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(keyboardWillChangeFrame:) name:UIKeyboardWillChangeFrameNotification object:nil];
In such a way you are saving state of your keyboard (if it is shown as docked one and really present on display, and BT keyboard is not used) into NSUserDefaultSettings. In your handlers which listen to keyboard notifications or orientation changes you should check this key value from the defaults.
One more additional method is [UIApplication currentBounds];
It is present in the app by extending it with category like: (.h file)
#import <UIKit/UIKit.h>
#interface UIApplication (AppDimensions)
+(CGSize) currentSize;
+(CGRect) currentBounds;
+(CGSize) sizeInOrientation:(UIInterfaceOrientation)orientation;
#end
.m file:
#import "UIApplication+AppDimensions.h"
#implementation UIApplication (AppDimensions)
+(CGSize) currentSize
{
return [UIApplication sizeInOrientation:[UIApplication sharedApplication].statusBarOrientation];
}
+(CGRect) currentBounds{
CGRect bounds = [UIScreen mainScreen].bounds;
bounds.size = [UIApplication currentSize];
return bounds;
}
+(CGSize) sizeInOrientation:(UIInterfaceOrientation)orientation
{
CGSize size = [UIScreen mainScreen].bounds.size;
UIApplication *application = [UIApplication sharedApplication];
if (UIInterfaceOrientationIsLandscape(orientation))
{
size = CGSizeMake(size.height, size.width);
}
if (application.statusBarHidden == NO)
{
size.height -= MIN(application.statusBarFrame.size.width, application.statusBarFrame.size.height);
}
return size;
}
#end
Hope this will help anyone who is concerned about handling presence of keyboard on the screen.

Dismissing UIAlertViews when entering background state

Apple recommends dismissing any UIAlertViews/UIActionSheets when entering background state in iOS 4. This is to avoid any confusion on the user's part when he relaunches the application later. I wonder how I could elegantly dismiss all UIAlertViews at once, without retaining a reference to it everytime I set one up...
Any idea ?
My call would be to add a category to UIAlertview adding the following function :
- (void) hide {
[self dismissWithClickedButtonIndex:0 animated:YES];
}
And to suscribe to UIApplicationWillResignActiveNotification :
[[NSNotificationCenter defaultCenter] addObserver:alertView selector:#selector(hide) name:#"UIApplicationWillResignActiveNotification" object:nil];
I was intrigued by Dad's answer (funny username :), and curious why it was down-voted.
So I tried it.
Here is the .m part of a subclass of UIAlertView.
Edit: (Cédric) I have added a way to catch calls to delegate methods and remove the observer then to avoid multiple registrations to the notification center.
Everything bundled in a class in this github repo: https://github.com/sdarlington/WSLViewAutoDismiss
#import "UIAlertViewAutoDismiss.h"
#import <objc/runtime.h>
#interface UIAlertViewAutoDismiss () <UIAlertViewDelegate> {
id<UIAlertViewDelegate> __unsafe_unretained privateDelegate;
}
#end
#implementation UIAlertViewAutoDismiss
- (id)initWithTitle:(NSString *)title
message:(NSString *)message
delegate:(id)delegate
cancelButtonTitle:(NSString *)cancelButtonTitle
otherButtonTitles:(NSString *)otherButtonTitles, ...
{
self = [super initWithTitle:title
message:message
delegate:self
cancelButtonTitle:cancelButtonTitle
otherButtonTitles:nil, nil];
if (self) {
va_list args;
va_start(args, otherButtonTitles);
for (NSString *anOtherButtonTitle = otherButtonTitles; anOtherButtonTitle != nil; anOtherButtonTitle = va_arg(args, NSString *)) {
[self addButtonWithTitle:anOtherButtonTitle];
}
privateDelegate = delegate;
}
return self;
}
- (void)dealloc
{
privateDelegate = nil;
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil];
[super dealloc];
}
- (void)setDelegate:(id)delegate
{
privateDelegate = delegate;
}
- (id)delegate
{
return privateDelegate;
}
- (void)show
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(applicationDidEnterBackground:)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
[super show];
}
- (void)applicationDidEnterBackground:(NSNotification *)notification
{
[super dismissWithClickedButtonIndex:[self cancelButtonIndex] animated:NO];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil];
}
#pragma mark - UIAlertViewDelegate
// The code below avoids to re-implement all protocol methods to forward to the real delegate.
- (id)forwardingTargetForSelector:(SEL)aSelector
{
struct objc_method_description hasMethod = protocol_getMethodDescription(#protocol(UIAlertViewDelegate), aSelector, NO, YES);
if (hasMethod.name != NULL) {
// The method is that of the UIAlertViewDelegate.
if (aSelector == #selector(alertView:didDismissWithButtonIndex:) ||
aSelector == #selector(alertView:clickedButtonAtIndex:))
{
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIApplicationDidEnterBackgroundNotification
object:nil];
}
return privateDelegate;
}
else {
return [super forwardingTargetForSelector:aSelector];
}
}
#end
It works nicely.
It's great, because you can just start using it the same way that you used to use UIAlertView.
I haven't had time to test it thoroughly, but I didn't notice any side effect.
A totally different approach is a recursive search.
Recursive function for your application delegate
- (void)checkViews:(NSArray *)subviews {
Class AVClass = [UIAlertView class];
Class ASClass = [UIActionSheet class];
for (UIView * subview in subviews){
if ([subview isKindOfClass:AVClass]){
[(UIAlertView *)subview dismissWithClickedButtonIndex:[(UIAlertView *)subview cancelButtonIndex] animated:NO];
} else if ([subview isKindOfClass:ASClass]){
[(UIActionSheet *)subview dismissWithClickedButtonIndex:[(UIActionSheet *)subview cancelButtonIndex] animated:NO];
} else {
[self checkViews:subview.subviews];
}
}
}
Calling it from the applicationDidEnterBackground procedure
[self checkViews:application.windows];
huh. Haven't tried this yet, but I wonder if it would make sense to create a subclass of UIAlertView that listens for this Notification and closes itself if so...
That'd have the "automatically" without retaining / keeping it around characteristic OP is requesting. Make sure to unregister for the notification on close (else boom!)
As someone mentioned in a comment: the accepted answer isn't the best/cleanest one since iOS 4.0 when we have blocks! Here's how I do it:
UIAlertView* alert = [[UIAlertView alloc] initWithTitle:#"Alert!" message:#"This alert will dismiss when application resigns active!" delegate:self cancelButtonTitle:#"OK" otherButtonTitles:nil];
[alert show];
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillResignActiveNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification* notification){
[alert dismissWithClickedButtonIndex:0 animated:NO];
}];
UIAlertView was deprecated in iOS 8 in favor of the UIAlertController. Unfortunately, this proves to be a tricky problem because the accepted solution won't work, as Apple explicitly doesn't support subclassing UIAlertController:
The UIAlertController class is intended to be used as-is and does not support subclassing. The view hierarchy for this class is private and must not be modified.
My solution is to simply traverse the view controller tree and dismiss all UIAlertControllers that you find. You can enable this globally by creating an extension of UIApplication and then calling it in the AppDelegate applicationDidEnterBackground method.
Try this (in Swift):
extension UIApplication
{
class func dismissOpenAlerts(base: UIViewController? = UIApplication.sharedApplication().keyWindow?.rootViewController)
{
//If it's an alert, dismiss it
if let alertController = base as? UIAlertController
{
alertController.dismissViewControllerAnimated(false, completion: nil)
}
//Check all children
if base != nil
{
for controller in base!.childViewControllers
{
if let alertController = controller as? UIAlertController
{
alertController.dismissViewControllerAnimated(false, completion: nil)
}
}
}
//Traverse the view controller tree
if let nav = base as? UINavigationController
{
dismissOpenAlerts(nav.visibleViewController)
}
else if let tab = base as? UITabBarController, let selected = tab.selectedViewController
{
dismissOpenAlerts(selected)
}
else if let presented = base?.presentedViewController
{
dismissOpenAlerts(presented)
}
}
}
And then in your AppDelegate:
func applicationDidEnterBackground(application: UIApplication)
{
UIApplication.dismissOpenAlerts()
}
I Have had solved this with the following code:
/* taken from the post above (Cédric)*/
- (void)checkViews:(NSArray *)subviews {
Class AVClass = [UIAlertView class];
Class ASClass = [UIActionSheet class];
for (UIView * subview in subviews){
NSLog(#"Class %#", [subview class]);
if ([subview isKindOfClass:AVClass]){
[(UIAlertView *)subview dismissWithClickedButtonIndex:[(UIAlertView *)subview cancelButtonIndex] animated:NO];
} else if ([subview isKindOfClass:ASClass]){
[(UIActionSheet *)subview dismissWithClickedButtonIndex:[(UIActionSheet *)subview cancelButtonIndex] animated:NO];
} else {
[self checkViews:subview.subviews];
}
}
}
/*go to background delegate*/
- (void)applicationDidEnterBackground:(UIApplication *)application
{
for (UIWindow* window in [UIApplication sharedApplication].windows) {
NSArray* subviews = window.subviews;
[self checkViews:subviews];
}
}
The straightforward way is to hold a reference to the UIAlertView so you can dismiss it. Of course as petert mentioned you can do it with a Notification or use the delegate method on UIApplication
applicationWillResignActive:
does not always mean that you are going to the background. You will for example also receive that delegate call and notification (you get both) when the user gets a phone call or receives and SMS. So you have to decide what should happen if the user gets an SMS and presses cancel to stay in your app. You maybe want to make sure that your UIAlertView is still there.
So I would dismiss the UIAlertView and save the state in the delegate call when you really go into the background:
applicationDidEnterBackground:
Have a look at Session 105 - Adopting Multitasking on iOS4 of WWDC10 available for free at developer.apple.com. It gets interesting at 16:00 min
Check out this graphic to understand the different states of an application
I have this on my TODO list, but my first instinct would be to listen out for the notifcation UIApplicationWillResignActiveNotification (see UIApplication) in the views where you have things like UIAlertView - here you can programmatically remove the alert view with:
(void)dismissWithClickedButtonIndex:(NSInteger)buttonIndex animated:(BOOL)animated
The discussion for this method even suggests what it's for in iOS4!
In iPhone OS 4.0, you may want to call this method whenever your application moves to the background. An alert view is not dismissed automatically when an application moves to the background. This behavior differs from previous versions of the operating system, where they were canceled automatically when the application was terminated. Dismissing the alert view gives your application a chance to save changes or abort the operation and perform any necessary cleanup in case your application is terminated later.
if you only have one or two specific alert windows you show (as do most apps), then you can just create an assign ivar to the alert:
#property (nonatomic, assign) UIAlertView* alertview;
Then, in the app delegate:
[self.viewController.alertview dismissWithClickedButtonIndex:[self.viewController.alertview cancelButtonIndex] animated:NO];
You can put this in applicationDidEnterBackground: or wherever you see fit. It closes the alert programmatically upon application exit. I've been doing this and it works great.
Create category on UIAlert View
Use http://nshipster.com/method-swizzling/
Swizzle "show" method
Keep track of alert view shown by keeping week references in array.
-
When you want to remove all data call Dismiss on saved alert views and empty an array.
An alternative solution, based on plkEL's, answer, where the observer is removed when the app is put in the background. If user dismisses the alert by pressing a button, the observer will still be active, but only until the app is put in the background (where the block is run - with an "nil alertView" - and the observer removed).
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:title
message:message
delegate:alertDelegate
cancelButtonTitle:cancelButtonText
otherButtonTitles:okButtonText, nil];
[alert show];
__weak UIAlertView *weakAlert = alert;
__block __weak id observer = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillResignActiveNotification object:nil queue: [NSOperationQueue mainQueue] usingBlock:^(NSNotification* notification){
[weakAlert dismissWithClickedButtonIndex:[weakAlert cancelButtonIndex] animated:NO];
[[NSNotificationCenter defaultCenter] removeObserver:observer];
observer = nil;
}];

Resources