|  | // 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" | 
|  |  | 
|  | #if !defined(__has_feature) || !__has_feature(objc_arc) | 
|  | #error "This file requires ARC support." | 
|  | #endif | 
|  |  | 
|  | 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 | 
|  | addGestureRecognizer:[[UITapGestureRecognizer alloc] | 
|  | initWithTarget:self | 
|  | action:@selector | 
|  | (tappedOnALineWithGestureRecognizer:)]]; | 
|  | // 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; | 
|  | } | 
|  | } | 
|  |  | 
|  | - (void)tappedOnALineWithGestureRecognizer: | 
|  | (UITapGestureRecognizer*)gestureRecognizer { | 
|  | [self.tapListener tappedOnLineNumber:gestureRecognizer.view.tag]; | 
|  | } | 
|  |  | 
|  | @end |