UIAlertController is moved to buggy position at top of screen when it calls `presentViewController:`
Asked Answered
H

13

48

Presenting a view from a UIAlertController moves the alert to a buggy position at the top-left corner of the screen. iOS 8.1, device and simulator.

We have noticed this in an app when we attempt to present a view from the current "top-most" view. If a UIAlertController happens to be the top-most view we get this behavior. We have changed our code to simply ignore UIAlertControllers, but I'm posting this in case others hit the same issue (as I couldn't find anything).

We have isolated this to a simple test project, full code at the bottom of this question.

  1. Implement viewDidAppear: on the View Controller in a new Single View Xcode project.
  2. Present aUIAlertController alert.
  3. Alert controller immediately calls presentViewController:animated:completion: to display and then dismiss another view controller:

The moment the presentViewController:... animation begins, the UIAlertController is moved to the top-left corner of the screen:

Alert has moved to top of screen

When the dismissViewControllerAnimated: animation ends, the alert has been moved even further into the top-left margin of the screen:

Alert has moved into the top-left margin of the screen

Full code:

- (void)viewDidAppear:(BOOL)animated {
    // Display a UIAlertController alert
    NSString *message = @"This UIAlertController will be moved to the top of the screen if it calls `presentViewController:`";
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"UIAlertController iOS 8.1" message:message preferredStyle:UIAlertControllerStyleAlert];
    [alert addAction:[UIAlertAction actionWithTitle:@"I think that's a Bug" style:UIAlertActionStyleCancel handler:nil]];
    [self presentViewController:alert animated:YES completion:nil];

    // The UIAlertController should Present and then Dismiss a view
    UIViewController *viewController = [[UIViewController alloc] init];
    viewController.view.backgroundColor = self.view.tintColor;
    [alert presentViewController:viewController animated:YES completion:^{
        dispatch_after(0, dispatch_get_main_queue(), ^{
            [viewController dismissViewControllerAnimated:YES completion:nil];
        });
    }];

    // RESULT:
    // UIAlertController has been moved to the top of the screen.
    // http://i.imgur.com/KtZobuK.png
}

Is there anything in the above code that would be causing this issue? Do any alternatives exist that would allow bug-free presentation of a view from a UIAlertController?

rdar://19037589
http://openradar.appspot.com/19037589

Humfrey answered 19/11, 2014 at 23:24 Comment(1)
weird behavior, Thanks for pointing out bugLibelant
H
14

rdar://19037589 was closed by Apple

Apple Developer Relations | 25-Feb-2015 10:52 AM

There are no plans to address this based on the following:

This isn't supported, please avoid presenting on a UIAlertController.

We are now closing this report.

If you have questions about the resolution, or if this is still a critical issue for you, then please update your bug report with that information.

Please be sure to regularly check new Apple releases for any updates that might affect this issue.

Humfrey answered 25/2, 2015 at 23:3 Comment(2)
I will accept a different answer if this is fixed in an iOS update.Humfrey
"This isn't supported. Please spend the rest of your life re-working architecture that was previously stable"Roseroseann
P
22

I encountered a situation where sometimes a modal view would present itself on top of a an alert (silly situation, I know), and the UIAlertController could appear in the top left (like the 2nd screenshot of the original question), and I found a one-liner solution that seems to work. For the controller that's about to be presented on the UIAlertController, change its modal presentation style like so:

viewControllerToBePresented.modalPresentationStyle = .OverFullScreen

This should be done just before you call presentViewController(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion completion: (() -> Void)?)

Peduncle answered 8/6, 2016 at 21:24 Comment(0)
H
14

rdar://19037589 was closed by Apple

Apple Developer Relations | 25-Feb-2015 10:52 AM

There are no plans to address this based on the following:

This isn't supported, please avoid presenting on a UIAlertController.

We are now closing this report.

If you have questions about the resolution, or if this is still a critical issue for you, then please update your bug report with that information.

Please be sure to regularly check new Apple releases for any updates that might affect this issue.

Humfrey answered 25/2, 2015 at 23:3 Comment(2)
I will accept a different answer if this is fixed in an iOS update.Humfrey
"This isn't supported. Please spend the rest of your life re-working architecture that was previously stable"Roseroseann
C
11

I was having this issue as well. If I presented a view controller while a UIAlertController was presented, the alert would go to the top left.

