chromium/ios/chrome/browser/alert_view/ui_bundled/alert_view_controller.mm

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/alert_view/ui_bundled/alert_view_controller.h"

#import <ostream>

#import "base/check_op.h"
#import "base/ios/ios_util.h"
#import "base/notreached.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/ui/elements/gray_highlight_button.h"
#import "ios/chrome/browser/shared/ui/elements/text_field_configuration.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/alert_view/ui_bundled/alert_action.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"

namespace {

// Properties of the alert shadow.
constexpr CGFloat kShadowOffsetX = 0;
constexpr CGFloat kShadowOffsetY = 15;
constexpr CGFloat kShadowRadius = 13;
constexpr float kShadowOpacity = 0.12;

// Properties of the alert view.
constexpr CGFloat kCornerRadius = 14;
constexpr CGFloat kAlertWidth = 270;
constexpr CGFloat kAlertWidthAccessibility = 402;
constexpr CGFloat kTextFieldCornerRadius = 5;
constexpr CGFloat kMinimumHeight = 30;
constexpr CGFloat kMinimumMargin = 4;

// Inset at the top of the alert. Is always present.
constexpr CGFloat kAlertMarginTop = 22;
// Space before the actions and everything else.
constexpr CGFloat kAlertActionsSpacing = 12;

// Insets for the content in the alert view.
constexpr CGFloat kTitleInsetLeading = 20;
constexpr CGFloat kTitleInsetBottom = 9;
constexpr CGFloat kTitleInsetTrailing = 20;

constexpr CGFloat kSpinnerInsetTop = 12;
constexpr CGFloat kSpinnerInsetBottom = 14;

constexpr CGFloat kMessageInsetLeading = 20;
constexpr CGFloat kMessageInsetBottom = 6;
constexpr CGFloat kMessageInsetTrailing = 20;

constexpr CGFloat kButtonInsetTop = 13;
constexpr CGFloat kButtonInsetLeading = 20;
constexpr CGFloat kButtonInsetBottom = 13;
constexpr CGFloat kButtonInsetTrailing = 20;

constexpr CGFloat kTextfieldStackInsetTop = 12;
constexpr CGFloat kTextfieldStackInsetLeading = 12;
constexpr CGFloat kTextfieldStackInsetTrailing = 12;

constexpr CGFloat kTextfieldInset = 8;

// This is how many bits UIViewAnimationCurve needs to be shifted to be in
// UIViewAnimationOptions format. Must match the one in UIView.h.
constexpr NSUInteger kUIViewAnimationCurveToOptionsShift = 16;

// The amount of time (in seconds) to wait before enabling the action buttons.
// This is only used if `actionButtonsAreInitiallyDisabled` is true.
constexpr NSTimeInterval kEnableActionButtonsDelay = 0.5;

// Returns the width and height of a single pixel in point.
CGFloat GetPixelLength() {
  return 1.0 / [UIScreen mainScreen].scale;
}

// Returns the width of the alert.
CGFloat GetAlertWidth() {
  BOOL is_a11y_content_size = UIContentSizeCategoryIsAccessibilityCategory(
      [UIApplication sharedApplication].preferredContentSizeCategory);
  return is_a11y_content_size ? kAlertWidthAccessibility : kAlertWidth;
}

// Positions the content view on the screen.
void PositionContentViewInParentView(UIView* contentView, UIView* parentView) {
  [NSLayoutConstraint activateConstraints:@[
    [contentView.centerXAnchor
        constraintEqualToAnchor:parentView.safeAreaLayoutGuide.centerXAnchor],
    [contentView.centerYAnchor
        constraintEqualToAnchor:parentView.safeAreaLayoutGuide.centerYAnchor],

    // Minimum Size.
    [contentView.heightAnchor
        constraintGreaterThanOrEqualToConstant:kMinimumHeight],

    // Maximum Size.
    [contentView.topAnchor
        constraintGreaterThanOrEqualToAnchor:parentView.safeAreaLayoutGuide
                                                 .topAnchor
                                    constant:kMinimumMargin],
    [contentView.bottomAnchor
        constraintLessThanOrEqualToAnchor:parentView.safeAreaLayoutGuide
                                              .bottomAnchor
                                 constant:-kMinimumMargin],
    [contentView.trailingAnchor
        constraintLessThanOrEqualToAnchor:parentView.safeAreaLayoutGuide
                                              .trailingAnchor
                                 constant:-kMinimumMargin],
    [contentView.leadingAnchor
        constraintGreaterThanOrEqualToAnchor:parentView.safeAreaLayoutGuide
                                                 .leadingAnchor
                                    constant:kMinimumMargin],
  ]];
}

// Adds a grey line with a thickness of 1px to `stackView`, used to create a
// separator that visually separates different elements.
void AddSeparatorToStackView(UIStackView* stackView) {
  UIView* separator = [[UIView alloc] init];
  separator.backgroundColor = [UIColor colorNamed:kSeparatorColor];
  separator.translatesAutoresizingMaskIntoConstraints = NO;
  [stackView addArrangedSubview:separator];
  if (stackView.axis == UILayoutConstraintAxisHorizontal) {
    [separator.widthAnchor constraintEqualToConstant:GetPixelLength()].active =
        YES;
    AddSameConstraintsToSides(stackView, separator,
                              LayoutSides::kTop | LayoutSides::kBottom);
  } else {
    [separator.heightAnchor constraintEqualToConstant:GetPixelLength()].active =
        YES;
    AddSameConstraintsToSides(stackView, separator,
                              LayoutSides::kTrailing | LayoutSides::kLeading);
  }
}

// Returns a GrayHighlightButton to be added to the alert for `action`.
GrayHighlightButton* GetButtonForAction(AlertAction* action) {
  UIFont* font = nil;
  UIColor* textColor = nil;
  if (action.style == UIAlertActionStyleDefault) {
    font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
    textColor = [UIColor colorNamed:kBlueColor];
  } else if (action.style == UIAlertActionStyleCancel) {
    font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
    textColor = [UIColor colorNamed:kBlueColor];
  } else {  // Style is UIAlertActionStyleDestructive
    font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
    textColor = [UIColor colorNamed:kRedColor];
  }

  UIButtonConfiguration* buttonConfiguration =
      [UIButtonConfiguration plainButtonConfiguration];
  buttonConfiguration.contentInsets =
      NSDirectionalEdgeInsetsMake(kButtonInsetTop, kButtonInsetLeading,
                                  kButtonInsetBottom, kButtonInsetTrailing);
  NSDictionary* attributes = @{NSFontAttributeName : font};
  NSAttributedString* title =
      [[NSAttributedString alloc] initWithString:action.title
                                      attributes:attributes];
  buttonConfiguration.attributedTitle = title;
  buttonConfiguration.baseForegroundColor = textColor;
  GrayHighlightButton* button =
      [GrayHighlightButton buttonWithConfiguration:buttonConfiguration
                                     primaryAction:nil];

  button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter;
  button.translatesAutoresizingMaskIntoConstraints = NO;

  button.tag = action.uniqueIdentifier;
  return button;
}

}  // namespace

