// 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/contextual_panel/entrypoint/ui/contextual_panel_entrypoint_view_controller.h"
#import "base/i18n/rtl.h"
#import "base/memory/weak_ptr.h"
#import "base/metrics/user_metrics.h"
#import "base/metrics/user_metrics_action.h"
#import "base/strings/sys_string_conversions.h"
#import "ios/chrome/browser/bubble/ui_bundled/bubble_util.h"
#import "ios/chrome/browser/contextual_panel/entrypoint/ui/contextual_panel_entrypoint_mutator.h"
#import "ios/chrome/browser/contextual_panel/entrypoint/ui/contextual_panel_entrypoint_visibility_delegate.h"
#import "ios/chrome/browser/contextual_panel/model/contextual_panel_item_configuration.h"
#import "ios/chrome/browser/location_bar/ui_bundled/location_bar_constants.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.h"
#import "ios/chrome/browser/shared/ui/util/layout_guide_names.h"
#import "ios/chrome/browser/shared/ui/util/util_swift.h"
#import "ios/chrome/browser/ui/fullscreen/fullscreen_animator.h"
#import "ios/chrome/common/material_timing.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/pointer_interaction_util.h"
namespace {
// The relative height of the entrypoint badge button compared to the location
// bar's height.
const CGFloat kEntrypointHeightMultiplier = 0.72;
// The margins before and after the entrypoint's label used as multipliers of
// the entrypoint container's height.
const CGFloat kLabelTrailingSpaceMultiplier = 0.375;
const CGFloat kLabelLeadingSpaceMultiplier = 0.095;
// Entrypoint and Infobar badges separator constants.
const CGFloat kSeparatorHeightMultiplier = 0.35;
const CGFloat kSeparatorWidthConstant = 1;
// Amount of time animating the entrypoint into the location bar should take.
const NSTimeInterval kEntrypointDisplayingAnimationTime = 0.3;
// Amount of time animating the large entrypoint (label)
// appearance/disappearance.
const NSTimeInterval kLargeEntrypointAppearingAnimationTime = 0.2;
const NSTimeInterval kLargeEntrypointDisappearingAnimationTime = 0.3;
// Entrypoint container shadow constants.
const float kEntrypointContainerShadowOpacity = 0.09f;
const float kEntrypointContainerShadowRadius = 5.0f;
const CGSize kEntrypointContainerShadowOffset = {0, 3};
// The point size of the entrypoint's symbol.
const CGFloat kEntrypointSymbolPointSize = 15;
// The colorset used for the Contextual Panel's entrypoint background.
NSString* const kContextualPanelEntrypointBackgroundColor =
@"contextual_panel_entrypoint_background_color";
// Accessibility identifier for the entrypoint's image view.
NSString* const kContextualPanelEntrypointImageViewIdentifier =
@"ContextualPanelEntrypointImageViewAXID";
// Accessibility identifier for the entrypoint's label.
NSString* const kContextualPanelEntrypointLabelIdentifier =
@"ContextualPanelEntrypointLabelAXID";
} // namespace
@interface ContextualPanelEntrypointViewController () {
// The UIButton contains the wrapper UIView, which itself contains the
// entrypoint items (image and label). The container (UIButton) is needed for
// button-like behavior and to create the shadow around the entire entrypoint
// package. The wrapper (UIView) is needed for clipping the label to the
// entrypoint's bounds for proper animations and sizing.
UIButton* _entrypointContainer;
UIView* _entrypointItemsWrapper;
UIImageView* _imageView;
UILabel* _label;
// The small vertical pill-shaped line separating the Contextual Panel
// entrypoint and Infobar badges, if present.
UIView* _separator;
// Constraints for the two states of the trailing edge of the entrypoint
// container. They are activated/deactivated as needed when the label is
// shown/hidden.
NSLayoutConstraint* _largeTrailingConstraint;
NSLayoutConstraint* _smallTrailingConstraint;
// Whether the entrypoint should be "tapped" visually, because the Contextual
// Panel is open.
BOOL _entrypointTapped;
// Whether the entrypoint should currently be shown or not (transcends
// fullscreen events).
BOOL _entrypointDisplayed;
// Whether the entrypoint should currently collapse for fullscreen.
BOOL _shouldCollapseForFullscreen;
// Whether there currently are any Infobar badges being shown.
BOOL _infobarBadgesCurrentlyShown;
// LayoutGuideCenter to register the entrypoint container's view for global
// access, only when it is large (i.e. dismissable).
LayoutGuideCenter* _layoutGuideCenter;
// Swipe gesture recognizer for the entrypoint (allows the user to "dismiss"
// the large chip entrypoint).
UISwipeGestureRecognizer* _swipeRecognizer;
}
@end
@implementation ContextualPanelEntrypointViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Set the view as hidden when created as it should only appear when the
// entrypoint should be shown.
self.view.hidden = YES;
self.view.isAccessibilityElement = NO;
_entrypointDisplayed = NO;
_entrypointContainer = [self configuredEntrypointContainer];
_entrypointItemsWrapper = [self configuredEntrypointItemsWrapper];
_imageView = [self configuredImageView];
_label = [self configuredLabel];
_separator = [self configuredSeparator];
[self.view addSubview:_entrypointContainer];
[self.view addSubview:_separator];
[_entrypointContainer addSubview:_entrypointItemsWrapper];
[_entrypointItemsWrapper addSubview:_imageView];
[_entrypointItemsWrapper addSubview:_label];
_entrypointContainer.isAccessibilityElement = !self.view.hidden;
[self activateInitialConstraints];
if (@available(iOS 17, *)) {
[self registerForTraitChanges:@[ UITraitPreferredContentSizeCategory.self ]
withAction:@selector(updateLabelFont)];
}
// TODO(crbug.com/361110974): Have bubbles gracefully handle orientation
// changes without needing to dismiss here.
NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
[center addObserver:self
selector:@selector(dismissIPHWithoutAnimation)
name:UIDeviceOrientationDidChangeNotification
object:nil];
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
_entrypointContainer.layer.cornerRadius =
_entrypointContainer.bounds.size.height / 2.0;
_entrypointItemsWrapper.layer.cornerRadius =
_entrypointItemsWrapper.bounds.size.height / 2.0;
_separator.layer.cornerRadius = _separator.bounds.size.width / 2.0;
}
- (void)displayEntrypointView:(BOOL)display {
if (!display) {
[self dismissIPHWithoutAnimation];
}
BOOL hidden = !display || !_entrypointDisplayed;
[self.visibilityDelegate setContextualPanelEntrypointHidden:hidden];
_entrypointContainer.isAccessibilityElement = !self.view.hidden;
}
- (CGPoint)helpAnchorUsingBottomOmnibox:(BOOL)isBottomOmnibox {
CGPoint anchorPointInSuperview =
CGPointMake(CGRectGetMidX(_entrypointContainer.bounds),
isBottomOmnibox ? CGRectGetMinY(_entrypointContainer.bounds)
: CGRectGetMaxY(_entrypointContainer.bounds));
CGPoint anchorPointInWindow =
[self.view.window convertPoint:anchorPointInSuperview
fromView:_entrypointContainer];
// The default bubble alignment is the minimum distance from the edge of the
// window that the bubble can appear at, so use MAX (or MIN in RTL) between
// that and the MidX of the entrypoint container.
anchorPointInWindow.x =
base::i18n::IsRTL() ? MIN(self.view.window.bounds.size.width -
bubble_util::BubbleDefaultAlignmentOffset(),
anchorPointInWindow.x)
: MAX(bubble_util::BubbleDefaultAlignmentOffset(),
anchorPointInWindow.x);
return anchorPointInWindow;
}
#pragma mark - private
// Creates and configures the entrypoint's button container view.
- (UIButton*)configuredEntrypointContainer {
UIButton* button = [[UIButton alloc] init];
button.translatesAutoresizingMaskIntoConstraints = NO;
button.backgroundColor =
[UIColor colorNamed:kContextualPanelEntrypointBackgroundColor];
button.clipsToBounds = NO;
button.pointerInteractionEnabled = YES;
button.pointerStyleProvider = CreateLiftEffectCirclePointerStyleProvider();
// Configure shadow.
button.layer.shadowColor = [[UIColor blackColor] CGColor];
button.layer.shadowOpacity = kEntrypointContainerShadowOpacity;
button.layer.shadowOffset = kEntrypointContainerShadowOffset;
button.layer.shadowRadius = kEntrypointContainerShadowRadius;
button.layer.masksToBounds = NO;
[button addTarget:self
action:@selector(userTappedEntrypoint)
forControlEvents:UIControlEventTouchUpInside];
return button;
}
// Creates and configures the entrypoint's items wrapper view which mirrors the
// container view but adds clipping to bounds.
- (UIView*)configuredEntrypointItemsWrapper {
UIView* view = [[UIView alloc] init];
view.translatesAutoresizingMaskIntoConstraints = NO;
view.userInteractionEnabled = NO;
view.clipsToBounds = YES;
return view;
}
// Creates and configures the entrypoint's image view.
- (UIImageView*)configuredImageView {
UIImageView* imageView = [[UIImageView alloc] init];
imageView.translatesAutoresizingMaskIntoConstraints = NO;
imageView.isAccessibilityElement = NO;
imageView.contentMode = UIViewContentModeCenter;
imageView.tintColor = [UIColor colorNamed:kBlue600Color];
imageView.accessibilityIdentifier =
kContextualPanelEntrypointImageViewIdentifier;
[imageView
setContentCompressionResistancePriority:UILayoutPriorityDefaultHigh + 1
forAxis:UILayoutConstraintAxisHorizontal];
UIImageSymbolConfiguration* symbolConfig = [UIImageSymbolConfiguration
configurationWithPointSize:kEntrypointSymbolPointSize
weight:UIImageSymbolWeightRegular
scale:UIImageSymbolScaleMedium];
imageView.preferredSymbolConfiguration = symbolConfig;
return imageView;
}
// Creates and configures the entrypoint's label for louder moments. Starts off
// as hidden.
- (UILabel*)configuredLabel {
UILabel* label = [[UILabel alloc] init];
label.translatesAutoresizingMaskIntoConstraints = NO;
label.font = [self entrypointLabelFont];
label.numberOfLines = 1;
label.accessibilityIdentifier = kContextualPanelEntrypointLabelIdentifier;
label.isAccessibilityElement = NO;
[label
setContentCompressionResistancePriority:UILayoutPriorityDefaultHigh + 1
forAxis:UILayoutConstraintAxisHorizontal];
return label;
}
// Creates and configures the entrypoint's pill-shaped separator (vertical
// line).
- (UIView*)configuredSeparator {
UIView* view = [[UIView alloc] init];
view.translatesAutoresizingMaskIntoConstraints = NO;
view.isAccessibilityElement = NO;
view.backgroundColor = [UIColor colorNamed:kGrey400Color];
return view;
}
- (void)activateInitialConstraints {
// Leading space before the start of the button container view.
UILayoutGuide* entrypointLeadingSpace = [[UILayoutGuide alloc] init];
[self.view addLayoutGuide:entrypointLeadingSpace];
UILayoutGuide* labelLeadingSpace = [[UILayoutGuide alloc] init];
UILayoutGuide* labelTrailingSpace = [[UILayoutGuide alloc] init];
[_entrypointItemsWrapper addLayoutGuide:labelLeadingSpace];
[_entrypointItemsWrapper addLayoutGuide:labelTrailingSpace];
_smallTrailingConstraint = [_entrypointContainer.trailingAnchor
constraintEqualToAnchor:_imageView.trailingAnchor];
_largeTrailingConstraint = [_entrypointContainer.trailingAnchor
constraintEqualToAnchor:labelTrailingSpace.trailingAnchor];
[NSLayoutConstraint activateConstraints:@[
[self.view.widthAnchor
constraintGreaterThanOrEqualToAnchor:self.view.heightAnchor],
_smallTrailingConstraint,
// The entrypoint doesn't fully fill the height of the location bar, so to
// make it exactly follow the curvature of the location bar's corner radius,
// it must be placed with the same amount of margin space horizontally that
// exists vertically between the entrypoint and the location bar itself.
[entrypointLeadingSpace.widthAnchor
constraintEqualToAnchor:self.view.heightAnchor
multiplier:((1 - kEntrypointHeightMultiplier) / 2)],
[entrypointLeadingSpace.leadingAnchor
constraintEqualToAnchor:self.view.leadingAnchor],
[entrypointLeadingSpace.trailingAnchor
constraintEqualToAnchor:_entrypointContainer.leadingAnchor],
[_entrypointContainer.leadingAnchor
constraintEqualToAnchor:entrypointLeadingSpace.trailingAnchor],
[_separator.centerXAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[_separator.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor],
[_separator.widthAnchor constraintEqualToConstant:kSeparatorWidthConstant],
[_separator.heightAnchor
constraintEqualToAnchor:self.view.heightAnchor
multiplier:kSeparatorHeightMultiplier],
[_entrypointContainer.heightAnchor
constraintEqualToAnchor:self.view.heightAnchor
multiplier:kEntrypointHeightMultiplier],
[_entrypointContainer.centerYAnchor
constraintEqualToAnchor:self.view.centerYAnchor],
[self.view.leadingAnchor
constraintEqualToAnchor:entrypointLeadingSpace.leadingAnchor],
[self.view.trailingAnchor
constraintGreaterThanOrEqualToAnchor:_entrypointContainer
.trailingAnchor],
[_imageView.heightAnchor
constraintEqualToAnchor:_entrypointContainer.heightAnchor],
[_imageView.widthAnchor constraintEqualToAnchor:_imageView.heightAnchor],
[_imageView.leadingAnchor
constraintEqualToAnchor:_entrypointContainer.leadingAnchor],
[_imageView.centerYAnchor
constraintEqualToAnchor:_entrypointContainer.centerYAnchor],
[labelLeadingSpace.leadingAnchor
constraintEqualToAnchor:_imageView.trailingAnchor],
[labelLeadingSpace.widthAnchor
constraintEqualToAnchor:self.view.heightAnchor
multiplier:kLabelLeadingSpaceMultiplier],
[labelTrailingSpace.widthAnchor
constraintEqualToAnchor:self.view.heightAnchor
multiplier:kLabelTrailingSpaceMultiplier],
[labelTrailingSpace.leadingAnchor
constraintEqualToAnchor:_label.trailingAnchor],
[_label.heightAnchor
constraintEqualToAnchor:_entrypointContainer.heightAnchor],
[_label.centerYAnchor
constraintEqualToAnchor:_entrypointContainer.centerYAnchor],
[_label.leadingAnchor
constraintEqualToAnchor:labelLeadingSpace.trailingAnchor],
]];
AddSameConstraints(_entrypointItemsWrapper, _entrypointContainer);
}
- (void)activateLargeEntrypointTrailingConstraint {
_smallTrailingConstraint.active = NO;
_largeTrailingConstraint.active = YES;
}
- (void)activateSmallEntrypointTrailingConstraint {
_largeTrailingConstraint.active = NO;
_smallTrailingConstraint.active = YES;
}
- (void)updateLabelFont {
_label.font = [self entrypointLabelFont];
}
- (void)dismissIPHWithoutAnimation {
[self.mutator dismissIPHAnimated:NO];
}
// Returns the preferred font and size given the current ContentSizeCategory.
- (UIFont*)entrypointLabelFont {
return PreferredFontForTextStyleWithMaxCategory(
UIFontTextStyleFootnote,
self.traitCollection.preferredContentSizeCategory,
UIContentSizeCategoryAccessibilityLarge);
}
// Sets the proper entrypoint visual features depending on current infobar
// badges status and whether the Contextual Panel is open.
- (void)refreshEntrypointVisualElements {
BOOL shouldShowMutedColors =
_infobarBadgesCurrentlyShown || _entrypointTapped;
// Entrypoint icon tint color.
_imageView.tintColor = shouldShowMutedColors
? [UIColor colorNamed:kGrey600Color]
: [UIColor colorNamed:kBlue600Color];
// Entrypoint container shadow.
_entrypointContainer.layer.shadowOpacity =
shouldShowMutedColors ? 0 : kEntrypointContainerShadowOpacity;
// Entrypoint container background color.
UIColor* untappedEntrypointColor =
_infobarBadgesCurrentlyShown
? nil
: [UIColor colorNamed:kContextualPanelEntrypointBackgroundColor];
_entrypointContainer.backgroundColor =
_entrypointTapped ? [UIColor colorNamed:kTertiaryBackgroundColor]
: untappedEntrypointColor;
// Separator visibility.
_separator.hidden = !_infobarBadgesCurrentlyShown;
}
// Applies the correct color to the entrypoint (highlighted blue when the
// in-product help is present), otherwise back to the normal colorset.
- (void)styleEntrypointForColoredState:(BOOL)colored {
_imageView.tintColor =
colored ? [UIColor colorNamed:kContextualPanelEntrypointBackgroundColor]
: [UIColor colorNamed:kBlue600Color];
_entrypointContainer.backgroundColor =
colored ? [UIColor colorNamed:kBlue600Color]
: [UIColor colorNamed:kContextualPanelEntrypointBackgroundColor];
}
// User swiped the large entrypoint chip towards the leading edge, intending to
// dismiss it.
- (void)largeEntrypointChipSwiped {
[self transitionToSmallEntrypoint];
base::RecordAction(base::UserMetricsAction(
"IOSContextualPanelEntrypointLargeChipDismissedWithSwipe"));
}
// Refreshes the VoiceOver bounding box if VoiceOver is currently running and
// the entrypoint is focused.
- (void)refreshVoiceOverBoundingBoxIfFocused {
if (!UIAccessibilityIsVoiceOverRunning() ||
![_entrypointContainer accessibilityElementIsFocused]) {
return;
}
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification,
_entrypointContainer);
}
#pragma mark - ContextualPanelEntrypointConsumer
- (void)setEntrypointConfig:
(base::WeakPtr<ContextualPanelItemConfiguration>)config {
if (!config) {
return;
}
_entrypointContainer.accessibilityLabel =
base::SysUTF8ToNSString(config->accessibility_label);
_label.text = base::SysUTF8ToNSString(config->entrypoint_message);
UIImage* image = CustomSymbolWithPointSize(
base::SysUTF8ToNSString(config->entrypoint_image_name),
kEntrypointSymbolPointSize);
_imageView.image = image;
}
- (void)setInfobarBadgesCurrentlyShown:(BOOL)infobarBadgesCurrentlyShown {
_infobarBadgesCurrentlyShown = infobarBadgesCurrentlyShown;
[self refreshEntrypointVisualElements];
[self transitionToSmallEntrypoint];
}
- (void)showEntrypoint {
[self refreshEntrypointVisualElements];
if (_entrypointDisplayed) {
return;
}
_entrypointDisplayed = YES;
if (_shouldCollapseForFullscreen) {
return;
}
// Animate the entrypoint appearance.
self.view.alpha = 0;
self.view.transform = CGAffineTransformMakeScale(0.95, 0.95);
[self.visibilityDelegate setContextualPanelEntrypointHidden:NO];
_entrypointContainer.isAccessibilityElement = !self.view.hidden;
__weak ContextualPanelEntrypointViewController* weakSelf = self;
[UIView animateWithDuration:kEntrypointDisplayingAnimationTime
delay:0
options:(UIViewAnimationOptionCurveEaseIn |
UIViewAnimationOptionAllowUserInteraction)
animations:^{
self.view.alpha = 1;
self.view.transform = CGAffineTransformIdentity;
}
completion:^(BOOL completed) {
[weakSelf refreshVoiceOverBoundingBoxIfFocused];
}];
}
- (void)hideEntrypoint {
[self transitionToSmallEntrypoint];
[self transitionToContextualPanelOpenedState:NO];
_entrypointDisplayed = NO;
[self.visibilityDelegate setContextualPanelEntrypointHidden:YES];
_entrypointContainer.isAccessibilityElement = !self.view.hidden;
[self.mutator setLocationBarLabelCenteredBetweenContent:NO];
[self.view layoutIfNeeded];
[self refreshVoiceOverBoundingBoxIfFocused];
}
- (void)transitionToLargeEntrypoint {
if (_largeTrailingConstraint.active) {
return;
}
[_layoutGuideCenter referenceView:_entrypointContainer
underName:kContextualPanelLargeEntrypointGuide];
_swipeRecognizer = [[UISwipeGestureRecognizer alloc]
initWithTarget:self
action:@selector(largeEntrypointChipSwiped)];
_swipeRecognizer.cancelsTouchesInView = YES;
_swipeRecognizer.direction = base::i18n::IsRTL()
? UISwipeGestureRecognizerDirectionRight
: UISwipeGestureRecognizerDirectionLeft;
[_entrypointContainer addGestureRecognizer:_swipeRecognizer];
__weak ContextualPanelEntrypointViewController* weakSelf = self;
void (^animateTransitionToLargeEntrypoint)() = ^{
ContextualPanelEntrypointViewController* strongSelf = weakSelf;
if (!strongSelf) {
return;
}
[strongSelf activateLargeEntrypointTrailingConstraint];
[strongSelf.mutator setLocationBarLabelCenteredBetweenContent:YES];
[strongSelf.view layoutIfNeeded];
};
[UIView animateWithDuration:kLargeEntrypointAppearingAnimationTime
delay:0
options:(UIViewAnimationOptionCurveEaseOut |
UIViewAnimationOptionAllowUserInteraction)
animations:animateTransitionToLargeEntrypoint
completion:^(BOOL completed) {
[weakSelf refreshVoiceOverBoundingBoxIfFocused];
}];
}
- (void)transitionToSmallEntrypoint {
if (_smallTrailingConstraint.active) {
return;
}
__weak ContextualPanelEntrypointViewController* weakSelf = self;
void (^animateTransitionToSmallEntrypoint)() = ^{
ContextualPanelEntrypointViewController* strongSelf = weakSelf;
if (!strongSelf) {
return;
}
[strongSelf activateSmallEntrypointTrailingConstraint];
[strongSelf.mutator setLocationBarLabelCenteredBetweenContent:NO];
[strongSelf.view layoutIfNeeded];
};
[UIView animateWithDuration:kLargeEntrypointDisappearingAnimationTime
delay:0
options:(UIViewAnimationOptionCurveEaseOut |
UIViewAnimationOptionAllowUserInteraction)
animations:animateTransitionToSmallEntrypoint
completion:^(BOOL completed) {
[weakSelf refreshVoiceOverBoundingBoxIfFocused];
}];
[_entrypointContainer removeGestureRecognizer:_swipeRecognizer];
[_layoutGuideCenter referenceView:nil
underName:kContextualPanelLargeEntrypointGuide];
}
- (void)transitionToContextualPanelOpenedState:(BOOL)opened {
_entrypointTapped = opened;
[self refreshEntrypointVisualElements];
[self transitionToSmallEntrypoint];
}
- (void)setEntrypointColored:(BOOL)colored {
if (!ShouldHighlightContextualPanelEntrypointDuringIPH()) {
return;
}
__weak ContextualPanelEntrypointViewController* weakSelf = self;
[UIView animateWithDuration:kEntrypointDisplayingAnimationTime
delay:0
options:(UIViewAnimationOptionCurveEaseOut |
UIViewAnimationOptionAllowUserInteraction)
animations:^{
[weakSelf styleEntrypointForColoredState:colored];
}
completion:nil];
}
#pragma mark - ContextualPanelEntrypointMutator
- (void)userTappedEntrypoint {
[self.mutator entrypointTapped];
}
#pragma mark FullscreenUIElement
- (void)updateForFullscreenProgress:(CGFloat)progress {
_shouldCollapseForFullscreen = progress <= kFullscreenProgressThreshold;
if (_shouldCollapseForFullscreen) {
self.view.hidden = YES;
} else {
self.view.hidden = !_entrypointDisplayed;
// Fade in/out the entrypoint badge.
CGFloat alphaValue = fmax((progress - kFullscreenProgressThreshold) /
(1 - kFullscreenProgressThreshold),
0);
self.view.alpha = alphaValue;
}
_entrypointContainer.isAccessibilityElement = !self.view.hidden;
}
#pragma mark - UIView
- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
if (@available(iOS 17, *)) {
return;
}
if (previousTraitCollection.preferredContentSizeCategory !=
self.traitCollection.preferredContentSizeCategory) {
[self updateLabelFont];
}
}
@end