32

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 ?

hpique
  • 112,774
  • 126
  • 328
  • 461
François P.
  • 5,136
  • 4
  • 30
  • 31

12 Answers12

26

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];
Mark Coleman
  • 39,076
  • 9
  • 79
  • 100
Charter
  • 783
  • 4
  • 6
  • 3
    Thanks this gives me a good idea on how to achieve what I want. I'll probably add two methods in a category - (void) showAndHidAutomatically, which will register itself and call show, upon the notification will call another method called hideAndUnregister, which will do just that. Enjoy your bounty reward :-) – François P. Jul 11 '10 at 13:40
  • I did some modifications: `[[NSNotificationCenter defaultCenter] addObserver:alert selector:@selector(hide) name:UIApplicationDidEnterBackgroundNotification object:nil];` –  Mar 06 '14 at 12:47
  • I have a question. I have removed an observer in hide-method. It's fine. But how remove observer if AlertView closed by user (method "hide" didn't invoked)? In NoificationCenter stored a dead reference on dismissed AlertView? – Serge Maslyakov Jun 27 '14 at 13:03
24

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.

Community
  • 1
  • 1
Guillaume
  • 21,205
  • 6
  • 60
  • 95
  • 1
    The only change I would make is to register the observer in the show method instead of init. – gotosleep Jul 07 '10 at 13:43
  • @Gotosleep good point, it would avoid to call - dismissWithClickedButtonIndex: animated: on an not visible alert. Don't think it's a problem at the moment, but will be better for future compatibility. In this case, make sure that removing an observer not registered is ok, or better, adapt the code to avoid it. – Guillaume Jul 07 '10 at 14:44
  • The Problem with this answer is that you will receive the applicationWillResignActive notification also when you are just receiving an SMS which you dismiss. Then you are back in your application and NOT in the background, but you dismissed your alert already. I do not think that this is properly handled. Take a look at the graphik I posted as a link in my answer below. – GorillaPatch Jul 08 '10 at 11:50
  • 1
    Btw, this is a nice example for using the new block syntax instead of the badly named applicationWillResignActive method. Why do you define if with the sender and do not use it? I think this code is not well designed, both in how it is implemented and also conceptionally. – GorillaPatch Jul 08 '10 at 11:54
  • @GorillaPatch You're correct. I edited the answer to register to the proper notification. Nice workflow graphic! – Guillaume Jul 08 '10 at 12:02
  • @Guillaume I think it is Apple's fault. They claim you used compile against iOS4 and everything works out of the box. But it does not. One has to really think how to manage the state of the application as there are new entry points now. – GorillaPatch Jul 08 '10 at 12:33
  • @GorillaPatch Regarding your second comment, I do not understand your question "Why do you define if with the sender and do not use it?". What do you mean? Regarding the code design and implementation quality: I spend time on SO to learn and improve myself. Always appreciate to get feedback. What do you think is wrong in this code? Seems good to me. What would you change? – Guillaume Jul 08 '10 at 12:59
  • @Guillaume My question was if a function has an argument, such as your - (void) applicationDidEnterBackground:(id) sender, the you usually do something with the argument in this case "sender" in the function, which you do not. I was just wondering why it is there. BTW if you like that graphic, feel free to upvote the answer ... – GorillaPatch Jul 08 '10 at 19:12
  • @Guillaume Regarding the block syntax: your code could look like this: [[NSNotificationCenter defaultCenter] addObserverForName: UIApplicationDidEnterBackgroundNotification object:self queue:nil usingBlock:^{[self dismissWithClickedButtonIndex:[self cancelButtonIndex] animated:NO];}]; Notice that I am not using a string like you do, but instead a static string defined by UIKit. This way you avoid typos and typing as it is auto-completed by Xcode. – GorillaPatch Jul 08 '10 at 19:23
  • 3
    In case anyone want to use code like this, I have created a repository on github that implements it and the equivalent for UIActionSheet: http://github.com/sdarlington/UIViewAutoDismiss – Stephen Darlington Sep 29 '10 at 12:56
  • @GorillaPatch: the reason for the sender parameter is that it is required by the [NSNotificationCenter class](http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/Foundation/Classes/NSNotificationCenter_Class/Reference/Reference.html), the selector for an observer must have 1 parameter, even though it is not needed here. However, it is misleading to type/name it "(id)sender" because it is actually a NSNotification*. – progrmr Aug 19 '11 at 12:54
  • Before you use this, I would warn you to check Apple's documentation - where it mentioned "The UIAlertView 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." You app might either get rejected or does not work properly in the future. – Byte Feb 27 '13 at 21:41
  • 1
    This worked well for me, however I was able to simplify it. Remove everything except for `dealloc`, `show`, and `applicationDidEnterBackground:` methods. Remove `privateDelegate` and leave the caller to define the delegate. The `UIAlertView` object is registered for the notification in `show` and deregistered in `dealloc`. Everything works perfectly for me. The only problem I could see is if you were to hold on to your `UIAlertView` references for much longer than it is visible. – Aaron May 13 '13 at 18:46
  • the private delegate from Cédric does not work. just use dealloc to remove the observer. I've tested that and it works just find. The private delegate the was added doesn't actually cause the superclass to call the methods on the delegate – Alpine Dec 03 '13 at 00:12
  • @StephenDarlington the link to github in your comment is broken. I added the fixed link in the answer. – Guillaume Jun 23 '14 at 13:16
  • @Guillaume Thanks. GitHub say that they redirect when you rename repositories... I guess that doesn't always work! – Stephen Darlington Jun 23 '14 at 13:21