@interface AlertViewController () <UITextFieldDelegate,
                                   UIGestureRecognizerDelegate>

// The actions for to this alert. `copy` for safety against mutable objects.
@property(nonatomic, copy) NSArray<NSArray<AlertAction*>*>* actions;

// This maps UIButtons' tags with AlertActions' uniqueIdentifiers.
@property(nonatomic, strong)
    NSMutableDictionary<NSNumber*, AlertAction*>* buttonAlertActionsDictionary;

// This is the view with the shadow, white background and round corners.
// Everything will be added here.
@property(nonatomic, strong) UIView* contentView;

// The message of the alert, will appear after the title.
@property(nonatomic, copy) NSString* message;

// Text field configurations for this alert. One text field will be created for
// each `TextFieldConfiguration`. `copy` for safety against mutable objects.
@property(nonatomic, copy)
    NSArray<TextFieldConfiguration*>* textFieldConfigurations;

// The alert view's accessibility identifier.
@property(nonatomic, copy) NSString* alertAccessibilityIdentifier;

// The text fields that had been added to this alert.
@property(nonatomic, strong) NSArray<UITextField*>* textFields;

// Recognizer used to dismiss the keyboard when tapping outside the container
// view.
@property(nonatomic, strong) UITapGestureRecognizer* tapRecognizer;