My fix is to refresh the center of the UIAlertController's view in viewDidLayoutSubviews; achieved by subclassing UIAlertController.

class MyBetterAlertController : UIAlertController {

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        let screenBounds = UIScreen.mainScreen().bounds

        if (preferredStyle == .ActionSheet) {
            self.view.center = CGPointMake(screenBounds.size.width*0.5, screenBounds.size.height - (self.view.frame.size.height*0.5) - 8)
        } else {
            self.view.center = CGPointMake(screenBounds.size.width*0.5, screenBounds.size.height*0.5)
        }
    }
}
Crawler answered 23/6, 2016 at 14:20 Comment(4)
haha, this answer so simple and clean. it worked for me, thanks you so much!Amherst
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. developer.apple.com/documentation/uikit/uialertcontroller#//…Falk
@BrianWhite so you're saying this doesn't work. Or that even if it does, it's not reliable and could break...Pleuron
The latter. If it works, it's not officially supported and could break or have unexpected results at any time.Falk
S
7

That is a bit disappointing... moving alerts to be UIViewControllers, but then disallowing some regular usage of them. I work on an application which did something similar -- it sometimes needs to jump to a new user context, and doing that presented a new view controller over top of whatever was there. Actually having the alerts be view controllers is almost better in this case, as they would be preserved. But we are seeing the same displacement now that we have switched to UIViewControllers.

We may have to come up with a different solution (using different windows perhaps), and maybe we avoid presenting if the top level is a UIAlertController. But, it is possible to restore the correct positioning. It may not be a good idea, because the code could break if Apple ever changes their screen positioning, but the following subclass seems to work (in iOS8) if this functionality is very much needed.

@interface MyAlertController : UIAlertController
@end

@implementation MyAlertController
/*
 * UIAlertControllers (of alert type, and action sheet type on iPhones/iPods) get placed in crazy
 * locations when you present a view controller over them.  This attempts to restore their original placement.
 */
- (void)_my_fixupLayout
{
    if (self.preferredStyle == UIAlertControllerStyleAlert && self.view.window)
    {
        CGRect myRect = self.view.bounds;
        CGRect windowRect = [self.view convertRect:myRect toView:nil];
        if (!CGRectContainsRect(self.view.window.bounds, windowRect) || CGPointEqualToPoint(windowRect.origin, CGPointZero))
        {
            CGPoint center = self.view.window.center;
            CGPoint myCenter = [self.view.superview convertPoint:center fromView:nil];
            self.view.center = myCenter;
        }
    }
    else if (self.preferredStyle == UIAlertControllerStyleActionSheet && self.traitCollection.userInterfaceIdiom == UIUserInterfaceIdiomPhone && self.view.window)
    {
        CGRect myRect = self.view.bounds;
        CGRect windowRect = [self.view convertRect:myRect toView:nil];
        if (!CGRectContainsRect(self.view.window.bounds, windowRect) || CGPointEqualToPoint(windowRect.origin, CGPointZero))
        {
            UIScreen *screen = self.view.window.screen;
            CGFloat borderPadding = ((screen.nativeBounds.size.width / screen.nativeScale) - myRect.size.width) / 2.0f;
            CGRect myFrame = self.view.frame;
            CGRect superBounds = self.view.superview.bounds;
            myFrame.origin.x = CGRectGetMidX(superBounds) - myFrame.size.width / 2;
            myFrame.origin.y = superBounds.size.height - myFrame.size.height - borderPadding;
            self.view.frame = myFrame;
        }
    }
}

- (void)viewWillLayoutSubviews
{
    [super viewWillLayoutSubviews];
    [self _my_fixupLayout];
}

@end

Apple may consider the view positioning to be private, so restoring it in this way may not be the best idea, but it works for now. It might be possible to store off the old frame in an override of -presentViewController:animated:, and simply restore that instead of re-calculating.

It's possible to swizzle UIAlertController itself to do the equivalent of the above, which would also cover UIAlertControllers presented by code you don't control, but I prefer to only use swizzles in places where it's a bug that Apple is going to fix (thus there is a time when the swizzle can be removed, and we allow existing code to "just work" without mucking it up just for a bug workaround). But if it's something that Apple is not going to fix (indicated by their reply as noted in another answer here), then it's usually safer to have a separate class to modify behavior, so you are using it only in known circumstances.

