UIAlertController перемещается в положение "багги" в верхней части экрана, когда он вызывает "presentViewController":

Представление представления из UIAlertController переводит предупреждение в положение с ошибкой в верхнем левом углу экрана. iOS 8.1, устройство и симулятор.

Мы заметили это в приложении, когда пытаемся представить представление из текущего "самого верхнего" представления. Если UIAlertController оказывается самым верхним видом, мы получаем такое поведение. Мы изменили наш код, чтобы просто игнорировать UIAlertControllers, но я отправляю его, если другие попадают в ту же проблему (поскольку я ничего не мог найти).

Мы выделили это на простой тестовый проект, полный код внизу этого вопроса.

  1. Реализовать viewDidAppear: на контроллере представления в новом проекте Single View Xcode.
  2. Представьте предупреждение UIAlertController.
  3. Контроллер Alert немедленно вызывает presentViewController:animated:completion: для отображения и затем отклонения другого контроллера представления:

Когда presentViewController:... анимация presentViewController:..., UIAlertController переместится в верхний левый угол экрана:

Alert has moved to top of screen

Когда dismissViewControllerAnimated: анимация 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 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

rdar://19037589 был закрыт Apple

Отношения с разработчиками Apple | 25-Feb-2015 10:52 AM

Планируется решить эту проблему на основе следующего:

Это не поддерживается, пожалуйста, избегайте представления на UIAlertController.

Сейчас мы закрываем этот отчет.

Если у вас есть вопросы по поводу разрешения или если это по-прежнему является важной проблемой для вас, пожалуйста, обновите отчет об ошибке этой информацией.

Пожалуйста, регулярно проверяйте новые выпуски Apple для любых обновлений, которые могут повлиять на эту проблему.

Ответ 2

Я столкнулся с ситуацией, когда иногда модальное представление представляло себя поверх предупреждения (глупая ситуация, я знаю), и UIAlertController может появиться в верхнем левом углу (например, второй скриншот исходного вопроса), и я нашел однострочное решение, которое, похоже, работает. Для контроллера, который должен быть представлен в UIAlertController, измените его модальный стиль презентации так:

viewControllerToBePresented.modalPresentationStyle = .OverFullScreen

Это должно быть сделано непосредственно перед вызовом presentViewController(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion completion: (() → Void)?)

Ответ 3

Это немного разочаровывает... движущиеся оповещения будут 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: анимированный :, и просто восстановить это вместо повторного вычисления.

Возможно, что сам UIAlertController сам выполнит эквивалент вышеуказанного, который также будет охватывать UIAlertControllers, представленные кодом, который вы не контролируете, но я предпочитаю использовать только swizzles в тех местах, где это ошибка, которую Apple собирается исправить (таким образом, там это время, когда swizzle можно удалить, и мы разрешаем существующий код "просто работать", не отбрасывая его только для обхода ошибки). Но если это то, что Apple не собирается исправлять (указано в ответе, как указано в другом ответе здесь), тогда обычно безопаснее иметь отдельный класс для изменения поведения, поэтому вы используете его только в известных обстоятельствах.

Ответ 4

Я думаю, что вам нужно только классифицировать 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

Ответ 5

У меня тоже была эта проблема. Если бы я представил контроллер представления, когда был представлен UIAlertController, предупреждение переместилось бы вверху слева.

Мое исправление - обновить центр представления UIAlertController в viewDidLayoutSubviews; достигается путем подклассификации 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)
        }
    }
}

Ответ 6

В дополнение к ответу Карла Линдберда. Следует учитывать и два случая:

  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

Ответ 7

Быстрое решение состоит в том, чтобы всегда представлять View Controller поверх нового 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];

Ответ 8

Пользователь manoj.agg отправил этот ответ в отчет об ошибке Open Radar, но говорит:

Почему-то у меня недостаточно репутации, чтобы публиковать ответы на Stackoverflow.

Проводя свой ответ здесь для потомков. Я не тестировал/не оценивал его.


Шаг 1:

Создайте собственный контроллер вида, наследующий от UIViewController и реализующий UIPopoverPresentationControllerDelegate:

@interface CustomUIViewController : UIViewController<UITextFieldDelegate, UIPopoverPresentationControllerDelegate>

Шаг 2:

Представьте представление в полноэкранном режиме, используя презентацию 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];
    });
}];

У меня была аналогичная проблема, когда представление ввода пароля необходимо было отображать поверх любого другого контроллера просмотра, включая UIAlertControllers. Вышеприведенный код помог мне в решении проблемы. Примечательное изменение в iOS 8 состоит в том, что UIAlertController наследует UIViewController, чего не было для UIAlertView.

Ответ 9

Я имел дело с той же проблемой с swift, и я исправил это, изменив это:

show(chooseEmailActionSheet!, sender: self) 

с этим:

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

Ответ 10

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