0

[ios] Add a moduleLayoutGuide to NTPViewController

This layout guide will initially be used to horizontally lay out all
the NTP "modules", including the MagicStack, the recent tab tile, MVTs,
and the "Feed Containment" module.

Using a layout guide allows the logic for module layout to be
centralized, and allows for easier maintenance of the code and an easier
path to implementing future changes.

Aligning the MagicStack with the layout guide required a number of
changes, in order to allow it to have a width that can change:
 * width constraints for modules are added when the module is added
 * module width is determined at the time it is needed, because it can
   change
 * a view was added to mask/clip the MagicStack in landscape, and a
   peek offset was used to ensure the module is aligned properly so that
   the modules on left and right peeking in looks right
 * when the module width changes, the MagicStackScrollView offset needs
   to be adjusted so that it is not left in the middle of a page

Demo of changes:
https://screenshot.googleplex.com/8zhB9SRjZAbphvP.png

Change-Id: I0fed92ddf67259deaf6e084aec1f64f5348494a0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5117205
Code-Coverage: findit-for-me@appspot.gserviceaccount.com <findit-for-me@appspot.gserviceaccount.com>
Reviewed-by: Chris Lu <thegreenfrog@chromium.org>
Commit-Queue: Scott Yoder <scottyoder@google.com>
Reviewed-by: Adam Arcaro <adamta@google.com>
Cr-Commit-Position: refs/heads/main@{#1238175}
This commit is contained in:
Scott Yoder
2023-12-15 19:01:34 +00:00
committed by Chromium LUCI CQ
parent 51bfc00eff
commit ced67f020f
13 changed files with 220 additions and 288 deletions

@@ -261,7 +261,6 @@ source_set("content_suggestions_ui") {
"//ios/chrome/browser/ui/content_suggestions/set_up_list:utils", "//ios/chrome/browser/ui/content_suggestions/set_up_list:utils",
"//ios/chrome/browser/ui/content_suggestions/tab_resumption", "//ios/chrome/browser/ui/content_suggestions/tab_resumption",
"//ios/chrome/browser/ui/ntp:constants", "//ios/chrome/browser/ui/ntp:constants",
"//ios/chrome/browser/ui/ntp:ntp_ui_util",
"//ios/chrome/browser/ui/start_surface:feature_flags", "//ios/chrome/browser/ui/start_surface:feature_flags",
"//ios/chrome/browser/ui/toolbar/public", "//ios/chrome/browser/ui/toolbar/public",
"//ios/chrome/browser/url_loading/model", "//ios/chrome/browser/url_loading/model",

@@ -22,10 +22,6 @@ enum class ContentSuggestionsModuleType;
- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE; - (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE;
- (instancetype)initWithCoder:(NSCoder*)aDecoder NS_UNAVAILABLE; - (instancetype)initWithCoder:(NSCoder*)aDecoder NS_UNAVAILABLE;
// Returns the module width depending on the horizontal `traitCollection`.
+ (CGFloat)moduleWidthForHorizontalTraitCollection:
(UITraitCollection*)traitCollection;
// Returns the title string for the module `type`. // Returns the title string for the module `type`.
+ (NSString*)titleStringForModule:(ContentSuggestionsModuleType)type; + (NSString*)titleStringForModule:(ContentSuggestionsModuleType)type;

@@ -38,20 +38,11 @@ const CGFloat kContentVerticalSpacing = 16.0f;
// The corner radius of this container. // The corner radius of this container.
const float kCornerRadius = 24; const float kCornerRadius = 24;
// The width of the modules.
const int kModuleWidthCompact = 343;
const int kModuleWidthRegular = 382;
// The max height of the modules. // The max height of the modules.
const int kModuleMaxHeight = 150; const int kModuleMaxHeight = 150;
const CGFloat kSeparatorHeight = 0.5; const CGFloat kSeparatorHeight = 0.5;
// The horizontal trailing spacing between the top horizontal StackView
// (containing the title and any subtitle/See More buttons) and the module's
// overall vertical container StackView when there is none between the overall
// vertical StackView and this container .
const CGFloat kTitleStackViewTrailingMargin = 16.0f;
} // namespace } // namespace
@interface MagicStackModuleContainer () <UIContextMenuInteractionDelegate> @interface MagicStackModuleContainer () <UIContextMenuInteractionDelegate>
@@ -62,7 +53,6 @@ const CGFloat kTitleStackViewTrailingMargin = 16.0f;
@end @end
@implementation MagicStackModuleContainer { @implementation MagicStackModuleContainer {
NSLayoutConstraint* _contentViewWidthAnchor;
id<MagicStackModuleContainerDelegate> _delegate; id<MagicStackModuleContainerDelegate> _delegate;
UILabel* _title; UILabel* _title;
UILabel* _subtitle; UILabel* _subtitle;
@@ -199,25 +189,13 @@ const CGFloat kTitleStackViewTrailingMargin = 16.0f;
UIStackView* stackView = [[UIStackView alloc] init]; UIStackView* stackView = [[UIStackView alloc] init];
stackView.translatesAutoresizingMaskIntoConstraints = NO; stackView.translatesAutoresizingMaskIntoConstraints = NO;
stackView.alignment = UIStackViewAlignmentLeading; stackView.alignment = UIStackViewAlignmentFill;
stackView.axis = UILayoutConstraintAxisVertical; stackView.axis = UILayoutConstraintAxisVertical;
stackView.spacing = kContentVerticalSpacing; stackView.spacing = kContentVerticalSpacing;
stackView.distribution = UIStackViewDistributionFill; stackView.distribution = UIStackViewDistributionFill;
[stackView addSubview:contentView]; [stackView addSubview:contentView];
if ([_title.text length] > 0) { if ([_title.text length] > 0) {
[stackView addArrangedSubview:titleStackView]; [stackView addArrangedSubview:titleStackView];
// Ensure that there is horizontal trailing spacing between the title
// stackview content and the module. The overall StackView has no trailing
// spacing for kCompactedSetUpList.
CGFloat trailingSpacing =
_type == ContentSuggestionsModuleType::kCompactedSetUpList
? -kTitleStackViewTrailingMargin
: 0;
[NSLayoutConstraint activateConstraints:@[
[titleStackView.trailingAnchor
constraintEqualToAnchor:stackView.trailingAnchor
constant:trailingSpacing],
]];
} }
if ([self shouldShowSeparator]) { if ([self shouldShowSeparator]) {
UIView* separator = [[UIView alloc] init]; UIView* separator = [[UIView alloc] init];
@@ -247,9 +225,6 @@ const CGFloat kTitleStackViewTrailingMargin = 16.0f;
} }
self.accessibilityElements = accessibilityElements; self.accessibilityElements = accessibilityElements;
_contentViewWidthAnchor = [contentView.widthAnchor
constraintEqualToConstant:[self contentViewWidth]];
[NSLayoutConstraint activateConstraints:@[ _contentViewWidthAnchor ]];
// Configures `contentView` to be the view willing to expand if needed to // Configures `contentView` to be the view willing to expand if needed to
// fill extra vertical space in the container. // fill extra vertical space in the container.
[contentView [contentView
@@ -276,14 +251,6 @@ const CGFloat kTitleStackViewTrailingMargin = 16.0f;
return self; return self;
} }
// Returns the module width (CGFloat) given `traitCollection`.
+ (CGFloat)moduleWidthForHorizontalTraitCollection:
(UITraitCollection*)traitCollection {
return traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassRegular
? kModuleWidthRegular
: kModuleWidthCompact;
}
// Returns the module's title, if any, given the Magic Stack module `type`. // Returns the module's title, if any, given the Magic Stack module `type`.
+ (NSString*)titleStringForModule:(ContentSuggestionsModuleType)type { + (NSString*)titleStringForModule:(ContentSuggestionsModuleType)type {
switch (type) { switch (type) {
@@ -348,9 +315,6 @@ const CGFloat kTitleStackViewTrailingMargin = 16.0f;
NSDirectionalEdgeInsetsMake(kContentTopInset, kContentHorizontalInset, NSDirectionalEdgeInsetsMake(kContentTopInset, kContentHorizontalInset,
kContentBottomInset, kContentHorizontalInset); kContentBottomInset, kContentHorizontalInset);
switch (_type) { switch (_type) {
case ContentSuggestionsModuleType::kCompactedSetUpList:
contentMargins.trailing = 0;
break;
case ContentSuggestionsModuleType::kMostVisited: case ContentSuggestionsModuleType::kMostVisited:
case ContentSuggestionsModuleType::kShortcuts: case ContentSuggestionsModuleType::kShortcuts:
case ContentSuggestionsModuleType::kSafetyCheckMultiRow: case ContentSuggestionsModuleType::kSafetyCheckMultiRow:
@@ -363,29 +327,6 @@ const CGFloat kTitleStackViewTrailingMargin = 16.0f;
return contentMargins; return contentMargins;
} }
// Returns the intrinsic content size.
- (CGSize)intrinsicContentSize {
// When the Most Visited Tiles module is not in the Magic Stack or if a module
// is the only module in the Magic Stack in a wider screen, the module should
// be wider to match the wider Magic Stack ScrollView.
BOOL MVTModuleShouldUseWideWidth =
(!_isPlaceholder && _type == ContentSuggestionsModuleType::kMostVisited &&
!ShouldPutMostVisitedSitesInMagicStack() &&
content_suggestions::ShouldShowWiderMagicStackLayer(self.traitCollection,
self.window));
BOOL moduleShouldUseWideWidth =
content_suggestions::ShouldShowWiderMagicStackLayer(self.traitCollection,
self.window) &&
[_delegate doesMagicStackShowOnlyOneModule:_type];
if (MVTModuleShouldUseWideWidth || moduleShouldUseWideWidth) {
return CGSizeMake(kMagicStackWideWidth, UIViewNoIntrinsicMetric);
}
return CGSizeMake(
[MagicStackModuleContainer
moduleWidthForHorizontalTraitCollection:self.traitCollection],
UIViewNoIntrinsicMetric);
}
#pragma mark - UITraitEnvironment #pragma mark - UITraitEnvironment
- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection { - (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
@@ -394,11 +335,6 @@ const CGFloat kTitleStackViewTrailingMargin = 16.0f;
self.traitCollection.preferredContentSizeCategory) { self.traitCollection.preferredContentSizeCategory) {
_title.font = [MagicStackModuleContainer fontForTitle]; _title.font = [MagicStackModuleContainer fontForTitle];
} }
_contentViewWidthAnchor.constant = [self contentViewWidth];
// Trigger relayout so intrinsic contentsize is recalculated.
[self invalidateIntrinsicContentSize];
[self sizeToFit];
[self layoutIfNeeded];
} }
#pragma mark - UIContextMenuInteractionDelegate #pragma mark - UIContextMenuInteractionDelegate
@@ -547,10 +483,4 @@ const CGFloat kTitleStackViewTrailingMargin = 16.0f;
} }
} }
// Returns the expected width of the contentView subview.
- (CGFloat)contentViewWidth {
NSDirectionalEdgeInsets insets = [self contentMargins];
return [self intrinsicContentSize].width - insets.leading - insets.trailing;
}
@end @end

@@ -17,7 +17,6 @@ class WebState;
@protocol NewTabPageControllerDelegate; @protocol NewTabPageControllerDelegate;
@protocol NewTabPageDelegate; @protocol NewTabPageDelegate;
@protocol NewTabPageMetricsDelegate; @protocol NewTabPageMetricsDelegate;
@protocol NewTabPageViewDelegate;
// Coordinator to manage the Suggestions UI via a // Coordinator to manage the Suggestions UI via a
// ContentSuggestionsViewController. // ContentSuggestionsViewController.
@@ -50,9 +49,6 @@ class WebState;
// recorder. // recorder.
@property(nonatomic, weak) id<NewTabPageMetricsDelegate> NTPMetricsDelegate; @property(nonatomic, weak) id<NewTabPageMetricsDelegate> NTPMetricsDelegate;
// Delegate for getting information about NTP views.
@property(nonatomic, weak) id<NewTabPageViewDelegate> NTPViewDelegate;
// Configure Content Suggestions if showing the Start Surface. NOTE: this should // Configure Content Suggestions if showing the Start Surface. NOTE: this should
// only be called once for every Start configuration. Calling it multiple times // only be called once for every Start configuration. Calling it multiple times
// in sequence can lead to unpredictable outcomes. // in sequence can lead to unpredictable outcomes.

@@ -274,7 +274,6 @@
self.contentSuggestionsViewController.parcelTrackingCommandHandler = self.contentSuggestionsViewController.parcelTrackingCommandHandler =
HandlerForProtocol(self.browser->GetCommandDispatcher(), HandlerForProtocol(self.browser->GetCommandDispatcher(),
ParcelTrackingOptInCommands); ParcelTrackingOptInCommands);
self.contentSuggestionsViewController.NTPViewDelegate = self.NTPViewDelegate;
self.contentSuggestionsMediator.consumer = self.contentSuggestionsMediator.consumer =
self.contentSuggestionsViewController; self.contentSuggestionsViewController;

@@ -12,7 +12,6 @@
@protocol ContentSuggestionsCommands; @protocol ContentSuggestionsCommands;
@protocol ContentSuggestionsMenuProvider; @protocol ContentSuggestionsMenuProvider;
@protocol ContentSuggestionsViewControllerAudience; @protocol ContentSuggestionsViewControllerAudience;
@protocol NewTabPageViewDelegate;
@protocol ParcelTrackingOptInCommands; @protocol ParcelTrackingOptInCommands;
@protocol SafetyCheckViewDelegate; @protocol SafetyCheckViewDelegate;
@protocol SetUpListViewDelegate; @protocol SetUpListViewDelegate;
@@ -42,9 +41,6 @@ class UrlLoadingBrowserAgent;
@property(nonatomic, weak) id<ContentSuggestionsMenuProvider> menuProvider; @property(nonatomic, weak) id<ContentSuggestionsMenuProvider> menuProvider;
@property(nonatomic, assign) UrlLoadingBrowserAgent* urlLoadingBrowserAgent; @property(nonatomic, assign) UrlLoadingBrowserAgent* urlLoadingBrowserAgent;
// Delegate for getting information about NTP views.
@property(nonatomic, weak) id<NewTabPageViewDelegate> NTPViewDelegate;
// Recorder for content suggestions metrics. // Recorder for content suggestions metrics.
@property(nonatomic, weak) @property(nonatomic, weak)
ContentSuggestionsMetricsRecorder* contentSuggestionsMetricsRecorder; ContentSuggestionsMetricsRecorder* contentSuggestionsMetricsRecorder;
@@ -59,6 +55,9 @@ class UrlLoadingBrowserAgent;
// The layout guide center to use to refer to the Magic Stack. // The layout guide center to use to refer to the Magic Stack.
@property(nonatomic, strong) LayoutGuideCenter* layoutGuideCenter; @property(nonatomic, strong) LayoutGuideCenter* layoutGuideCenter;
// Called when the module width has changed.
- (void)moduleWidthDidUpdate;
@end @end
#endif // IOS_CHROME_BROWSER_UI_CONTENT_SUGGESTIONS_CONTENT_SUGGESTIONS_VIEW_CONTROLLER_H_ #endif // IOS_CHROME_BROWSER_UI_CONTENT_SUGGESTIONS_CONTENT_SUGGESTIONS_VIEW_CONTROLLER_H_

@@ -55,7 +55,6 @@
#import "ios/chrome/browser/ui/content_suggestions/tab_resumption/tab_resumption_view.h" #import "ios/chrome/browser/ui/content_suggestions/tab_resumption/tab_resumption_view.h"
#import "ios/chrome/browser/ui/content_suggestions/tab_resumption/tab_resumption_view_delegate.h" #import "ios/chrome/browser/ui/content_suggestions/tab_resumption/tab_resumption_view_delegate.h"
#import "ios/chrome/browser/ui/ntp/new_tab_page_header_constants.h" #import "ios/chrome/browser/ui/ntp/new_tab_page_header_constants.h"
#import "ios/chrome/browser/ui/ntp/new_tab_page_view_delegate.h"
#import "ios/chrome/browser/ui/start_surface/start_surface_features.h" #import "ios/chrome/browser/ui/start_surface/start_surface_features.h"
#import "ios/chrome/browser/ui/toolbar/public/toolbar_utils.h" #import "ios/chrome/browser/ui/toolbar/public/toolbar_utils.h"
#import "ios/chrome/browser/url_loading/model/url_loading_browser_agent.h" #import "ios/chrome/browser/url_loading/model/url_loading_browser_agent.h"
@@ -81,6 +80,14 @@ const float kMagicStackMinimumPaginationScrollVelocity = 0.2f;
// The spacing between modules in the Magic Stack. // The spacing between modules in the Magic Stack.
const float kMagicStackSpacing = 10.0f; const float kMagicStackSpacing = 10.0f;
// The reduction in width of MagicStack modules from NTP modules. This
// reduction allows the next module to peek in from the side.
const float kMagicStackPeekInset = 16.0f;
// Controls the size of the view that masks/clips the MagicStack on wide
// screens. Defined as a multiplier of the NTP module width.
const CGFloat kMagicStackMaskWidth = 1.05;
// The corner radius of the Magic Stack. // The corner radius of the Magic Stack.
const float kMagicStackCornerRadius = 16.0f; const float kMagicStackCornerRadius = 16.0f;
@@ -165,13 +172,13 @@ const base::TimeDelta kSetUpListHideAnimationDuration = base::Milliseconds(250);
@end @end
@implementation ContentSuggestionsViewController { @implementation ContentSuggestionsViewController {
// Width Anchor of the Return To Recent Tab tile.
NSLayoutConstraint* _returnToRecentTabWidthAnchor;
UIScrollView* _magicStackScrollView; UIScrollView* _magicStackScrollView;
UIStackView* _magicStack; UIStackView* _magicStack;
// View that masks the MagicStack in landscape, allowing the next module to
// peek in.
UIView* _magicStackMask;
BOOL _magicStackRankReceived; BOOL _magicStackRankReceived;
NSMutableArray<NSNumber*>* _magicStackModuleOrder; NSMutableArray<NSNumber*>* _magicStackModuleOrder;
NSLayoutConstraint* _magicStackScrollViewWidthAnchor;
NSArray<SetUpListItemViewData*>* _savedSetUpListItems; NSArray<SetUpListItemViewData*>* _savedSetUpListItems;
SetUpListItemView* _setUpListSyncItemView; SetUpListItemView* _setUpListSyncItemView;
SetUpListItemView* _setUpListDefaultBrowserItemView; SetUpListItemView* _setUpListDefaultBrowserItemView;
@@ -186,6 +193,8 @@ const base::TimeDelta kSetUpListHideAnimationDuration = base::Milliseconds(250);
NSMutableArray<MagicStackModuleContainer*>* _parcelTrackingModuleContainers; NSMutableArray<MagicStackModuleContainer*>* _parcelTrackingModuleContainers;
NSLayoutConstraint* _mostVisitedTilesStackviewHeightAnchor; NSLayoutConstraint* _mostVisitedTilesStackviewHeightAnchor;
NSLayoutConstraint* _shortcutsStackviewHeightAnchor; NSLayoutConstraint* _shortcutsStackviewHeightAnchor;
// The most recently selected MagicStack module's page index.
NSUInteger _magicStackPage;
} }
- (instancetype)init { - (instancetype)init {
@@ -298,6 +307,13 @@ const base::TimeDelta kSetUpListHideAnimationDuration = base::Milliseconds(250);
[self.audience viewWillDisappear]; [self.audience viewWillDisappear];
} }
- (void)moduleWidthDidUpdate {
if (_magicStackScrollView) {
_magicStackMask.clipsToBounds = [self shouldMaskMagicStack];
[self snapToNearestMagicStackModule];
}
}
#pragma mark - UIGestureRecognizerDelegate #pragma mark - UIGestureRecognizerDelegate
- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
@@ -952,24 +968,6 @@ const base::TimeDelta kSetUpListHideAnimationDuration = base::Milliseconds(250);
_shortcutsStackviewHeightAnchor.constant = height; _shortcutsStackviewHeightAnchor.constant = height;
} }
if (content_suggestions::ShouldShowWiderMagicStackLayer(self.traitCollection,
self.view.window)) {
if (_returnToRecentTabWidthAnchor) {
// Match Module width when Magic Stack is enabled.
_returnToRecentTabWidthAnchor.constant = kMagicStackWideWidth;
}
_magicStackScrollView.clipsToBounds = YES;
_magicStackScrollViewWidthAnchor.constant = kMagicStackWideWidth;
} else {
if (_returnToRecentTabWidthAnchor) {
_returnToRecentTabWidthAnchor.constant =
content_suggestions::SearchFieldWidth(self.view.bounds.size.width,
self.traitCollection);
}
_magicStackScrollView.clipsToBounds = NO;
_magicStackScrollViewWidthAnchor.constant = [MagicStackModuleContainer
moduleWidthForHorizontalTraitCollection:self.traitCollection];
}
} }
#pragma mark - UIScrollViewDelegate #pragma mark - UIScrollViewDelegate
@@ -1057,21 +1055,9 @@ const base::TimeDelta kSetUpListHideAnimationDuration = base::Milliseconds(250);
} }
- (void)layoutReturnToRecentTabTile { - (void)layoutReturnToRecentTabTile {
CGFloat cardWidth = content_suggestions::SearchFieldWidth( [_returnToRecentTabTile.widthAnchor
self.view.bounds.size.width, self.traitCollection); constraintEqualToAnchor:self.view.widthAnchor]
if (IsMagicStackEnabled() && .active = YES;
content_suggestions::ShouldShowWiderMagicStackLayer(self.traitCollection,
self.view.window)) {
// Match Module width when Magic Stack is enabled.
cardWidth = kMagicStackWideWidth;
}
_returnToRecentTabWidthAnchor =
[_returnToRecentTabTile.widthAnchor constraintEqualToConstant:cardWidth];
[NSLayoutConstraint activateConstraints:@[
_returnToRecentTabWidthAnchor,
[_returnToRecentTabTile.heightAnchor
constraintEqualToConstant:ReturnToRecentTabHeight()]
]];
} }
- (void)createAndInsertMostVisitedModule { - (void)createAndInsertMostVisitedModule {
@@ -1105,21 +1091,12 @@ const base::TimeDelta kSetUpListHideAnimationDuration = base::Milliseconds(250);
atIndex:insertionIndex]; atIndex:insertionIndex];
[self.verticalStackView setCustomSpacing:kMostVisitedBottomMargin [self.verticalStackView setCustomSpacing:kMostVisitedBottomMargin
afterView:self.mostVisitedModuleContainer]; afterView:self.mostVisitedModuleContainer];
// When feed containment is enabled, the module on top of the magic stack [NSLayoutConstraint activateConstraints:@[
// should match the width of the feed module. [self.mostVisitedModuleContainer.widthAnchor
if (IsFeedContainmentEnabled()) { constraintEqualToAnchor:self.view.widthAnchor],
[NSLayoutConstraint activateConstraints:@[ [self.mostVisitedModuleContainer.centerXAnchor
[self.mostVisitedModuleContainer.widthAnchor constraintEqualToAnchor:self.view.centerXAnchor],
constraintEqualToConstant:self.view.frame.size.width - ]];
[self.NTPViewDelegate
homeModulePadding]],
[self.mostVisitedModuleContainer.centerXAnchor
constraintEqualToAnchor:self.view.centerXAnchor],
[self.mostVisitedStackView.centerXAnchor
constraintEqualToAnchor:self.mostVisitedModuleContainer
.centerXAnchor],
]];
}
} }
} else { } else {
[self.verticalStackView insertArrangedSubview:self.mostVisitedStackView [self.verticalStackView insertArrangedSubview:self.mostVisitedStackView
@@ -1219,17 +1196,33 @@ const base::TimeDelta kSetUpListHideAnimationDuration = base::Milliseconds(250);
- (void)createMagicStack { - (void)createMagicStack {
_magicStackScrollView = [[UIScrollView alloc] init]; _magicStackScrollView = [[UIScrollView alloc] init];
[_magicStackScrollView setShowsHorizontalScrollIndicator:NO]; [_magicStackScrollView setShowsHorizontalScrollIndicator:NO];
_magicStackScrollView.clipsToBounds = _magicStackScrollView.clipsToBounds = NO;
content_suggestions::ShouldShowWiderMagicStackLayer(self.traitCollection,
self.view.window);
_magicStackScrollView.layer.cornerRadius = kMagicStackCornerRadius;
_magicStackScrollView.delegate = self; _magicStackScrollView.delegate = self;
_magicStackScrollView.decelerationRate = UIScrollViewDecelerationRateFast; _magicStackScrollView.decelerationRate = UIScrollViewDecelerationRateFast;
_magicStackScrollView.accessibilityIdentifier = _magicStackScrollView.accessibilityIdentifier =
kMagicStackScrollViewAccessibilityIdentifier; kMagicStackScrollViewAccessibilityIdentifier;
// Allow MagicStack modules to peek in from the sides, when scrolled.
_magicStackMask = [[UIView alloc] init];
[_magicStackMask addSubview:_magicStackScrollView];
_magicStackMask.clipsToBounds = [self shouldMaskMagicStack];
_magicStackMask.layer.cornerRadius = kMagicStackCornerRadius;
_magicStackMask.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:@[
[_magicStackMask.topAnchor
constraintEqualToAnchor:_magicStackScrollView.topAnchor],
[_magicStackMask.bottomAnchor
constraintEqualToAnchor:_magicStackScrollView.bottomAnchor],
[_magicStackMask.centerXAnchor
constraintEqualToAnchor:_magicStackScrollView.centerXAnchor],
[_magicStackMask.widthAnchor
constraintEqualToAnchor:_magicStackScrollView.widthAnchor
multiplier:kMagicStackMaskWidth],
]];
[self.verticalStackView addSubview:_magicStackMask];
[self addUIElement:_magicStackScrollView [self addUIElement:_magicStackScrollView
withCustomBottomSpacing:kMostVisitedBottomMargin]; withCustomBottomSpacing:kMostVisitedBottomMargin];
_magicStackPage = 0;
_magicStack = [[UIStackView alloc] init]; _magicStack = [[UIStackView alloc] init];
_magicStack.translatesAutoresizingMaskIntoConstraints = NO; _magicStack.translatesAutoresizingMaskIntoConstraints = NO;
_magicStack.axis = UILayoutConstraintAxisHorizontal; _magicStack.axis = UILayoutConstraintAxisHorizontal;
@@ -1251,30 +1244,14 @@ const base::TimeDelta kSetUpListHideAnimationDuration = base::Milliseconds(250);
[_magicStack.heightAnchor [_magicStack.heightAnchor
constraintEqualToAnchor:_magicStackScrollView.heightAnchor], constraintEqualToAnchor:_magicStackScrollView.heightAnchor],
]]; ]];
// Define width of ScrollView. // Define width of ScrollView.
// With feed containment enabled, the magic stack should be left aligned with [NSLayoutConstraint activateConstraints:@[
// the other modules. [_magicStackScrollView.leadingAnchor
if (IsFeedContainmentEnabled()) { constraintEqualToAnchor:self.view.leadingAnchor],
[NSLayoutConstraint activateConstraints:@[ [_magicStackScrollView.trailingAnchor
[_magicStackScrollView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
constraintEqualToAnchor:self.view.trailingAnchor ]];
constant:-([self.NTPViewDelegate homeModulePadding] /
2)],
]];
} else {
CGFloat width = [MagicStackModuleContainer
moduleWidthForHorizontalTraitCollection:self.traitCollection];
// Magic Stack has a wider width for wider screens so that clipToBounds can
// be YES with a peeking module still visible.
if (content_suggestions::ShouldShowWiderMagicStackLayer(
self.traitCollection, self.view.window)) {
width = kMagicStackWideWidth;
}
_magicStackScrollViewWidthAnchor =
[_magicStackScrollView.widthAnchor constraintEqualToConstant:width];
[NSLayoutConstraint
activateConstraints:@[ _magicStackScrollViewWidthAnchor ]];
}
} }
// Resets and fills the Magic Stack with modules using `_magicStackModuleOrder`. // Resets and fills the Magic Stack with modules using `_magicStackModuleOrder`.
@@ -1355,6 +1332,7 @@ const base::TimeDelta kSetUpListHideAnimationDuration = base::Milliseconds(250);
} }
if (moduleContainer) { if (moduleContainer) {
[_magicStack addArrangedSubview:moduleContainer]; [_magicStack addArrangedSubview:moduleContainer];
[self addWidthConstraintToMagicStackModule:moduleContainer];
[self logTopModuleImpressionForType:moduleContainer.type]; [self logTopModuleImpressionForType:moduleContainer.type];
} }
} }
@@ -1456,14 +1434,19 @@ const base::TimeDelta kSetUpListHideAnimationDuration = base::Milliseconds(250);
// first found module with a rank higher than `moduleToInsert` or just before // first found module with a rank higher than `moduleToInsert` or just before
// the last arrangedSubview (e.g. edit button). // the last arrangedSubview (e.g. edit button).
[_magicStack insertArrangedSubview:moduleToInsert atIndex:magicStackIndex]; [_magicStack insertArrangedSubview:moduleToInsert atIndex:magicStackIndex];
[self addWidthConstraintToMagicStackModule:moduleToInsert];
}
// Returns the current width of MagicStack modules.
- (CGFloat)magicStackModuleWidth {
return _magicStackScrollView.frame.size.width - kMagicStackPeekInset;
} }
// Returns the `ContentSuggestionsModuleType` type of the module being currently // Returns the `ContentSuggestionsModuleType` type of the module being currently
// shown in the Magic Stack. // shown in the Magic Stack.
- (ContentSuggestionsModuleType)currentlyShownModule { - (ContentSuggestionsModuleType)currentlyShownModule {
CGFloat offset = _magicStackScrollView.contentOffset.x; CGFloat offset = _magicStackScrollView.contentOffset.x;
CGFloat moduleWidth = [MagicStackModuleContainer CGFloat moduleWidth = [self magicStackModuleWidth];
moduleWidthForHorizontalTraitCollection:self.traitCollection];
NSUInteger moduleCount = [_magicStackModuleOrder count]; NSUInteger moduleCount = [_magicStackModuleOrder count];
// Find closest page to the current scroll offset. // Find closest page to the current scroll offset.
CGFloat closestPage = roundf(offset / moduleWidth); CGFloat closestPage = roundf(offset / moduleWidth);
@@ -1495,6 +1478,7 @@ const base::TimeDelta kSetUpListHideAnimationDuration = base::Milliseconds(250);
} }
__weak __typeof(self) weakSelf = self; __weak __typeof(self) weakSelf = self;
CGFloat moduleWidth = [self magicStackModuleWidth];
ProceduralBlock removeRemainingModules = ^{ ProceduralBlock removeRemainingModules = ^{
__typeof(self) strongSelf = weakSelf; __typeof(self) strongSelf = weakSelf;
if (!strongSelf) { if (!strongSelf) {
@@ -1512,8 +1496,6 @@ const base::TimeDelta kSetUpListHideAnimationDuration = base::Milliseconds(250);
} }
// Compensate for removed module count so the currently visible module is // Compensate for removed module count so the currently visible module is
// still displayed. // still displayed.
CGFloat moduleWidth = [MagicStackModuleContainer
moduleWidthForHorizontalTraitCollection:self.traitCollection];
CGFloat offsetRemoved = (removedModuleCount)*moduleWidth + CGFloat offsetRemoved = (removedModuleCount)*moduleWidth +
((removedModuleCount)*kMagicStackSpacing); ((removedModuleCount)*kMagicStackSpacing);
[strongSelf->_magicStackScrollView [strongSelf->_magicStackScrollView
@@ -1565,6 +1547,7 @@ const base::TimeDelta kSetUpListHideAnimationDuration = base::Milliseconds(250);
completion:(ProceduralBlock)completion { completion:(ProceduralBlock)completion {
UIView* moduleToHide = [_magicStack arrangedSubviews][index]; UIView* moduleToHide = [_magicStack arrangedSubviews][index];
__weak __typeof(self) weakSelf = self; __weak __typeof(self) weakSelf = self;
__weak __typeof(_magicStack) weakMagicStack = _magicStack;
ProceduralBlock animateInNewModule = ^{ ProceduralBlock animateInNewModule = ^{
[UIView animateWithDuration:0.5 [UIView animateWithDuration:0.5
@@ -1610,19 +1593,38 @@ const base::TimeDelta kSetUpListHideAnimationDuration = base::Milliseconds(250);
// Remove module to hide, add the new module with an initial position to // Remove module to hide, add the new module with an initial position to
// the left and hidden from view in preparation for a fade in. // the left and hidden from view in preparation for a fade in.
newModule.alpha = 0; newModule.alpha = 0;
[strongSelf->_magicStack removeArrangedSubview:moduleToHide]; [weakMagicStack removeArrangedSubview:moduleToHide];
[strongSelf->_magicStack insertArrangedSubview:newModule atIndex:index]; [weakMagicStack insertArrangedSubview:newModule atIndex:index];
[strongSelf addWidthConstraintToMagicStackModule:newModule];
newModule.transform = CGAffineTransformTranslate( newModule.transform = CGAffineTransformTranslate(
CGAffineTransformIdentity, CGAffineTransformIdentity,
-kMagicStackReplaceModuleFadeAnimationDistance, 0); -kMagicStackReplaceModuleFadeAnimationDistance, 0);
[moduleToHide removeFromSuperview]; [moduleToHide removeFromSuperview];
[strongSelf->_magicStack setNeedsLayout]; [weakMagicStack setNeedsLayout];
[strongSelf->_magicStack layoutIfNeeded]; [weakMagicStack layoutIfNeeded];
animateInNewModule(); animateInNewModule();
}]; }];
} }
// Returns the extra offset needed to have a MagicStack module be left, center,
// or right aligned depending on whether the module is first, in the middle, or
// last.
- (CGFloat)peekOffsetForMagicStackPage:(NSUInteger)page {
if (page == 0) {
// The first module should be leading aligned so that the next module peeks
// in from the trailing edge.
return 0;
} else if (page == [_magicStackModuleOrder count] - 1) {
// The last module should be trailing aligned so the previous module peeks.
return kMagicStackPeekInset;
} else {
// Modules in the middle should show peek on both sides.
return kMagicStackPeekInset / 2;
}
}
// Determines the final page offset given the scroll `offset` and the `velocity` // Determines the final page offset given the scroll `offset` and the `velocity`
// scroll. If the drag is slow enough, then the closest page is the final state. // scroll. If the drag is slow enough, then the closest page is the final state.
// If the drag is in the negative direction, then go to the page previous to the // If the drag is in the negative direction, then go to the page previous to the
@@ -1630,23 +1632,51 @@ const base::TimeDelta kSetUpListHideAnimationDuration = base::Milliseconds(250);
// the page after the closest current page. // the page after the closest current page.
- (CGFloat)getNextPageOffsetForOffset:(CGFloat)offset - (CGFloat)getNextPageOffsetForOffset:(CGFloat)offset
velocity:(CGFloat)velocity { velocity:(CGFloat)velocity {
CGFloat moduleWidth = [MagicStackModuleContainer CGFloat moduleWidth = [self magicStackModuleWidth];
moduleWidthForHorizontalTraitCollection:self.traitCollection];
NSUInteger moduleCount = [_magicStackModuleOrder count]; NSUInteger moduleCount = [_magicStackModuleOrder count];
// Find closest page to the current scroll offset. // Find closest page to the current scroll offset.
CGFloat closestPage = roundf(offset / moduleWidth); CGFloat closestPage = roundf(offset / moduleWidth);
closestPage = fminf(closestPage, moduleCount); closestPage = fminf(closestPage, moduleCount);
if (fabs(velocity) < kMagicStackMinimumPaginationScrollVelocity) { if (fabs(velocity) >= kMagicStackMinimumPaginationScrollVelocity) {
return closestPage * moduleWidth + (closestPage * 10); if (velocity < 0) {
closestPage--;
} else {
closestPage++;
}
} }
if (velocity < 0) { _magicStackPage = closestPage;
return (closestPage - 1) * moduleWidth + return _magicStackPage * (moduleWidth + kMagicStackSpacing) -
((closestPage - 1) * kMagicStackSpacing); [self peekOffsetForMagicStackPage:_magicStackPage];
}
return (closestPage + 1) * moduleWidth +
((closestPage + 1) * kMagicStackSpacing);
} }
// Snaps the MagicStack ScrollView's contentOffset to the nearest module. Can
// be used after the width of the MagicStack changes to ensure that it doesn't
// end up scrolled to the middle of a module.
- (void)snapToNearestMagicStackModule {
CGFloat moduleWidth = [self magicStackModuleWidth];
CGPoint offset = _magicStackScrollView.contentOffset;
offset.x = _magicStackPage * (moduleWidth + kMagicStackSpacing) -
[self peekOffsetForMagicStackPage:_magicStackPage];
_magicStackScrollView.contentOffset = offset;
}
// Returns YES if the MagicStack should be masked so that modules only peek in
// from the sides. This is needed in landscape and on iPads.
- (BOOL)shouldMaskMagicStack {
return self.traitCollection.horizontalSizeClass ==
UIUserInterfaceSizeClassRegular ||
IsLandscape(self.view.window);
}
// Adds a layout constraint on the width of the given MagicStack module
// `container`.
- (void)addWidthConstraintToMagicStackModule:
(MagicStackModuleContainer*)container {
[container.widthAnchor
constraintEqualToAnchor:_magicStackScrollView.widthAnchor
constant:-kMagicStackPeekInset]
.active = YES;
}
@end @end

@@ -1209,12 +1209,14 @@ id<GREYMatcher> mostlyNotVisible() {
[[AppLaunchManager sharedManager] ensureAppLaunchedWithConfiguration:config]; [[AppLaunchManager sharedManager] ensureAppLaunchedWithConfiguration:config];
if (![ChromeEarlGrey isIPadIdiom]) { id<GREYMatcher> magicStackScrollView =
[EarlGrey rotateDeviceToOrientation:UIDeviceOrientationLandscapeLeft grey_accessibilityID(kMagicStackScrollViewAccessibilityIdentifier);
error:nil];
[[EarlGrey selectElementWithMatcher:chrome_test_util::NTPCollectionView()] // Scroll down to find the MagicStack.
performAction:grey_scrollInDirection(kGREYDirectionDown, 100)]; [[[EarlGrey selectElementWithMatcher:magicStackScrollView]
} usingSearchAction:grey_scrollInDirection(kGREYDirectionDown, 100.0f)
onElementWithMatcher:chrome_test_util::NTPCollectionView()]
assertWithMatcher:grey_notNil()];
// Verify Most Visited Tiles module title is visible. // Verify Most Visited Tiles module title is visible.
[[EarlGrey selectElementWithMatcher: [[EarlGrey selectElementWithMatcher:
@@ -1223,10 +1225,11 @@ id<GREYMatcher> mostlyNotVisible() {
assertWithMatcher:grey_sufficientlyVisible()]; assertWithMatcher:grey_sufficientlyVisible()];
// Swipe to next module // Swipe to next module
[[EarlGrey // Need to swipe at least half of the widest a module can be.
selectElementWithMatcher: CGFloat moduleSwipeAmount = 250;
grey_accessibilityID(kMagicStackScrollViewAccessibilityIdentifier)] [[EarlGrey selectElementWithMatcher:magicStackScrollView]
performAction:grey_scrollInDirection(kGREYDirectionRight, 343)]; performAction:GREYScrollInDirectionWithStartPoint(
kGREYDirectionRight, moduleSwipeAmount, 0.9, 0.5)];
// Verify Shortcuts module title is visible. // Verify Shortcuts module title is visible.
[[EarlGrey selectElementWithMatcher: [[EarlGrey selectElementWithMatcher:
@@ -1259,10 +1262,9 @@ id<GREYMatcher> mostlyNotVisible() {
assertWithMatcher:grey_sufficientlyVisible()]; assertWithMatcher:grey_sufficientlyVisible()];
// Swipe back to first module // Swipe back to first module
[[EarlGrey [[EarlGrey selectElementWithMatcher:magicStackScrollView]
selectElementWithMatcher: performAction:GREYScrollInDirectionWithStartPoint(
grey_accessibilityID(kMagicStackScrollViewAccessibilityIdentifier)] kGREYDirectionLeft, moduleSwipeAmount, 0.10, 0.5)];
performAction:grey_swipeFastInDirection(kGREYDirectionRight)];
// Verify Most Visited Tiles module title is visible. // Verify Most Visited Tiles module title is visible.
[[EarlGrey selectElementWithMatcher: [[EarlGrey selectElementWithMatcher:

@@ -195,7 +195,6 @@ source_set("ntp_internal") {
":feature_flags", ":feature_flags",
":logo", ":logo",
":ntp", ":ntp",
":ntp_ui_util",
"resources:fake_omnibox_background_color", "resources:fake_omnibox_background_color",
"resources:fake_omnibox_bottom_gradient_color", "resources:fake_omnibox_bottom_gradient_color",
"resources:fake_omnibox_solid_background_color", "resources:fake_omnibox_solid_background_color",
@@ -292,11 +291,6 @@ source_set("feed_menu") {
] ]
} }
source_set("ntp_ui_util") {
sources = [ "new_tab_page_view_delegate.h" ]
deps = []
}
source_set("unit_tests") { source_set("unit_tests") {
testonly = true testonly = true
sources = [ sources = [

@@ -699,7 +699,6 @@
self.contentSuggestionsCoordinator.NTPDelegate = self; self.contentSuggestionsCoordinator.NTPDelegate = self;
self.contentSuggestionsCoordinator.delegate = self; self.contentSuggestionsCoordinator.delegate = self;
self.contentSuggestionsCoordinator.NTPMetricsDelegate = self; self.contentSuggestionsCoordinator.NTPMetricsDelegate = self;
self.contentSuggestionsCoordinator.NTPViewDelegate = self.NTPViewController;
[self.contentSuggestionsCoordinator start]; [self.contentSuggestionsCoordinator start];
} }

@@ -9,7 +9,6 @@
#import "ios/chrome/browser/ui/ntp/new_tab_page_consumer.h" #import "ios/chrome/browser/ui/ntp/new_tab_page_consumer.h"
#import "ios/chrome/browser/ui/ntp/new_tab_page_header_view_controller_delegate.h" #import "ios/chrome/browser/ui/ntp/new_tab_page_header_view_controller_delegate.h"
#import "ios/chrome/browser/ui/ntp/new_tab_page_view_delegate.h"
@class BubblePresenter; @class BubblePresenter;
@class ContentSuggestionsViewController; @class ContentSuggestionsViewController;
@@ -26,7 +25,6 @@
@interface NewTabPageViewController @interface NewTabPageViewController
: UIViewController <NewTabPageConsumer, : UIViewController <NewTabPageConsumer,
NewTabPageHeaderViewControllerDelegate, NewTabPageHeaderViewControllerDelegate,
NewTabPageViewDelegate,
UIScrollViewDelegate> UIScrollViewDelegate>
// View controller wrapping the feed. // View controller wrapping the feed.
@@ -80,6 +78,9 @@
// Whether or not the fake omnibox is pinned to the top of the NTP. // Whether or not the fake omnibox is pinned to the top of the NTP.
@property(nonatomic, readonly) BOOL isFakeboxPinned; @property(nonatomic, readonly) BOOL isFakeboxPinned;
// Layout guide for NTP modules.
@property(nonatomic, readonly) UILayoutGuide* moduleLayoutGuide;
// Initializes the new tab page view controller. // Initializes the new tab page view controller.
- (instancetype)init NS_DESIGNATED_INITIALIZER; - (instancetype)init NS_DESIGNATED_INITIALIZER;

@@ -44,6 +44,8 @@ const CGFloat kShiftTilesDownAnimationDuration = 0.2;
const CGFloat kShiftTilesUpAnimationDuration = 0.1; const CGFloat kShiftTilesUpAnimationDuration = 0.1;
// The minimum height of the feed container. // The minimum height of the feed container.
const CGFloat kFeedContainerMinimumHeight = 1000; const CGFloat kFeedContainerMinimumHeight = 1000;
// The width of NTP modules, as a multiplier of the view width.
const CGFloat kModuleWidth = 0.92;
} // namespace } // namespace
@interface NewTabPageViewController () <UICollectionViewDelegate, @interface NewTabPageViewController () <UICollectionViewDelegate,
@@ -158,6 +160,10 @@ const CGFloat kFeedContainerMinimumHeight = 1000;
// YES if the view is in the process of appearing, but viewDidAppear hasn't // YES if the view is in the process of appearing, but viewDidAppear hasn't
// finished yet. // finished yet.
BOOL _appearing; BOOL _appearing;
// Layout Guide for NTP modules.
UILayoutGuide* _moduleLayoutGuide;
// Constraint controlling the width of modules on the NTP.
NSLayoutConstraint* _moduleWidth;
} }
- (instancetype)init { - (instancetype)init {
@@ -326,6 +332,7 @@ const CGFloat kFeedContainerMinimumHeight = 1000;
void (^alongsideBlock)(id<UIViewControllerTransitionCoordinatorContext>) = ^( void (^alongsideBlock)(id<UIViewControllerTransitionCoordinatorContext>) = ^(
id<UIViewControllerTransitionCoordinatorContext> context) { id<UIViewControllerTransitionCoordinatorContext> context) {
[self updateModuleWidth];
[weakSelf handleStickyElementsForScrollPosition:[weakSelf scrollPosition] [weakSelf handleStickyElementsForScrollPosition:[weakSelf scrollPosition]
force:YES]; force:YES];
@@ -693,6 +700,23 @@ const CGFloat kFeedContainerMinimumHeight = 1000;
[[NSNotificationCenter defaultCenter] removeObserver:self]; [[NSNotificationCenter defaultCenter] removeObserver:self];
} }
- (UILayoutGuide*)moduleLayoutGuide {
if (!_moduleLayoutGuide) {
_moduleLayoutGuide = [[UILayoutGuide alloc] init];
UIView* view = self.view;
[view addLayoutGuide:_moduleLayoutGuide];
[self updateModuleWidth];
[NSLayoutConstraint activateConstraints:@[
[_moduleLayoutGuide.centerXAnchor
constraintEqualToAnchor:view.centerXAnchor],
[_moduleLayoutGuide.topAnchor constraintEqualToAnchor:view.topAnchor],
[_moduleLayoutGuide.bottomAnchor
constraintEqualToAnchor:view.bottomAnchor],
]];
}
return _moduleLayoutGuide;
}
#pragma mark - NewTabPageConsumer #pragma mark - NewTabPageConsumer
- (void)restoreScrollPosition:(CGFloat)scrollPosition { - (void)restoreScrollPosition:(CGFloat)scrollPosition {
@@ -1001,19 +1025,6 @@ const CGFloat kFeedContainerMinimumHeight = 1000;
[self.animator startAnimation]; [self.animator startAnimation];
} }
#pragma mark - NewTabPageViewDelegate
- (CGFloat)homeModulePadding {
if (!IsFeedContainmentEnabled()) {
return 0;
}
int screenWidth = self.view.frame.size.width;
int minPadding = HomeModuleMinimumPadding();
return minPadding - std::clamp(static_cast<int>(screenWidth -
kDiscoverFeedContentMaxWidth),
0, minPadding);
}
#pragma mark - Private #pragma mark - Private
// Returns YES if scroll should be skipped when focusing the omnibox. // Returns YES if scroll should be skipped when focusing the omnibox.
@@ -1439,46 +1450,35 @@ const CGFloat kFeedContainerMinimumHeight = 1000;
if (self.feedHeaderViewController) { if (self.feedHeaderViewController) {
[self cleanUpCollectionViewConstraints]; [self cleanUpCollectionViewConstraints];
// Apply parent collection view constraints.
[NSLayoutConstraint activateConstraints:@[
[self.collectionView.centerXAnchor
constraintEqualToAnchor:[self containerView].centerXAnchor],
[self.collectionView.widthAnchor
constraintLessThanOrEqualToConstant:kDiscoverFeedContentMaxWidth],
]];
// Apply feed header constraints.
if (IsFeedContainmentEnabled()) { if (IsFeedContainmentEnabled()) {
[NSLayoutConstraint activateConstraints:@[ [self.collectionView.widthAnchor
[self.feedHeaderViewController.view.centerXAnchor constraintEqualToAnchor:self.moduleLayoutGuide.widthAnchor]
constraintEqualToAnchor:self.collectionView.centerXAnchor], .active = YES;
[self.feedHeaderViewController.view.widthAnchor
constraintEqualToAnchor:self.collectionView.widthAnchor
constant:-[self homeModulePadding]],
]];
} else { } else {
NSLayoutConstraint* headerWidthConstraint = [self.collectionView.widthAnchor
[self.feedHeaderViewController.view.widthAnchor constraintLessThanOrEqualToConstant:kDiscoverFeedContentMaxWidth]
constraintEqualToAnchor:self.collectionView.widthAnchor]; .active = YES;
headerWidthConstraint.priority = UILayoutPriorityDefaultHigh;
[NSLayoutConstraint activateConstraints:@[
[self.feedHeaderViewController.view.centerXAnchor
constraintEqualToAnchor:self.collectionView.centerXAnchor],
[self.feedHeaderViewController.view.widthAnchor
constraintLessThanOrEqualToConstant:kDiscoverFeedContentMaxWidth],
headerWidthConstraint,
]];
} }
[NSLayoutConstraint activateConstraints:@[
// Apply parent collection view constraints.
[self.collectionView.centerXAnchor
constraintEqualToAnchor:self.moduleLayoutGuide.centerXAnchor],
// Apply feed header constraints.
[self.feedHeaderViewController.view.centerXAnchor
constraintEqualToAnchor:self.collectionView.centerXAnchor],
[self.feedHeaderViewController.view.widthAnchor
constraintEqualToAnchor:self.collectionView.widthAnchor],
]];
[self setInitialFeedHeaderConstraints]; [self setInitialFeedHeaderConstraints];
if (self.feedTopSectionViewController) { if (self.feedTopSectionViewController) {
[NSLayoutConstraint activateConstraints:@[ [NSLayoutConstraint activateConstraints:@[
[self.feedTopSectionViewController.view.centerXAnchor [self.feedTopSectionViewController.view.centerXAnchor
constraintEqualToAnchor:self.collectionView.centerXAnchor], constraintEqualToAnchor:self.collectionView.centerXAnchor],
[self.feedTopSectionViewController.view.widthAnchor [self.feedTopSectionViewController.view.widthAnchor
constraintEqualToAnchor:self.collectionView.widthAnchor constraintEqualToAnchor:self.collectionView.widthAnchor],
constant:-[self homeModulePadding]],
[self.feedTopSectionViewController.view.topAnchor [self.feedTopSectionViewController.view.topAnchor
constraintEqualToAnchor:self.feedHeaderViewController.view constraintEqualToAnchor:self.feedHeaderViewController.view
.bottomAnchor], .bottomAnchor],
@@ -1498,10 +1498,9 @@ const CGFloat kFeedContainerMinimumHeight = 1000;
CHECK(IsFeedContainmentEnabled()); CHECK(IsFeedContainmentEnabled());
[NSLayoutConstraint activateConstraints:@[ [NSLayoutConstraint activateConstraints:@[
[_feedContainer.widthAnchor [_feedContainer.widthAnchor
constraintEqualToAnchor:self.collectionView.widthAnchor constraintEqualToAnchor:self.moduleLayoutGuide.widthAnchor],
constant:-[self homeModulePadding]],
[_feedContainer.centerXAnchor [_feedContainer.centerXAnchor
constraintEqualToAnchor:self.collectionView.centerXAnchor], constraintEqualToAnchor:self.moduleLayoutGuide.centerXAnchor],
[_feedContainer.topAnchor [_feedContainer.topAnchor
constraintEqualToAnchor:self.feedHeaderViewController.view.topAnchor], constraintEqualToAnchor:self.feedHeaderViewController.view.topAnchor],
]]; ]];
@@ -1513,27 +1512,12 @@ const CGFloat kFeedContainerMinimumHeight = 1000;
constraintEqualToAnchor:self.headerViewController.view.leadingAnchor], constraintEqualToAnchor:self.headerViewController.view.leadingAnchor],
[[self containerView].safeAreaLayoutGuide.trailingAnchor [[self containerView].safeAreaLayoutGuide.trailingAnchor
constraintEqualToAnchor:self.headerViewController.view.trailingAnchor], constraintEqualToAnchor:self.headerViewController.view.trailingAnchor],
[contentSuggestionsView.leadingAnchor
constraintEqualToAnchor:self.moduleLayoutGuide.leadingAnchor],
[contentSuggestionsView.trailingAnchor
constraintEqualToAnchor:self.moduleLayoutGuide.trailingAnchor],
]]; ]];
[self setInitialFakeOmniboxConstraints]; [self setInitialFakeOmniboxConstraints];
if (IsFeedContainmentEnabled()) {
// This should be an objective improvement since it prevents the width of
// the Content Suggestions from surpassing their parent, but the flag will
// guard the change for now to be safe.
[NSLayoutConstraint activateConstraints:@[
[contentSuggestionsView.safeAreaLayoutGuide.leadingAnchor
constraintEqualToAnchor:self.collectionView.leadingAnchor],
[contentSuggestionsView.safeAreaLayoutGuide.trailingAnchor
constraintEqualToAnchor:self.collectionView.trailingAnchor],
]];
} else {
[NSLayoutConstraint activateConstraints:@[
[[self containerView].safeAreaLayoutGuide.leadingAnchor
constraintEqualToAnchor:contentSuggestionsView.leadingAnchor],
[[self containerView].safeAreaLayoutGuide.trailingAnchor
constraintEqualToAnchor:contentSuggestionsView.trailingAnchor],
]];
}
} }
// Sets minimum height for the NTP collection view, allowing it to scroll enough // Sets minimum height for the NTP collection view, allowing it to scroll enough
@@ -1643,6 +1627,27 @@ const CGFloat kFeedContainerMinimumHeight = 1000;
self.feedContainerHeightConstraint.active = YES; self.feedContainerHeightConstraint.active = YES;
} }
// Updates the width constraint of `moduleLayoutGuide`.
- (void)updateModuleWidth {
BOOL existingConstraintActive = _moduleWidth.active;
_moduleWidth.active = NO;
CGFloat width;
if (IsFeedContainmentEnabled()) {
width = MIN(self.view.frame.size.width * kModuleWidth,
kDiscoverFeedContentMaxWidth);
} else {
width = content_suggestions::SearchFieldWidth(self.view.frame.size.width,
self.traitCollection);
}
_moduleWidth =
[self.moduleLayoutGuide.widthAnchor constraintEqualToConstant:width];
_moduleWidth.active = YES;
if (existingConstraintActive) {
[self.view layoutIfNeeded];
[self.contentSuggestionsViewController moduleWidthDidUpdate];
}
}
#pragma mark - Helpers #pragma mark - Helpers
- (UIViewController*)contentSuggestionsViewController { - (UIViewController*)contentSuggestionsViewController {

@@ -1,18 +0,0 @@
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef IOS_CHROME_BROWSER_UI_NTP_NEW_TAB_PAGE_VIEW_DELEGATE_H_
#define IOS_CHROME_BROWSER_UI_NTP_NEW_TAB_PAGE_VIEW_DELEGATE_H_
// Delegate for NTP view information to be passed to the Content Suggestions.
@protocol NewTabPageViewDelegate
// Returns the necessary padding between the Home modules and the sides of the
// screen. This can range anywhere between 0 and `HomeModuleMinimumPadding()`,
// depending on the screen size.
- (CGFloat)homeModulePadding;
@end
#endif // IOS_CHROME_BROWSER_UI_NTP_NEW_TAB_PAGE_VIEW_DELEGATE_H_