Saying answered 4/3, 2015 at 18:53 Comment(1)
Very good fix ! I have tried this code, I have put this code in a category of UIAlertController, and all system alerts are automatically fixed.Constringent
F
5

I think that you only should to categorize UIAlertController like this:

@implementation UIAlertController(UIAlertControllerExtended)

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];

    if (self.preferredStyle == UIAlertControllerStyleAlert) {
        __weak UIAlertController *pSelf = self;

        dispatch_async(dispatch_get_main_queue(), ^{
            CGRect screenRect = [[UIScreen mainScreen] bounds];
            CGFloat screenWidth = screenRect.size.width;
            CGFloat screenHeight = screenRect.size.height;

            [pSelf.view setCenter:CGPointMake(screenWidth / 2.0, screenHeight / 2.0)];
            [pSelf.view setNeedsDisplay];
        });
    }
}

@end
Faiyum answered 23/12, 2015 at 2:52 Comment(1)
If Apple ever adds an actual implementation of -viewDidAppear: on UIAlertController, your implementation will prevent that code from ever being called, if it's not already. You need to do a more careful swizzle if there is an existing implementation.Saying
D
2

I was dealing with the same problem with swift, and I fixed it by changing this:

show(chooseEmailActionSheet!, sender: self) 

to this:

self.present(chooseEmailActionSheet!, animated: true, completion: nil) 
Dux answered 28/1, 2019 at 19:4 Comment(0)
H
2

I set modalPresentationStyle to .OverFullScreen

This worked for me.

Hege answered 7/3, 2019 at 14:7 Comment(0)
W
1

In addition to Carl Lindberg's answer There are two cases that also should be taken into account:

  1. Device rotating
  2. Keyboard height when there is a text field inside alert

So, the full answer that worked for me:

// fix for rotation

-(void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
    [coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
    } completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
        [self.view setNeedsLayout];
    }];
}

// fix for keyboard

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
}

