UIAlertController is moved to buggy position at to

2020-02-02 11:40发布

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

11条回答
ゆ 、 Hurt°
2楼-- · 2020-02-02 11:59

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) 
查看更多
Fickle 薄情
3楼-- · 2020-02-02 12:00

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.

查看更多
何必那么认真
4楼-- · 2020-02-02 12:01

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.

查看更多
\"骚年 ilove
5楼-- · 2020-02-02 12:02

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
查看更多
狗以群分
6楼-- · 2020-02-02 12:02

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
查看更多
姐就是有狂的资本
7楼-- · 2020-02-02 12:03
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)
    })
}
查看更多
登录 后发表回答