// Recognizer used to dismiss the keyboard swipping down the alert.
@property(nonatomic, strong) UISwipeGestureRecognizer* swipeRecognizer;

// This is the last focused text field, the gestures to dismiss the keyboard
// will end up calling `resignFirstResponder` on this.
@property(nonatomic, weak) UITextField* lastFocusedTextField;

// This holds the text field stack view.
@property(nonatomic, strong) UIView* textFieldStackHolder;

// Whether the activity indicator should be visible in the alert view.
@property(nonatomic, assign) BOOL shouldShowActivityIndicator;

// Whether the action buttons should initially be disabled.
@property(nonatomic, assign) BOOL actionButtonsAreInitiallyDisabled;

@end

@implementation AlertViewController

#pragma mark - Public

- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
  [super traitCollectionDidChange:previousTraitCollection];

  if ([self.traitCollection
          hasDifferentColorAppearanceComparedToTraitCollection:
              previousTraitCollection]) {
    self.textFieldStackHolder.layer.borderColor =
        [UIColor colorNamed:kSeparatorColor].CGColor;
  }
}

- (void)loadView {
  [super loadView];
  self.view.backgroundColor = [UIColor colorNamed:kScrimBackgroundColor];
  self.view.accessibilityViewIsModal = YES;

  self.tapRecognizer = [[UITapGestureRecognizer alloc]
      initWithTarget:self
              action:@selector(dismissKeyboard)];
  self.tapRecognizer.enabled = NO;
  self.tapRecognizer.delegate = self;
  [self.view addGestureRecognizer:self.tapRecognizer];

  self.contentView = [[UIView alloc] init];
  self.contentView.accessibilityIdentifier = self.alertAccessibilityIdentifier;
  self.contentView.clipsToBounds = YES;
  self.contentView.backgroundColor =
      [UIColor colorNamed:kPrimaryBackgroundColor];
  self.contentView.layer.cornerRadius = kCornerRadius;
  self.contentView.layer.shadowOffset =
      CGSizeMake(kShadowOffsetX, kShadowOffsetY);
  self.contentView.layer.shadowRadius = kShadowRadius;
  self.contentView.layer.shadowOpacity = kShadowOpacity;
  self.contentView.translatesAutoresizingMaskIntoConstraints = NO;
  [self.view addSubview:self.contentView];

  self.swipeRecognizer = [[UISwipeGestureRecognizer alloc]
      initWithTarget:self
              action:@selector(dismissKeyboard)];
  self.swipeRecognizer.direction = UISwipeGestureRecognizerDirectionDown;
  self.swipeRecognizer.enabled = NO;
  [self.contentView addGestureRecognizer:self.swipeRecognizer];

  NSLayoutConstraint* widthConstraint =
      [self.contentView.widthAnchor constraintEqualToConstant:GetAlertWidth()];
  widthConstraint.priority = UILayoutPriorityRequired - 1;

  [[NSNotificationCenter defaultCenter]
      addObserverForName:UIContentSizeCategoryDidChangeNotification
                  object:nil
                   queue:[NSOperationQueue mainQueue]
              usingBlock:^(NSNotification* _Nonnull note) {
                widthConstraint.constant = GetAlertWidth();
              }];
  widthConstraint.active = YES;
  PositionContentViewInParentView(self.contentView, self.view);

  UIScrollView* scrollView = [[UIScrollView alloc] init];
  scrollView.delaysContentTouches = NO;
  scrollView.showsVerticalScrollIndicator = YES;
  scrollView.showsHorizontalScrollIndicator = NO;
  scrollView.translatesAutoresizingMaskIntoConstraints = NO;
  scrollView.scrollEnabled = YES;
  scrollView.contentInsetAdjustmentBehavior =
      UIScrollViewContentInsetAdjustmentAlways;
  [self.contentView addSubview:scrollView];
  AddSameConstraints(scrollView, self.contentView);

  UIStackView* stackView = [[UIStackView alloc] init];
  stackView.axis = UILayoutConstraintAxisVertical;
  stackView.translatesAutoresizingMaskIntoConstraints = NO;
  stackView.alignment = UIStackViewAlignmentCenter;
  [scrollView addSubview:stackView];

  NSLayoutConstraint* heightConstraint = [scrollView.heightAnchor
      constraintEqualToAnchor:scrollView.contentLayoutGuide.heightAnchor];
  // UILayoutPriorityDefaultHigh is the default priority for content
  // compression. Setting this lower avoids compressing the content of the
  // scroll view.
  heightConstraint.priority = UILayoutPriorityDefaultHigh - 1;
  heightConstraint.active = YES;

  NSDirectionalEdgeInsets stackViewInsets =
      NSDirectionalEdgeInsetsMake(kAlertMarginTop, 0, 0, 0);
  AddSameConstraintsWithInsets(stackView, scrollView, stackViewInsets);

  if (self.title.length) {
    UILabel* titleLabel = [[UILabel alloc] init];
    titleLabel.numberOfLines = 0;
    titleLabel.font =
        [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
    titleLabel.adjustsFontForContentSizeCategory = YES;
    titleLabel.textAlignment = NSTextAlignmentCenter;
    titleLabel.text = self.title;
    titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
    [stackView addArrangedSubview:titleLabel];
    [stackView setCustomSpacing:self.shouldShowActivityIndicator
                                    ? kTitleInsetBottom + kSpinnerInsetTop
                                    : kTitleInsetBottom
                      afterView:titleLabel];

    NSDirectionalEdgeInsets titleInsets = NSDirectionalEdgeInsetsMake(
        0, kTitleInsetLeading, 0, kTitleInsetTrailing);
    AddSameConstraintsToSidesWithInsets(
        titleLabel, self.contentView,
        LayoutSides::kTrailing | LayoutSides::kLeading, titleInsets);
  }

  if (self.shouldShowActivityIndicator) {
    UIActivityIndicatorView* spinner = GetLargeUIActivityIndicatorView();
    [spinner startAnimating];
    [stackView addArrangedSubview:spinner];
    [stackView setCustomSpacing:kSpinnerInsetBottom afterView:spinner];
  }

  if (self.message.length) {
    UILabel* messageLabel = [[UILabel alloc] init];
    messageLabel.numberOfLines = 0;
    messageLabel.font =
        [UIFont preferredFontForTextStyle:UIFontTextStyleFootnote];
    messageLabel.adjustsFontForContentSizeCategory = YES;
    messageLabel.textAlignment = NSTextAlignmentCenter;
    messageLabel.text = self.message;
    messageLabel.translatesAutoresizingMaskIntoConstraints = NO;
    [stackView addArrangedSubview:messageLabel];
    [stackView setCustomSpacing:kMessageInsetBottom afterView:messageLabel];

    NSDirectionalEdgeInsets messageInsets = NSDirectionalEdgeInsetsMake(
        0, kMessageInsetLeading, 0, kMessageInsetTrailing);
    AddSameConstraintsToSidesWithInsets(
        messageLabel, self.contentView,
        LayoutSides::kTrailing | LayoutSides::kLeading, messageInsets);
  }

  if (self.textFieldConfigurations.count) {
    // Updates the custom space before the text fields to account for their
    // inset.
    UIView* previousView = stackView.arrangedSubviews.lastObject;
    if (previousView) {
      CGFloat spaceBefore = [stackView customSpacingAfterView:previousView];
      [stackView setCustomSpacing:kTextfieldStackInsetTop + spaceBefore
                        afterView:previousView];
    }
    [stackView addArrangedSubview:self.textFieldStackHolder];
    NSDirectionalEdgeInsets stackHolderContentInsets =
        NSDirectionalEdgeInsetsMake(0, kTextfieldStackInsetLeading, 0,
                                    kTextfieldStackInsetTrailing);
    AddSameConstraintsToSidesWithInsets(
        self.textFieldStackHolder, self.contentView,
        LayoutSides::kTrailing | LayoutSides::kLeading,
        stackHolderContentInsets);
  }

  UIView* lastArrangedView = stackView.arrangedSubviews.lastObject;
  if (lastArrangedView) {
    [stackView setCustomSpacing:kAlertActionsSpacing
                      afterView:lastArrangedView];
  }

  if ([self.actions count] > 0) {
    UIStackView* buttonStackView = [self createButtonStackView];
    [stackView addArrangedSubview:buttonStackView];
    AddSameConstraintsToSides(buttonStackView, self.contentView,
                              LayoutSides::kTrailing | LayoutSides::kLeading);
  }

  [[NSNotificationCenter defaultCenter]
      addObserver:self
         selector:@selector(handleKeyboardWillShow:)
             name:UIKeyboardWillShowNotification
           object:nil];

  [[NSNotificationCenter defaultCenter]
      addObserver:self
         selector:@selector(handleKeyboardWillHide:)
             name:UIKeyboardWillHideNotification
           object:nil];
}

#pragma mark - Getters

- (NSArray<UITextField*>*)textFields {
  if (!_textFields) {
    NSMutableArray<UITextField*>* textFields = [[NSMutableArray alloc]
        initWithCapacity:self.textFieldConfigurations.count];
    for (TextFieldConfiguration* textFieldConfiguration in self
             .textFieldConfigurations) {
      UITextField* textField = [[UITextField alloc] init];
      textField.text = textFieldConfiguration.text;
      textField.placeholder = textFieldConfiguration.placeholder;
      textField.autocapitalizationType =
          textFieldConfiguration.autocapitalizationType;
      textField.secureTextEntry = textFieldConfiguration.secureTextEntry;
      textField.accessibilityIdentifier =
          textFieldConfiguration.accessibilityIdentifier;
      textField.translatesAutoresizingMaskIntoConstraints = NO;
      textField.delegate = self;
      textField.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
      textField.adjustsFontForContentSizeCategory = YES;
      [textFields addObject:textField];
    }
    _textFields = textFields;
  }
  return _textFields;
}

- (NSArray<NSString*>*)textFieldResults {
  if (!self.textFields) {
    return nil;
  }
  NSMutableArray<NSString*>* results =
      [[NSMutableArray alloc] initWithCapacity:self.textFields.count];
  for (UITextField* textField in self.textFields) {
    [results addObject:textField.text];
  }
  return results;
}

- (UIView*)textFieldStackHolder {
  if (!_textFieldStackHolder) {
    // `stackHolder` has the background, border and round corners of the stacked
    // fields.
    _textFieldStackHolder = [[UIView alloc] init];
    _textFieldStackHolder.layer.cornerRadius = kTextFieldCornerRadius;
    _textFieldStackHolder.layer.borderColor =
        [UIColor colorNamed:kSeparatorColor].CGColor;
    // Use performAsCurrentTraitCollection to get the correct CGColor for the
    // given dynamic color and current userInterfaceStyle.
    [self.traitCollection performAsCurrentTraitCollection:^{
      _textFieldStackHolder.layer.borderColor =
          [UIColor colorNamed:kSeparatorColor].CGColor;
    }];
    _textFieldStackHolder.layer.borderWidth = GetPixelLength();
    _textFieldStackHolder.clipsToBounds = YES;
    _textFieldStackHolder.backgroundColor =
        [UIColor colorNamed:kSecondaryBackgroundColor];
    _textFieldStackHolder.translatesAutoresizingMaskIntoConstraints = NO;

    // Add text field configurations.
    UIStackView* fieldStack = [[UIStackView alloc] init];
    fieldStack.axis = UILayoutConstraintAxisVertical;
    fieldStack.translatesAutoresizingMaskIntoConstraints = NO;
    fieldStack.spacing = kTextfieldInset;
    fieldStack.alignment = UIStackViewAlignmentCenter;
    [_textFieldStackHolder addSubview:fieldStack];
    NSDirectionalEdgeInsets fieldStackContentInsets =
        NSDirectionalEdgeInsetsMake(kTextfieldInset, 0.0, kTextfieldInset, 0.0);
    AddSameConstraintsWithInsets(fieldStack, _textFieldStackHolder,
                                 fieldStackContentInsets);
    for (UITextField* textField in self.textFields) {
      if (textField != [self.textFields firstObject]) {
        AddSeparatorToStackView(fieldStack);
      }
      [fieldStack addArrangedSubview:textField];
      NSDirectionalEdgeInsets fieldInsets = NSDirectionalEdgeInsetsMake(
          0.0, kTextfieldInset, 0.0, kTextfieldInset);
      AddSameConstraintsToSidesWithInsets(
          textField, fieldStack, LayoutSides::kTrailing | LayoutSides::kLeading,
          fieldInsets);
    }
  }
  return _textFieldStackHolder;
}

- (NSDictionary<NSNumber*, AlertAction*>*)buttonAlertActionsDictionary {
  if (!_buttonAlertActionsDictionary) {
    NSMutableDictionary<NSNumber*, AlertAction*>* buttonAlertActionsDictionary =
        [[NSMutableDictionary alloc] init];
    for (NSArray<AlertAction*>* rowOfActions in self.actions) {
      for (AlertAction* action in rowOfActions) {
        buttonAlertActionsDictionary[@(action.uniqueIdentifier)] = action;
      }
    }
    _buttonAlertActionsDictionary = buttonAlertActionsDictionary;
  }
  return _buttonAlertActionsDictionary;
}

#pragma mark - UIGestureRecognizerDelegate

- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
       shouldReceiveTouch:(UITouch*)touch {
  if (self.tapRecognizer != gestureRecognizer) {
    return YES;
  }
  CGPoint locationInContentView = [touch locationInView:self.contentView];
  return !CGRectContainsPoint(self.contentView.bounds, locationInContentView);
}

#pragma mark - UITextFieldDelegate

- (void)textFieldDidBeginEditing:(UITextField*)textField {
  self.lastFocusedTextField = textField;
}

- (BOOL)textFieldShouldReturn:(UITextField*)textField {
  NSUInteger index = [self.textFields indexOfObject:textField];
  if (index + 1 < self.textFields.count) {
    [self.textFields[index + 1] becomeFirstResponder];
  } else {
    [textField resignFirstResponder];
  }
  return NO;
}

#pragma mark - Private

// Displays the keyboard.
- (void)handleKeyboardWillShow:(NSNotification*)notification {
  CGRect keyboardFrame =
      [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
  CGRect viewFrameInWindow = [self.view convertRect:self.view.bounds
                                             toView:nil];
  CGRect intersectedFrame =
      CGRectIntersection(keyboardFrame, viewFrameInWindow);

  CGFloat additionalBottomInset =
      intersectedFrame.size.height - self.view.safeAreaInsets.bottom;

  if (additionalBottomInset > 0) {
    self.additionalSafeAreaInsets =
        UIEdgeInsetsMake(0, 0, additionalBottomInset, 0);
    [self animateLayoutFromKeyboardNotification:notification];
  }

  self.tapRecognizer.enabled = YES;
  self.swipeRecognizer.enabled = YES;
}

// Hides the keyboard.
- (void)handleKeyboardWillHide:(NSNotification*)notification {
  self.additionalSafeAreaInsets = UIEdgeInsetsZero;
  [self animateLayoutFromKeyboardNotification:notification];

  self.tapRecognizer.enabled = NO;
  self.swipeRecognizer.enabled = NO;
}

// Helper method that displays the keyboard with an animation.
- (void)animateLayoutFromKeyboardNotification:(NSNotification*)notification {
  double duration =
      [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey]
          doubleValue];
  UIViewAnimationCurve animationCurve = static_cast<UIViewAnimationCurve>(
      [notification.userInfo[UIKeyboardAnimationCurveUserInfoKey]
          integerValue]);
  UIViewAnimationOptions options = animationCurve
                                   << kUIViewAnimationCurveToOptionsShift;

  [UIView animateWithDuration:duration
                        delay:0
                      options:options
                   animations:^{
                     [self.view layoutIfNeeded];
                   }
                   completion:nil];
}

// Returns a stack of formatted buttons to be added to the bottom of the alert.
- (UIStackView*)createButtonStackView {
  UIStackView* buttons = [[UIStackView alloc] init];
  buttons.axis = UILayoutConstraintAxisVertical;
  buttons.translatesAutoresizingMaskIntoConstraints = NO;
  buttons.alignment = UIStackViewAlignmentCenter;
  for (NSArray<AlertAction*>* rowOfActions in self.actions) {
    DCHECK_GT([rowOfActions count], 0U);
    AddSeparatorToStackView(buttons);
    // Calculate the axis for the sub-stackview.
    CGFloat maxWidth = 0;
    NSMutableArray<GrayHighlightButton*>* rowOfButtons =
        [[NSMutableArray alloc] init];
    for (AlertAction* action in rowOfActions) {
      GrayHighlightButton* button = GetButtonForAction(action);
      if (self.actionButtonsAreInitiallyDisabled) {
        button.enabled = NO;
        [self performSelector:@selector(enableActionButton:)
                   withObject:button
                   afterDelay:kEnableActionButtonsDelay];
      }
      [button addTarget:self
                    action:@selector(didSelectActionForButton:)
          forControlEvents:UIControlEventTouchUpInside];
      [rowOfButtons addObject:button];
      maxWidth = MAX(maxWidth, button.intrinsicContentSize.width);
    }
    UILayoutConstraintAxis axis =
        maxWidth > GetAlertWidth() / rowOfActions.count
            ? UILayoutConstraintAxisVertical
            : UILayoutConstraintAxisHorizontal;
    // Actually creates and adds the stack view to the view, and position the
    // buttons.
    UIStackView* rowOfButtonStackView = [[UIStackView alloc] init];
    rowOfButtonStackView.axis = axis;
    rowOfButtonStackView.alignment = UIStackViewAlignmentCenter;
    GrayHighlightButton* firstButton = [rowOfButtons firstObject];
    GrayHighlightButton* lastButton = [rowOfButtons lastObject];
    for (GrayHighlightButton* button in rowOfButtons) {
      [rowOfButtonStackView addArrangedSubview:button];
      if (button != lastButton) {
        AddSeparatorToStackView(rowOfButtonStackView);
      }
      if (axis == UILayoutConstraintAxisHorizontal) {
        [button.widthAnchor constraintEqualToAnchor:firstButton.widthAnchor]
            .active = YES;
        AddSameConstraintsToSides(button, rowOfButtonStackView,
                                  (LayoutSides::kTop | LayoutSides::kBottom));
      } else {
        AddSameConstraintsToSides(
            button, rowOfButtonStackView,
            (LayoutSides::kTrailing | LayoutSides::kLeading));
      }
    }
    [buttons addArrangedSubview:rowOfButtonStackView];
    AddSameConstraintsToSides(rowOfButtonStackView, buttons,
                              (LayoutSides::kTrailing | LayoutSides::kLeading));
  }
  return buttons;
}

// React to user taps on `button`.
- (void)didSelectActionForButton:(UIButton*)button {
  AlertAction* action = self.buttonAlertActionsDictionary[@(button.tag)];
  if (action.handler) {
    action.handler(action);
  }
}

// Dismiss the keyboard, if visible.
- (void)dismissKeyboard {
  [self.lastFocusedTextField resignFirstResponder];
}

// Enables `button`.
- (void)enableActionButton:(UIButton*)actionButton {
  actionButton.enabled = YES;
}

@end