18

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];
auco
  • 8,812
  • 2
  • 42
  • 53
Wilbert
  • 197
  • 2
  • 2
  • Worked great for me. I did refactor it into two UIView category methods, though. `- (void)dismissActionSheetsSubviews:(BOOL)animated` and `- (void)dismissAlertViewSubviews:(BOOL)animated {`. – zekel Jun 24 '11 at 17:52
  • 7
    This unfortunately no longer works in iOS7 because UIalertviews are no longer subviews of one of the application's windows – Brad Thomas Mar 21 '14 at 18:10
  • @fibnochi: please make a new answer out of your edit or use a comment. It really makes no sense to change the original post in such a drastic way. Thanks! – auco May 14 '14 at 14:49
12

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];
    }];
pIkEL
  • 541
  • 5
  • 14
  • 1
    I'm wondering why this answer isn't upvoted more. It works really well and is very simple. Is this a poor solution and I'm not seeing why? – Jordan H Oct 16 '14 at 20:31
  • I'm not sure, but should the observer be removed when the alert is dismissed? – SAHM Nov 03 '14 at 23:59
  • 1
    The reason why this isn't upvoted more is because, you have to do this to every alertview you show in your app. Some apps present many many UIAlertViews and it would be tedious to go around doing this everywhere. – wsidell Nov 25 '14 at 21:04
  • @wsidell That's when you take advantage of CLANG and create a shortcut like `[alert showWithAutoDismiss];` where `showWithAutoDismiss` is all defined with the above logic in one CLANG declaration. :D – Albert Renshaw Jun 29 '16 at 07:33
12

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!)

Dad
  • 5,473
  • 1
  • 24
  • 32
  • "Bravo!" to Guillaume for taking the time to write the test code I didn't have time to write. http://stackoverflow.com/questions/3105974/dismissing-uialertviews-when-entering-background-state/3181223#3181223 Glad it worked as I'd imagined it would. Seems like a pretty clean and complete answer to the problem to me. Elegant even (?), if I do say so myself :-? – Dad Jul 08 '10 at 02:13
  • This is an elegant solution. It shows an appreciation for the strengths of Objective-C. – TechZen Jul 10 '10 at 22:19
8

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()
}
jcady
  • 3,278
  • 1
  • 17
  • 20
  • I might be missing something at the first glance, but is there any reason why this recursive function should be returning UIViewController? – Andriy Gordiychuk Mar 16 '16 at 22:53
  • 1
    @AndriyGordiychuk Good call, I made this by porting another method I wrote to get the top-most view controller, but forgot to remove the return. Updated, thanks. – jcady Mar 17 '16 at 20:56
7

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];
    }
}
Ponja
  • 664
  • 1
  • 6
  • 15
3

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

GorillaPatch
  • 4,925
  • 1
  • 33
  • 54
1

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.

petert
  • 6,552
  • 3
  • 34
  • 45
  • But Francois asked for a solution without retaining the UIAlertView, if you don't retain (i.e retain count = 0), you can never call dismiss. I didn't try but what the behaviour is when you don't call dismiss? Will it appear again when you move back your app to foreground? – vodkhang Jun 24 '10 at 10:02
  • 1
    Good point, I also follow `[alertView show]` by `[alertView release]`. Docs for `UIAlertView` say "Now, it is up to you to decide whether to dismiss the alert view (and execute its cancellation handler) or leave it visible for when your application moves back to the foreground. Remember that your application can still be terminated while in the background, so some type of action may be necessary in either case.". So i guess you need to decide to keep a reference around or not. – petert Jun 24 '10 at 11:06
0

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.

0

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;
   }];
Community
  • 1
  • 1
ckibsen
  • 923
  • 1
  • 7
  • 9
0

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.

Mark Coleman
  • 39,076
  • 9
  • 79
  • 100
johnbakers
  • 22,776
  • 20
  • 106
  • 230