-(void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillHideNotification object:nil];
}

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (void)keyboardWillShow:(NSNotification *)notification
{
    NSDictionary *keyboardUserInfo = [notification userInfo];
    CGSize keyboardSize = [[keyboardUserInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size;
    self.keyboardHeight = keyboardSize.height;
    [self.view setNeedsLayout];
}

- (void)keyboardWillHide:(NSNotification *)notification
{
    self.keyboardHeight = 0;
    [self.view setNeedsLayout];
}

// position layout fix

-(void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];
    [self fixAlertPosition];
}

-(void)fixAlertPosition
{
    if (self.preferredStyle == UIAlertControllerStyleAlert && self.view.window)
    {
        CGRect myRect = self.view.bounds;
        CGRect windowRect = [self.view convertRect:myRect toView:nil];
        if (!CGRectContainsRect(self.view.window.bounds, windowRect) || CGPointEqualToPoint(windowRect.origin, CGPointZero))
        {
            CGRect myFrame = self.view.frame;
            CGRect superBounds = self.view.superview.bounds;
            myFrame.origin.x = CGRectGetMidX(superBounds) - myFrame.size.width / 2;
            myFrame.origin.y = (superBounds.size.height - myFrame.size.height - self.keyboardHeight) / 2;
            self.view.frame = myFrame;
        }
    }
    else if (self.preferredStyle == UIAlertControllerStyleActionSheet && self.traitCollection.userInterfaceIdiom == UIUserInterfaceIdiomPhone && self.view.window)
    {
        CGRect myRect = self.view.bounds;
        CGRect windowRect = [self.view convertRect:myRect toView:nil];
        if (!CGRectContainsRect(self.view.window.bounds, windowRect) || CGPointEqualToPoint(windowRect.origin, CGPointZero))
        {
            UIScreen *screen = self.view.window.screen;
            CGFloat borderPadding = ((screen.nativeBounds.size.width / screen.nativeScale) - myRect.size.width) / 2.0f;
            CGRect myFrame = self.view.frame;
            CGRect superBounds = self.view.superview.bounds;
            myFrame.origin.x = CGRectGetMidX(superBounds) - myFrame.size.width / 2;
            myFrame.origin.y = superBounds.size.height - myFrame.size.height - borderPadding;
            self.view.frame = myFrame;
        }
    }
}

Also, if using category, then you need to store keyboard height somehow, like this:

@interface UIAlertController (Extended)

@property (nonatomic) CGFloat keyboardHeight;

@end

@implementation UIAlertController (Extended)

static char keyKeyboardHeight;

- (void) setKeyboardHeight:(CGFloat)height {
    objc_setAssociatedObject (self,&keyKeyboardHeight,@(height),OBJC_ASSOCIATION_RETAIN);
}

-(CGFloat)keyboardHeight {
    NSNumber *value = (id)objc_getAssociatedObject(self, &keyKeyboardHeight);
    return value.floatValue;
}

@end
Walsh answered 22/2, 2017 at 17:23 Comment(0)
P
1

A quick fix is to always present the View Controller on top of a new UIWindow:

UIWindow *window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
window.rootViewController = [[UIViewController alloc] init];
window.windowLevel = UIWindowLevelNormal;
[window makeKeyAndVisible];
[window.rootViewController presentViewController: viewController
                                        animated:YES 
                                      completion:nil];
Pompeii answered 21/1, 2019 at 8:55 Comment(0)
V
1

EDIT: Tested in 2020, Xcode 11.2, iOS 13

If anyone still looking for a better answer to this, then here is my solution. Use updateConstraints method to readjust the constraints.

your_alert_controller_obj.updateConstraints()
Volcanology answered 20/4, 2020 at 7:41 Comment(0)
H
0

User manoj.agg posted this answer to the Open Radar bug report, but says:

Somehow I don't have enough reputation to post answers on Stackoverflow.

Posting his answer here for posterity. I have not tested/evaluated it.


Step 1:

Create a custom View Controller inheriting from UIViewController and implement UIPopoverPresentationControllerDelegate:

@interface CustomUIViewController : UIViewController<UITextFieldDelegate, UIPopoverPresentationControllerDelegate>

Step 2:

Present the view in fullscreen, making use of the presentation popover:

CustomUIViewController *viewController = [[CustomUIViewController alloc] init];
viewController.view.backgroundColor = self.view.tintColor;
viewController.modalPresentationStyle = UIModalPresentationOverFullScreen;

UIPopoverPresentationController *popController = viewController.popoverPresentationController;
popController.delegate = viewController;

[alert presentViewController:viewController animated:YES completion:^{
    dispatch_after(0, dispatch_get_main_queue(), ^{
        [viewController dismissViewControllerAnimated:YES completion:nil];
    });
}];

I had a similar problem where a password input view needed to be displayed on top of any other View Controller, including UIAlertControllers. The above code helped me in solving the problem. Noteworthy change in iOS 8 is that UIAlertController inherits from UIViewController, which was not the case for UIAlertView.

Humfrey answered 7/5, 2015 at 18:42 Comment(2)
I suspect the use of UIModalPresentationOverFullScreen is the key here, as that should avoid the problem since it would leave the underlying views in the view hierarchy, and avoid the appearance calls on them while another view controller is modally on top of it.Saying
I had a similar requirement as pkamb and ran into the same problem with the alert shifting position. As of iOS 10 the solution for me was to simply set UIModalPresentationOverFullScreen explicitly on the view controller I was presenting. No other code was required.Scattering
P
0

In my case I was using func show(_ vc: UIViewController, sender: Any?) for my UIAlertController.

It used to work perfectly for my alerts but for some reason stopped working.

I replaced:

func show(_ vc: UIViewController, sender: Any?)

with:

func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil)

Now working like a charm.

Pyrogen answered 13/2, 2023 at 18:22 Comment(0)
S
-4
var kbHeight: CGFloat = 0    

override func keyboardWillShow(_ notification: Notification) {
    if let userInfo = notification.userInfo {
        if let keyboardSize =  (userInfo[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
            kbHeight = keyboardSize.height
            self.animateTextField(up: true)
        }
    }
}

override func keyboardWillHide(_ notification: Notification) {
      self.animateTextField(up: false)
}


func animateTextField(up: Bool) {
    let movement = (up ? -kbHeight : kbHeight)
    UIView.animate(withDuration: 0.3, animations: {
        self.view.frame =  CGRect.offsetBy(self.view.frame)(dx: 0, dy: movement)
    })
}
Schlep answered 18/5, 2018 at 10:16 Comment(1)
Posting raw code as an answer is discouraged, please, add some explanations about what you did and how your solution works.Turtleneck

© 2022 - 2024 — McMap. All rights reserved.