当UIAlertController调用`presentViewController:`时,它会移动到屏幕顶部的有缺陷的位置。

48

在iOS 8.1设备和模拟器中,如果从UIAlertController呈现视图,则会将警报移到屏幕左上角的有错误的位置。

我们在一个应用程序中注意到了这一点,当我们尝试从当前“最顶部”的视图呈现视图时。如果UIAlertController恰好是最顶部的视图,就会出现这种行为。我们已经改变了我们的代码,简单地忽略UIAlertControllers,但我发帖是为了帮助其他遇到同样问题的人(因为我找不到任何东西)。

我们已将此隔离为一个简单的测试项目,完整的代码在本问题底部。

  1. 在新的Single View Xcode项目中的View Controller上实现viewDidAppear:
  2. 呈现UIAlertController警报。
  3. 警报控制器立即调用presentViewController:animated:completion:来显示并解除另一个视图控制器:

presentViewController:...动画开始时,UIAlertController就会被移动到屏幕左上角:

Alert has moved to top of screen

dismissViewControllerAnimated:动画结束时,警报已经被移动到屏幕左上角更远的位置:

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

完整代码:

- (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
}

以上代码中是否存在任何可能导致此问题的内容?是否存在其他替代方案,可以无错误地显示UIAlertController中的视图?

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


1
奇怪的行为,感谢指出错误。 - Jageen
13个回答

22

我遇到了这样一种情况:有时模态视图会出现在警报框的上方(很傻的情况,我知道),UIAlertController可能会出现在左上角(就像原始问题的第二张截图),但我找到了一个看起来有效的一行代码解决方案。对于即将呈现在UIAlertController上的控制器,请这样更改其模态呈现样式:

viewControllerToBePresented.modalPresentationStyle = .OverFullScreen

在调用presentViewController(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion completion: (() -> Void)?)之前应该执行此操作。


14

rdar://19037589 已被苹果关闭。

苹果开发者关系 | 2015年2月25日上午10:52

根据以下原因,我们没有计划解决这个问题:

UIAlertController 不支持此操作,请避免使用它进行呈现。

我们现在正在关闭此报告。

如果您对解决方案有疑问,或者仍然认为这是一个重要问题,请通过更新您的错误报告提供相关信息。

请确保定期检查新的苹果发布内容以获取可能影响此问题的任何更新。


1
如果这个问题在 iOS 更新中得到修复,我会接受不同的答案。 - pkamb
20
不支持此操作,请花费余生重新调整之前稳定的架构。 - Bradley Thomas

11

我也遇到了这个问题。如果在UIAlertController已经弹出的情况下呈现一个视图控制器,那么警告框就会跑到左上角。

我的解决方法是在viewDidLayoutSubviews中重新刷新UIAlertController视图的中心位置;通过子类化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)
        }
    }
}

哈哈,这个答案非常简单清晰。它对我有用,非常感谢你! - Tritmm
2
UIAlertController类旨在按原样使用,不支持子类化。该类的视图层次结构是私有的,不能修改。 - Brian White
@BrianWhite,你的意思是这个不起作用。或者即使它起作用了,也不可靠并且可能会出问题... - mfaani
1
后者。如果它能工作,则不受官方支持,可能会在任何时候中断或产生意外结果。 - Brian White

7
这有点令人失望...将警报移动到UIViewControllers,但随后又禁止了一些常规用法。我在一个应用程序上工作,它做了类似的事情--有时需要跳转到新的用户上下文,并且这样做会在顶部呈现一个新的视图控制器。实际上,将警报设置为视图控制器在这种情况下几乎更好,因为它们将被保留。但是,我们现在看到了同样的位移,因为我们已经切换到了UIViewControllers。
我们可能需要想出一个不同的解决方案(也许使用不同的窗口),并且如果顶层是UIAlertController,则可以避免呈现。但是,可以恢复正确的定位。这可能不是一个好主意,因为如果苹果改变了他们的屏幕定位,代码可能会中断,但是如果非常需要此功能,则以下子类(在iOS8中)似乎有效。
@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

苹果可能认为视图定位是私有的,因此以这种方式恢复可能不是最好的主意,但目前它可以工作。可能可以在 -presentViewController:animated:的覆盖中存储旧框架,并仅恢复它而不是重新计算。
可以打补丁UIAlertController本身以执行上述相当的操作,这也可以涵盖您无法控制的代码所呈现的UIAlertControllers,但我更喜欢仅在Apple将要修复错误的地方使用打补丁(因此有一个时间可以删除打补丁,并且我们允许现有的代码“只需运行”而不会搞砸它只是为了解决错误)。但是,如果苹果不打算修复它(如另一个答案中所述),那么通常更安全的方法是使用单独的类来修改行为,以便您仅在已知情况下使用它。

2
非常好的修复!我已经尝试了这段代码,将其放入了UIAlertController类别中,所有系统警报都自动修复了。 - Michaël

5

我认为你应该将UIAlertController归类如下:

@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

1
如果苹果在UIAlertController上添加了-viewDidAppear:的实际实现,您的实现将防止该代码被调用(如果尚未调用)。如果存在现有实现,则需要进行更仔细的交换。 - Carl Lindberg

2

我将modalPresentationStyle设置为.OverFullScreen

这对我有效。


2
我曾经也遇到了Swift相关的同样问题,我通过更改以下内容来解决它:

最初的回答:

show(chooseEmailActionSheet!, sender: self) 

to this:

self.present(chooseEmailActionSheet!, animated: true, completion: nil) 

1
除了Carl Lindberg的回答之外,还有两种情况也应该考虑:
  1. 设备旋转
  2. 当警报框内有文本字段时,键盘高度
因此,对我有效的完整答案是:
// 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;
        }
    }
}

此外,如果使用分类,则需要以某种方式存储键盘高度,例如:

@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

1

一个快速的解决方法是始终将视图控制器呈现在新的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];

1

编辑:在2020年测试,Xcode 11.2,iOS 13

如果有人仍然在寻找更好的答案,那么这是我的解决方案。 使用updateConstraints方法来重新调整约束。

your_alert_controller_obj.updateConstraints()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接