【问题标题】:UIAlertController is moved to buggy position at top of screen when it calls `presentViewController:`UIAlertController 在调用 `presentViewController:` 时被移动到屏幕顶部的错误位置
【发布时间】:2015-01-17 16:30:21
【问题描述】:

UIAlertController 显示视图会将警报移动到屏幕左上角的错误位置。 iOS 8.1、设备和模拟器。

当我们尝试从当前“最顶层”视图呈现视图时,我们在应用中注意到了这一点。如果 UIAlertController 恰好是最顶层的视图,我们就会得到这种行为。我们已将代码更改为简单地忽略 UIAlertControllers,但我发布此内容以防其他人遇到同样的问题(因为我找不到任何东西)。

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

  1. 在新的 Single View Xcode 项目中的 View Controller 上实现 viewDidAppear:
  2. 发送UIAlertController 提醒。
  3. 警报控制器立即调用presentViewController:animated:completion: 显示然后关闭另一个视图控制器:

presentViewController:... 动画开始的那一刻,UIAlertController 移动到屏幕的左上角:

dismissViewControllerAnimated: 动画结束时,警报已被进一步移动到屏幕的左上角:

完整代码:

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

【问题讨论】:

  • 奇怪的行为,感谢指出错误

标签: ios cocoa-touch ios8 uialertview uialertcontroller


【解决方案1】:

我遇到了一种情况,有时模态视图会出现在警报之上(我知道这种情况很愚蠢),并且 UIAlertController 可能出现在左上角(如2nd screenshot of the original question),我发现了一个似乎可行的单线解决方案。对于即将在 UIAlertController 上呈现的控制器,更改其模态呈现样式,如下所示:

viewControllerToBePresented.modalPresentationStyle = .OverFullScreen

这应该在您致电presentViewController(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion completion: (() -> Void)?)之前完成

【讨论】:

    【解决方案2】:

    rdar://19037589 已被 Apple 关闭

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

    没有计划根据以下情况解决此问题:

    不支持此功能,请避免在 UIAlertController 上显示。

    我们现在关闭此报告。

    如果您对解决方案有疑问,或者这对您来说仍然是一个关键问题,请使用该信息更新您的错误报告。

    请务必定期检查新的 Apple 版本,了解可能影响此问题的任何更新。

    【讨论】:

    • 如果这个问题在 iOS 更新中得到修复,我会接受不同的答案。
    • “不支持。请用余生重新设计之前稳定的架构”
    【解决方案3】:

    我也遇到了这个问题。如果我在呈现 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)
            }
        }
    }
    

    【讨论】:

    • 哈哈,这个答案如此简单明了。它对我有用,非常感谢!
    • UIAlertController 类旨在按原样使用,不支持子类化。此类的视图层次结构是私有的,不得修改。 developer.apple.com/documentation/uikit/uialertcontroller#//…
    • @BrianWhite 所以你说这行不通。或者即使它确实如此,它也不可靠并且可能会损坏......
    • 后者。如果它有效,则它不受官方支持,并且可能随时中断或产生意外结果。
    【解决方案4】:

    这有点令人失望...将警报移动到 UIViewControllers,但随后不允许它们的一些常规使用。我在一个应用程序上做类似的事情——它有时需要跳转到一个新的用户上下文,并且这样做会在现有的任何东西之上呈现一个新的视图控制器。实际上,在这种情况下,让警报成为视图控制器几乎更好,因为它们会被保留。但是,现在我们切换到 UIViewControllers 后,我们看到了相同的位移。

    我们可能不得不想出一个不同的解决方案(也许使用不同的窗口),如果顶层是 UIAlertController,我们可能会避免展示。但是,可以恢复正确的定位。这可能不是一个好主意,因为如果 Apple 更改屏幕位置,代码可能会中断,但如果非常需要此功能,以下子类似乎可以工作(在 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
    

    Apple 可能认为视图定位是私有的,因此以这种方式恢复它可能不是最好的主意,但它现在可以工作。可以将旧帧存储在 -presentViewController:animated: 的覆盖中,然后简单地恢复它而不是重新计算。

    可以调整 UIAlertController 本身以执行与上述等效的操作,这也将涵盖由您无法控制的代码呈现的 UIAlertController,但我更喜欢仅在 Apple 将要修复的错误的地方使用 swizzles (因此有一段时间可以删除调酒,我们允许现有代码“正常工作”,而不会仅仅为了解决错误而将其搞砸)。但如果这是 Apple 不打算修复的问题(由他们在此处的另一个答案中指出的回复表明),那么拥有一个单独的类来修改行为通常更安全,因此您仅在已知情况下使用它。

    【讨论】:

    • 很好的修复!这段代码我试过了,我把这段代码放在UIAlertController的类别里,所有系统警报都自动修复了。
    【解决方案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
    

    【讨论】:

    • 如果 Apple 在 UIAlertController 上添加了 -viewDidAppear: 的实际实现,您的实现将阻止调用该代码(如果尚未调用)。如果有现有的实现,您需要进行更仔细的调配。
    【解决方案6】:

    我用 swift 处理了同样的问题,我通过改变这个来解决它:

    show(chooseEmailActionSheet!, sender: self) 
    

    对此:

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

    【讨论】:

      【解决方案7】:

      我将 modalPresentationStyle 设置为 .OverFullScreen

      这对我有用。

      【讨论】:

        【解决方案8】:

        除了Carl Lindberg's answer 还应考虑两种情况:

        1. 设备旋转
        2. alert 中有文本字段时的键盘高度

        所以,对我有用的完整答案:

        // 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
        

        【讨论】:

          【解决方案9】:

          一个快速的解决方法是始终在新的 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];
          

          【讨论】:

            【解决方案10】:

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

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

            your_alert_controller_obj.updateConstraints()
            

            【讨论】:

              【解决方案11】:

              用户manoj.agg 将此答案发布到Open Radar bug report,但说:

              不知何故,我没有足够的声誉在 Stackoverflow 上发布答案。

              在这里发布他的答案以供后代使用。我没有测试/评估它。


              第 1 步:

              创建一个继承自UIViewController的自定义视图控制器并实现UIPopoverPresentationControllerDelegate

              @interface CustomUIViewController : UIViewController<UITextFieldDelegate, UIPopoverPresentationControllerDelegate>
              

              第 2 步:

              使用演示弹出框全屏显示视图:

              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];
                  });
              }];
              

              我有一个类似的问题,密码输入视图需要显示在任何其他视图控制器之上,包括 UIAlertControllers。上面的代码帮助我解决了这个问题。 iOS 8 中值得注意的变化是 UIAlertController 继承自 UIViewController,而 UIAlertView 并非如此。

              【讨论】:

              • 我怀疑使用 UIModalPresentationOverFullScreen 是这里的关键,因为这应该可以避免问题,因为它会将底层视图留在视图层次结构中,并避免在另一个视图控制器模态时对它们进行外观调用在它之上。
              • 我有一个与 pkamb 类似的要求,并在警报换档位置遇到了同样的问题。从 iOS 10 开始,我的解决方案是简单地在我呈现的视图控制器上显式设置 UIModalPresentationOverFullScreen。不需要其他代码。
              【解决方案12】:
              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)
                  })
              }
              

              【讨论】:

              • 不鼓励发布原始代码作为答案,请添加一些关于您所做的以及您的解决方案如何工作的解释。
              猜你喜欢
              • 2017-01-22
              • 2021-01-22
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2014-04-05
              • 2016-12-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多