// Copyright 2024 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/autofill/ui_bundled/manual_fill/expanded_manual_fill_view_controller.h"
#import "base/metrics/user_metrics.h"
#import "ios/chrome/browser/autofill/ui_bundled/manual_fill/fallback_view_controller.h"
#import "ios/chrome/browser/autofill/ui_bundled/manual_fill/manual_fill_constants.h"
#import "ios/chrome/browser/shared/ui/elements/extended_touch_target_button.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#import "ios/chrome/grit/ios_branded_strings.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util.h"
using manual_fill::ManualFillDataType;
namespace {
// Size of the Chrome logo.
constexpr CGFloat kChromeLogoSize = 24;
// Size of the close button.
constexpr CGFloat kCloseButtonSize = 30;
// Size of the data type icons representing the different segments
// of the segmented control.
constexpr CGFloat kDataTypeIconSize = 18;
// Bottom padding for the header view.
constexpr CGFloat kHeaderViewBottomPadding = 12;
// Leading and trailing padding for the header view.
constexpr CGFloat kHeaderViewHorizontalPadding = 16;
// Top padding for the header view.
constexpr CGFloat kHeaderViewTopPadding = 8;
// Height of the segmented control.
constexpr CGFloat kSegmentedControlHeight = 32;
// Multiplier used to constraint the view's height.
constexpr CGFloat kViewHeightMultiplier = 0.6;
// Height of the header's top view. Used for the narrow layout only.
constexpr CGFloat kHeaderTopViewHeightNarrowLayout = 44;
// Vertical spacing between the bottom of the header top view and segmented
// control. Used for the narrow layout only.
constexpr CGFloat kHeaderTopViewBottomSpacingNarrowLayout = 4;
// Height of the header view. Used for the wide layout only.
constexpr CGFloat kHeaderViewHeightWideLayout = 44;
// Horizontal spacing between the Chrome logo and segmented control. Used for
// the wide layout only.
constexpr CGFloat kSegmentedControlLeadingSpacingWideLayout = 18;
// Horizontal spacing between the segmented control and close button. Used for
// the wide layout only.
constexpr CGFloat kSegmentedControlTrailingSpacingWideLayout = 15;
// Helper method to get the right segment index depending on the `data_type`.
int GetSegmentIndexForDataType(ManualFillDataType data_type) {
switch (data_type) {
case ManualFillDataType::kPassword:
return 0;
case ManualFillDataType::kPaymentMethod:
return 1;
case ManualFillDataType::kAddress:
return 2;
case ManualFillDataType::kOther:
NOTREACHED();
}
}
} // namespace
@interface ExpandedManualFillViewController ()
// Delegate to handle user interactions.
@property(nonatomic, weak) id<ExpandedManualFillViewControllerDelegate>
delegate;
// Control allowing switching between the different data types. Not an ivar so
// that it can be used in tests.
@property(nonatomic, strong) UISegmentedControl* segmentedControl;
@end
@implementation ExpandedManualFillViewController {
// Header view presented at the top of this view controller's view. Contains
// the Chrome logo, close button and segmented control. The
// positiong of these elements depends on the device's orientation:
// - Narrow layout: When in iPhone portrait mode. Chrome logo and close
// button are aligned horizontally above the segmented control.
// - Wide layout: When in iPhone landscape mode. Chrome logo, segmented
// control and close button are all aligned horizontally.
UIView* _headerView;
// View positioned at the top the of the header view when in narrow layout.
// Contains the Chrome logo and close button.
UIView* _headerTopView;
// Header view's height constraint. Used for the wide layout only.
NSLayoutConstraint* _headerViewHeightConstraint;
// Header view's leading constraint.
NSLayoutConstraint* _headerViewLeadingConstraint;
// Header view's trailing constraint.
NSLayoutConstraint* _headerViewTrailingConstraint;
// Image view containing the Chrome logo.
UIImageView* _chromeLogo;
// Button to close the view.
ExtendedTouchTargetButton* _closeButton;
// Initial data type to present in the view. Reflects the type of the form the
// user wants to fill.
ManualFillDataType _initialDataType;
// The leading and trailing inset of the child view controller's table view
// cells. Used to constraint the leading and trailing sides of the header view
// so that they horizontally align with the cells.
CGFloat _tableViewCellHorizontalInset;
}
- (instancetype)initWithDelegate:
(id<ExpandedManualFillViewControllerDelegate>)delegate
forDataType:(ManualFillDataType)dataType {
self = [super initWithNibName:nil bundle:nil];
if (self) {
_delegate = delegate;
_initialDataType = dataType;
}
return self;
}
#pragma mark - UIViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.accessibilityIdentifier = manual_fill::kExpandedManualFillViewID;
self.view.backgroundColor =
[UIColor colorNamed:kGroupedPrimaryBackgroundColor];
// Set the view's frame to get the right height initially. Once the view's
// window is loaded in `viewDidAppear`, the view's height will be dynamically
// constraint to its window's height instead.
self.view.autoresizingMask = UIViewAutoresizingNone;
self.view.frame = CGRectMake(
0, 0, 0, UIScreen.mainScreen.bounds.size.height * kViewHeightMultiplier);
_headerView = [self createHeaderView];
_headerTopView = [self createHeaderTopView];
_chromeLogo = [self createChromeLogo];
_closeButton = [self createCloseButton];
_segmentedControl =
[self createSegmentedControlAndSelectDataType:_initialDataType];
_headerViewHeightConstraint = [_headerView.heightAnchor
constraintEqualToConstant:kHeaderViewHeightWideLayout];
[self setUpHeaderView:_headerView
headerViewHeightConstraint:_headerViewHeightConstraint
chromeLogo:_chromeLogo
closeButton:_closeButton
segmentedControl:_segmentedControl
headerTopView:_headerTopView];
[self.view addSubview:_headerView];
// `_headerView` constraints.
_headerViewLeadingConstraint = [_headerView.leadingAnchor
constraintEqualToAnchor:self.view.safeAreaLayoutGuide.leadingAnchor
constant:kHeaderViewHorizontalPadding];
_headerViewTrailingConstraint = [_headerView.trailingAnchor
constraintEqualToAnchor:self.view.safeAreaLayoutGuide.trailingAnchor
constant:-kHeaderViewHorizontalPadding];
[NSLayoutConstraint activateConstraints:@[
[_headerView.topAnchor constraintEqualToAnchor:self.view.topAnchor
constant:kHeaderViewTopPadding],
_headerViewLeadingConstraint,
_headerViewTrailingConstraint,
]];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// Anchor the view's height to its window's height so that the view's height
// resizes dynamically when switching between portrait and landscape modes.
self.view.autoresizingMask = UIViewAutoresizingFlexibleHeight;
[NSLayoutConstraint activateConstraints:@[
[self.view.heightAnchor
constraintEqualToAnchor:self.view.window.heightAnchor
multiplier:kViewHeightMultiplier],
]];
// Bring focus to the expanded view by focusing on the Chrome logo.
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification,
_chromeLogo);
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
UITableView* tableView = self.childViewController.tableView;
UITableViewStyle style = tableView.style;
CGFloat tableViewCellHorizontalInset =
tableView.visibleCells.firstObject.layoutMargins.left;
// If needed, update the horizontal contraints of the header view so that it
// is horizontally aligned with the table view cells.
if (style == UITableViewStyleInsetGrouped && tableViewCellHorizontalInset &&
_tableViewCellHorizontalInset != tableViewCellHorizontalInset) {
_tableViewCellHorizontalInset = tableViewCellHorizontalInset;
[self updateHeaderViewHorizontalConstraints:_headerViewLeadingConstraint
trailingConstraint:_headerViewTrailingConstraint
constant:_tableViewCellHorizontalInset];
}
}
#pragma mark - UITraitEnvironment
- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
if (self.traitCollection.verticalSizeClass !=
previousTraitCollection.verticalSizeClass) {
// Update the header view's layout when the view's vertical size class
// changes.
[self resetHeaderView:_headerView
headerViewHeightConstraint:_headerViewHeightConstraint
chromeLogo:_chromeLogo
closeButton:_closeButton
segmentedControl:_segmentedControl
headerTopView:_headerTopView];
}
}
#pragma mark - Setters
- (void)setChildViewController:(FallbackViewController*)childViewController {
if (_childViewController == childViewController) {
return;
}
// Remove the previous child view controller.
[_childViewController willMoveToParentViewController:nil];
[_childViewController.view removeFromSuperview];
[_childViewController removeFromParentViewController];
_childViewController = childViewController;
_childViewController.view.translatesAutoresizingMaskIntoConstraints = NO;
[_childViewController willMoveToParentViewController:self];
[self addChildViewController:_childViewController];
[self.view addSubview:self.childViewController.view];
[_childViewController didMoveToParentViewController:self];
// `_childViewController.view` constraints.
[_childViewController.view.topAnchor
constraintEqualToAnchor:_headerView.bottomAnchor
constant:kHeaderViewBottomPadding]
.active = YES;
AddSameConstraintsToSides(
_childViewController.view, self.view,
LayoutSides::kBottom | LayoutSides::kTrailing | LayoutSides::kLeading);
}
#pragma mark - Private
// Creates and configures the header view.
- (UIView*)createHeaderView {
UIView* headerView = [[UIView alloc] init];
headerView.translatesAutoresizingMaskIntoConstraints = NO;
headerView.accessibilityIdentifier =
manual_fill::kExpandedManualFillHeaderViewID;
return headerView;
}
// Creates and configures the header's top view.
- (UIView*)createHeaderTopView {
UIView* headerTopView = [[UIView alloc] init];
headerTopView.translatesAutoresizingMaskIntoConstraints = NO;
headerTopView.accessibilityIdentifier =
manual_fill::kExpandedManualFillHeaderTopViewID;
return headerTopView;
}
// Creates and configures the Chrome logo.
- (UIImageView*)createChromeLogo {
#if BUILDFLAG(IOS_USE_BRANDED_SYMBOLS)
UIImage* image = MakeSymbolMulticolor(
CustomSymbolWithPointSize(kMulticolorChromeballSymbol, kChromeLogoSize));
#else
UIImage* image =
CustomSymbolWithPointSize(kChromeProductSymbol, kChromeLogoSize);
#endif // BUILDFLAG(IOS_USE_BRANDED_SYMBOLS)
UIImageView* chromeLogo = [[UIImageView alloc] initWithImage:image];
chromeLogo.translatesAutoresizingMaskIntoConstraints = NO;
chromeLogo.contentMode = UIViewContentModeCenter;
chromeLogo.isAccessibilityElement = YES;
chromeLogo.accessibilityLabel = l10n_util::GetNSString(
IDS_IOS_EXPANDED_MANUAL_FILL_VIEW_ACCESSIBILITY_ANNOUNCEMENT);
chromeLogo.accessibilityTraits = UIAccessibilityTraitNone;
chromeLogo.accessibilityIdentifier =
manual_fill::kExpandedManualFillChromeLogoID;
[chromeLogo setContentHuggingPriority:UILayoutPriorityRequired
forAxis:UILayoutConstraintAxisHorizontal];
[chromeLogo
setContentCompressionResistancePriority:UILayoutPriorityRequired
forAxis:UILayoutConstraintAxisHorizontal];
return chromeLogo;
}
// Creates and configures the close button.
- (ExtendedTouchTargetButton*)createCloseButton {
ExtendedTouchTargetButton* closeButton =
[ExtendedTouchTargetButton buttonWithType:UIButtonTypeSystem];
closeButton.translatesAutoresizingMaskIntoConstraints = NO;
closeButton.contentMode = UIViewContentModeCenter;
closeButton.accessibilityLabel = l10n_util::GetNSString(
IDS_IOS_EXPANDED_MANUAL_FILL_CLOSE_BUTTON_ACCESSIBILITY_LABEL);
UIImageSymbolConfiguration* symbolConfiguration = [UIImageSymbolConfiguration
configurationWithPointSize:kCloseButtonSize
weight:UIImageSymbolWeightRegular
scale:UIImageSymbolScaleMedium];
UIImage* buttonImage = SymbolWithPalette(
DefaultSymbolWithConfiguration(kXMarkCircleFillSymbol,
symbolConfiguration),
@[
[[UIColor secondaryLabelColor] colorWithAlphaComponent:0.6],
[UIColor tertiarySystemFillColor]
]);
[closeButton setImage:buttonImage forState:UIControlStateNormal];
[closeButton setContentHuggingPriority:UILayoutPriorityRequired
forAxis:UILayoutConstraintAxisHorizontal];
[closeButton
setContentCompressionResistancePriority:UILayoutPriorityRequired
forAxis:UILayoutConstraintAxisHorizontal];
[closeButton addTarget:self
action:@selector(onCloseButtonPressed:)
forControlEvents:UIControlEventTouchUpInside];
return closeButton;
}
// Creates and configures the segmented control. `dataType` indicates which
// segment to select.
- (UISegmentedControl*)createSegmentedControlAndSelectDataType:
(ManualFillDataType)dataType {
UIImageSymbolConfiguration* symbolConfiguration = [UIImageSymbolConfiguration
configurationWithPointSize:kDataTypeIconSize
weight:UIImageSymbolWeightRegular
scale:UIImageSymbolScaleMedium];
UIImage* passwordIcon =
CustomSymbolWithConfiguration(kPasswordSymbol, symbolConfiguration);
passwordIcon.accessibilityLabel = l10n_util::GetNSString(
IDS_IOS_EXPANDED_MANUAL_FILL_PASSWORD_TAB_ACCESSIBILITY_LABEL);
UIImage* cardIcon =
DefaultSymbolWithConfiguration(kCreditCardSymbol, symbolConfiguration);
cardIcon.accessibilityLabel = l10n_util::GetNSString(
IDS_IOS_EXPANDED_MANUAL_FILL_PAYMENT_TAB_ACCESSIBILITY_LABEL);
UIImage* addressIcon =
CustomSymbolWithConfiguration(kLocationSymbol, symbolConfiguration);
addressIcon.accessibilityLabel = l10n_util::GetNSString(
IDS_IOS_EXPANDED_MANUAL_FILL_ADDRESS_TAB_ACCESSIBILITY_LABEL);
UISegmentedControl* segmentedControl = [[UISegmentedControl alloc]
initWithItems:@[ passwordIcon, cardIcon, addressIcon ]];
segmentedControl.translatesAutoresizingMaskIntoConstraints = NO;
segmentedControl.selectedSegmentIndex = GetSegmentIndexForDataType(dataType);
[segmentedControl addTarget:self
action:@selector(onSegmentSelected:)
forControlEvents:UIControlEventValueChanged];
return segmentedControl;
}
// Sets up the header view depending on the device's orientation.
- (void)setUpHeaderView:(UIView*)headerView
headerViewHeightConstraint:(NSLayoutConstraint*)headerViewHeightConstraint
chromeLogo:(UIImageView*)chromeLogo
closeButton:(UIButton*)closeButton
segmentedControl:(UISegmentedControl*)segmentedControl
headerTopView:(UIView*)headerTopView {
// If the vertical size class is compact, apply the wide layout. Otherwise,
// apply the narrow layout.
if (IsCompactHeight(self)) {
[headerView addSubview:chromeLogo];
[headerView addSubview:closeButton];
[headerView addSubview:segmentedControl];
headerViewHeightConstraint.active = YES;
[NSLayoutConstraint activateConstraints:@[
// `chromeLogo` constraints.
[chromeLogo.centerYAnchor
constraintEqualToAnchor:headerView.centerYAnchor],
[chromeLogo.leadingAnchor
constraintEqualToAnchor:headerView.leadingAnchor],
// `closeButton` constraints.
[closeButton.centerYAnchor
constraintEqualToAnchor:headerView.centerYAnchor],
[closeButton.trailingAnchor
constraintEqualToAnchor:headerView.trailingAnchor],
// `segmentedControl` constraints.
[segmentedControl.centerYAnchor
constraintEqualToAnchor:headerView.centerYAnchor],
[segmentedControl.leadingAnchor
constraintEqualToAnchor:chromeLogo.trailingAnchor
constant:kSegmentedControlLeadingSpacingWideLayout],
[segmentedControl.trailingAnchor
constraintEqualToAnchor:closeButton.leadingAnchor
constant:-kSegmentedControlTrailingSpacingWideLayout],
]];
} else {
[headerView addSubview:headerTopView];
[headerView addSubview:segmentedControl];
[headerTopView addSubview:chromeLogo];
[headerTopView addSubview:closeButton];
headerViewHeightConstraint.active = NO;
[NSLayoutConstraint activateConstraints:@[
// `chromeLogo` constraints.
[chromeLogo.centerYAnchor
constraintEqualToAnchor:headerTopView.centerYAnchor],
[chromeLogo.centerXAnchor
constraintEqualToAnchor:headerTopView.centerXAnchor],
// `closeButton` constraints.
[closeButton.centerYAnchor
constraintEqualToAnchor:headerTopView.centerYAnchor],
[closeButton.trailingAnchor
constraintEqualToAnchor:headerTopView.trailingAnchor],
// `headerTopView` constraints.
[headerTopView.topAnchor constraintEqualToAnchor:headerView.topAnchor],
[headerTopView.trailingAnchor
constraintEqualToAnchor:headerView.trailingAnchor],
[headerTopView.leadingAnchor
constraintEqualToAnchor:headerView.leadingAnchor],
[headerTopView.heightAnchor
constraintEqualToConstant:kHeaderTopViewHeightNarrowLayout],
// `segmentedControl` constraints.
[segmentedControl.topAnchor
constraintEqualToAnchor:headerTopView.bottomAnchor
constant:kHeaderTopViewBottomSpacingNarrowLayout],
[segmentedControl.bottomAnchor
constraintEqualToAnchor:headerView.bottomAnchor],
[segmentedControl.trailingAnchor
constraintEqualToAnchor:headerView.trailingAnchor],
[segmentedControl.leadingAnchor
constraintEqualToAnchor:headerView.leadingAnchor],
]];
}
// Constraints that are common to both layouts.
[NSLayoutConstraint activateConstraints:@[
// `segmentedControl` constraints.
[segmentedControl.heightAnchor
constraintEqualToConstant:kSegmentedControlHeight],
]];
}
// Resets the header view. Called when a layout change is needed.
- (void)resetHeaderView:(UIView*)headerView
headerViewHeightConstraint:(NSLayoutConstraint*)headerViewHeightConstraint
chromeLogo:(UIImageView*)chromeLogo
closeButton:(UIButton*)closeButton
segmentedControl:(UISegmentedControl*)segmentedControl
headerTopView:(UIView*)headerTopView {
// Remove subviews to reset their constraints.
[chromeLogo removeFromSuperview];
[closeButton removeFromSuperview];
[segmentedControl removeFromSuperview];
[headerTopView removeFromSuperview];
[self setUpHeaderView:headerView
headerViewHeightConstraint:headerViewHeightConstraint
chromeLogo:chromeLogo
closeButton:closeButton
segmentedControl:segmentedControl
headerTopView:headerTopView];
}
// Updates the horizontal constraints of the header view with the given
// `constant`.
- (void)updateHeaderViewHorizontalConstraints:
(NSLayoutConstraint*)leadingConstraint
trailingConstraint:
(NSLayoutConstraint*)trailingConstraint
constant:(CGFloat)constant {
leadingConstraint.constant = constant;
trailingConstraint.constant = -constant;
}
// Handles taps on the close button.
- (void)onCloseButtonPressed:(id)sender {
base::RecordAction(base::UserMetricsAction("ManualFallback_Close"));
[self.delegate expandedManualFillViewController:self
didPressCloseButton:sender];
}
// Handles the selection of a different data type from the segmented control.
- (void)onSegmentSelected:(UISegmentedControl*)segmentedControl {
ManualFillDataType selectedType =
static_cast<ManualFillDataType>(segmentedControl.selectedSegmentIndex);
[self.delegate expandedManualFillViewController:self
didSelectSegmentOfType:selectedType];
}
@end