// Copyright 2021 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/shared/ui/elements/instruction_view.h"
#import "base/check.h"
#import "ios/chrome/browser/shared/ui/elements/elements_constants.h"
#import "ios/chrome/common/string_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/common/ui/util/dynamic_type_util.h"
#import "ios/chrome/common/ui/util/ui_util.h"
namespace {
constexpr CGFloat kStepNumberLabelSize = 20;
constexpr CGFloat kLeadingMargin = 15;
constexpr CGFloat kSpacing = 14;
constexpr CGFloat kVerticalMargin = 9;
constexpr CGFloat kTrailingMargin = 16;
constexpr CGFloat kCornerRadius = 12;
constexpr CGFloat kSeparatorLeadingMargin = 60;
constexpr CGFloat kSeparatorHeight = 0.5;
constexpr CGFloat kIconLabelWidth = 30;
// Height minimum for a line.
constexpr CGFloat kMinimumLineHeight = 44;
// Creates a view with `icon` in it.
UIView* CreateIconView(UIImage* icon) {
UIImageView* icon_image_view = [[UIImageView alloc] initWithImage:icon];
icon_image_view.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:@[
[icon_image_view.widthAnchor constraintEqualToConstant:kIconLabelWidth],
[icon_image_view.heightAnchor constraintEqualToConstant:kIconLabelWidth],
]];
return icon_image_view;
}
} // namespace
@interface InstructionView ()
// The style of the instruction view.
@property(nonatomic, assign) InstructionViewStyle style;
// A list of step number labels for color reset on trait collection change.
@property(nonatomic, strong) NSMutableArray<UILabel*>* stepNumberLabels;
@end
@implementation InstructionView
#pragma mark - Public
- (instancetype)initWithList:(NSArray<NSString*>*)instructionList
style:(InstructionViewStyle)style
iconViews:(NSArray<UIView*>*)iconViews {
self = [super initWithFrame:CGRectZero];
if (self) {
BOOL useIcon = iconViews != nil;
if (useIcon) {
DCHECK(iconViews.count == instructionList.count);
}
_style = style;
_stepNumberLabels =
[[NSMutableArray alloc] initWithCapacity:instructionList.count];
UIStackView* stackView = [[UIStackView alloc] init];
stackView.translatesAutoresizingMaskIntoConstraints = NO;
stackView.axis = UILayoutConstraintAxisVertical;
UIView* firstBulletPoint =
useIcon ? iconViews[0] : [self createStepNumberView:1];
firstBulletPoint.translatesAutoresizingMaskIntoConstraints = NO;
[stackView addArrangedSubview:[self createLineInstruction:instructionList[0]
bulletPointView:firstBulletPoint
index:0]];
for (NSUInteger i = 1; i < [instructionList count]; i++) {
UIView* bulletPoint =
useIcon ? iconViews[i] : [self createStepNumberView:i + 1];
bulletPoint.translatesAutoresizingMaskIntoConstraints = NO;
[stackView addArrangedSubview:[self createLineSeparator]];
[stackView
addArrangedSubview:[self createLineInstruction:instructionList[i]
bulletPointView:bulletPoint
index:i]];
}
[self addSubview:stackView];
AddSameConstraints(self, stackView);
switch (style) {
case InstructionViewStyleGrayscale:
self.backgroundColor =
[UIColor colorNamed:kGroupedSecondaryBackgroundColor];
break;
case InstructionViewStyleDefault:
self.backgroundColor = [UIColor colorNamed:kSecondaryBackgroundColor];
break;
}
self.layer.cornerRadius = kCornerRadius;
}
return self;
}
- (instancetype)initWithList:(NSArray<NSString*>*)instructionList
style:(InstructionViewStyle)style
icons:(NSArray<UIImage*>*)icons {
NSMutableArray<UIView*>* iconViews = nil;
if (icons) {
iconViews = [NSMutableArray array];
for (UIImage* icon in icons) {
UIView* iconView = CreateIconView(icon);
[iconViews addObject:iconView];
}
}
return [self initWithList:instructionList style:style iconViews:iconViews];
}
- (instancetype)initWithList:(NSArray<NSString*>*)instructionList
style:(InstructionViewStyle)style {
return [self initWithList:instructionList style:style icons:nil];
}
- (instancetype)initWithList:(NSArray<NSString*>*)instructionList {
return [self initWithList:instructionList style:InstructionViewStyleDefault];
}
- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
if (self.traitCollection.userInterfaceStyle !=
previousTraitCollection.userInterfaceStyle) {
for (UILabel* stepNumberLabel in self.stepNumberLabels) {
[self updateColorForStepNumberLabel:stepNumberLabel];
}
}
}
#pragma mark - Private
// Creates a separator line.
- (UIView*)createLineSeparator {
UIView* liner = [[UIView alloc] init];
UIView* separator = [[UIView alloc] init];
separator.backgroundColor = [UIColor colorNamed:kGrey300Color];
separator.translatesAutoresizingMaskIntoConstraints = NO;
[liner addSubview:separator];
[NSLayoutConstraint activateConstraints:@[
[separator.leadingAnchor constraintEqualToAnchor:liner.leadingAnchor
constant:kSeparatorLeadingMargin],
[separator.trailingAnchor constraintEqualToAnchor:liner.trailingAnchor],
[separator.topAnchor constraintEqualToAnchor:liner.topAnchor],
[separator.bottomAnchor constraintEqualToAnchor:liner.bottomAnchor],
[liner.heightAnchor
constraintEqualToConstant:AlignValueToPixel(kSeparatorHeight)],
]];
return liner;
}
// Creates an instruction line with a bullet point view followed by
// instructions.
- (UIView*)createLineInstruction:(NSString*)instruction
bulletPointView:(UIView*)bulletPointView
index:(NSInteger)index {
UILabel* instructionLabel = [[UILabel alloc] init];
instructionLabel.textColor = [UIColor colorNamed:kGrey800Color];
instructionLabel.font =
[UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline];
instructionLabel.attributedText =
PutBoldPartInString(instruction, UIFontTextStyleSubheadline);
instructionLabel.numberOfLines = 0;
instructionLabel.adjustsFontForContentSizeCategory = YES;
instructionLabel.translatesAutoresizingMaskIntoConstraints = NO;
[instructionLabel
setContentCompressionResistancePriority:UILayoutPriorityDefaultHigh + 1
forAxis:UILayoutConstraintAxisVertical];
UIView* line = [[UIView alloc] init];
[line addSubview:bulletPointView];
[line addSubview:instructionLabel];
// Add constraints for bulletPointView and instructionLabel vertical margins
// to make sure that they are as small as possible.
NSLayoutConstraint* minimumBulletPointTopMargin =
[bulletPointView.topAnchor constraintEqualToAnchor:line.topAnchor
constant:kVerticalMargin];
minimumBulletPointTopMargin.priority = UILayoutPriorityDefaultHigh;
NSLayoutConstraint* minimumBulletPointBottomMargin =
[bulletPointView.bottomAnchor constraintEqualToAnchor:line.bottomAnchor
constant:-kVerticalMargin];
minimumBulletPointBottomMargin.priority = UILayoutPriorityDefaultHigh;
NSLayoutConstraint* minimumLabelTopMargin =
[instructionLabel.topAnchor constraintEqualToAnchor:line.topAnchor
constant:kVerticalMargin];
minimumLabelTopMargin.priority = UILayoutPriorityDefaultHigh;
NSLayoutConstraint* minimumLabelBottomMargin =
[instructionLabel.bottomAnchor constraintEqualToAnchor:line.bottomAnchor
constant:-kVerticalMargin];
minimumLabelBottomMargin.priority = UILayoutPriorityDefaultHigh;
[NSLayoutConstraint activateConstraints:@[
[line.heightAnchor
constraintGreaterThanOrEqualToConstant:kMinimumLineHeight],
[bulletPointView.leadingAnchor constraintEqualToAnchor:line.leadingAnchor
constant:kLeadingMargin],
[bulletPointView.centerYAnchor constraintEqualToAnchor:line.centerYAnchor],
[instructionLabel.leadingAnchor
constraintEqualToAnchor:bulletPointView.trailingAnchor
constant:kSpacing],
[instructionLabel.centerYAnchor constraintEqualToAnchor:line.centerYAnchor],
minimumBulletPointTopMargin, minimumBulletPointBottomMargin,
minimumLabelTopMargin, minimumLabelBottomMargin,
[bulletPointView.bottomAnchor
constraintLessThanOrEqualToAnchor:line.bottomAnchor
constant:-kVerticalMargin],
[bulletPointView.topAnchor
constraintGreaterThanOrEqualToAnchor:line.topAnchor
constant:kVerticalMargin],
[instructionLabel.bottomAnchor
constraintLessThanOrEqualToAnchor:line.bottomAnchor
constant:-kVerticalMargin],
[instructionLabel.topAnchor
constraintGreaterThanOrEqualToAnchor:line.topAnchor
constant:kVerticalMargin],
[instructionLabel.trailingAnchor constraintEqualToAnchor:line.trailingAnchor
constant:-kTrailingMargin]
]];
line.tag = index;
line.accessibilityIdentifier =
InstructionViewRowAccessibilityIdentifier(index);
line.accessibilityElements = @[ bulletPointView, instructionLabel ];
// Don't set the accessibility traits indicating that it is tappable as we do
// not actually expect any action, instead, we just want to measure how many
// people believe it’s tappable.
return line;
}
// Creates a view with a round numbered label in it.
- (UIView*)createStepNumberView:(NSInteger)stepNumber {
UILabel* stepNumberLabel = [[UILabel alloc] initWithFrame:CGRectZero];
stepNumberLabel.translatesAutoresizingMaskIntoConstraints = NO;
stepNumberLabel.textAlignment = NSTextAlignmentCenter;
stepNumberLabel.text = [@(stepNumber) stringValue];
stepNumberLabel.font = PreferredFontForTextStyleWithMaxCategory(
UIFontTextStyleFootnote,
self.traitCollection.preferredContentSizeCategory,
UIContentSizeCategoryExtraExtraExtraLarge);
UIFontDescriptor* boldFontDescriptor = [stepNumberLabel.font.fontDescriptor
fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitBold];
stepNumberLabel.font = [UIFont fontWithDescriptor:boldFontDescriptor size:0];
stepNumberLabel.layer.cornerRadius = kStepNumberLabelSize / 2;
[self updateColorForStepNumberLabel:stepNumberLabel];
[self.stepNumberLabels addObject:stepNumberLabel];
UIView* labelContainer = [[UIView alloc] initWithFrame:CGRectZero];
labelContainer.translatesAutoresizingMaskIntoConstraints = NO;
[labelContainer addSubview:stepNumberLabel];
[NSLayoutConstraint activateConstraints:@[
[stepNumberLabel.centerYAnchor
constraintEqualToAnchor:labelContainer.centerYAnchor],
[stepNumberLabel.centerXAnchor
constraintEqualToAnchor:labelContainer.centerXAnchor],
[stepNumberLabel.widthAnchor
constraintEqualToConstant:kStepNumberLabelSize],
[stepNumberLabel.heightAnchor
constraintEqualToConstant:kStepNumberLabelSize],
[labelContainer.widthAnchor constraintEqualToConstant:kIconLabelWidth],
[labelContainer.heightAnchor
constraintEqualToAnchor:stepNumberLabel.heightAnchor],
]];
return labelContainer;
}
// Sets and update the background color of the step number label on
// initialization and when entering or exiting dark mode.
- (void)updateColorForStepNumberLabel:(UILabel*)stepNumberLabel {
switch (self.style) {
case InstructionViewStyleGrayscale:
stepNumberLabel.textColor = [UIColor colorNamed:kGrey600Color];
stepNumberLabel.layer.backgroundColor =
[UIColor colorNamed:kGroupedPrimaryBackgroundColor].CGColor;
break;
case InstructionViewStyleDefault:
stepNumberLabel.textColor = [UIColor colorNamed:kBlueColor];
stepNumberLabel.layer.backgroundColor =
[UIColor colorNamed:kPrimaryBackgroundColor].CGColor;
break;
}
}
@end