Skip to content

Commit

Permalink
[Tabs] Adds safe area inset (#7753)
Browse files Browse the repository at this point in the history
  • Loading branch information
leonmz authored and Robert Moore committed Jul 1, 2019
1 parent 029692a commit 079c42e
Show file tree
Hide file tree
Showing 7 changed files with 182 additions and 9 deletions.
73 changes: 64 additions & 9 deletions components/Tabs/src/TabBarView/MDCTabBarView.m
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ @interface MDCTabBarView ()
@property(nonnull, nonatomic, strong)
NSMutableDictionary<NSNumber *, UIColor *> *stateToImageTintColor;

/** The constraints for the justified layout style. */
@property(nullable, nonatomic) NSArray<NSLayoutConstraint *> *justifiedLayoutConstraints;

/** The constraints for the scrollable layout style. */
@property(nullable, nonatomic) NSArray<NSLayoutConstraint *> *scrollableLayoutConstraints;

/** The title font for bar items. */
@property(nonnull, nonatomic, strong) NSMutableDictionary<NSNumber *, UIFont *> *stateToTitleFont;
@end
Expand All @@ -73,6 +79,12 @@ - (instancetype)init {
_containerView = [[UIStackView alloc] init];
_containerView.axis = UILayoutConstraintAxisHorizontal;
_containerView.translatesAutoresizingMaskIntoConstraints = NO;

// By deafult, inset the content within the safe area. This is generally the desired behavior,
// but clients can override it if they want.
if (@available(iOS 11.0, *)) {
[super setContentInsetAdjustmentBehavior:UIScrollViewContentInsetAdjustmentAlways];
}
[self addSubview:_containerView];
}
return self;
Expand Down Expand Up @@ -120,6 +132,8 @@ - (void)setItems:(NSArray<UITabBarItem *> *)items {
itemView.iconImageView.image = item.image;
[itemView setContentCompressionResistancePriority:UILayoutPriorityRequired
forAxis:UILayoutConstraintAxisHorizontal];
[itemView setContentCompressionResistancePriority:UILayoutPriorityRequired
forAxis:UILayoutConstraintAxisVertical];
UITapGestureRecognizer *tapGesture =
[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didTapItemView:)];
[itemView addGestureRecognizer:tapGesture];
Expand Down Expand Up @@ -391,11 +405,22 @@ - (void)observeValueForKeyPath:(NSString *)keyPath
- (void)layoutSubviews {
[super layoutSubviews];

CGFloat availableWidth = CGRectGetWidth(self.bounds);
CGRect availableBounds = self.bounds;
if (@available(iOS 11.0, *)) {
availableBounds = UIEdgeInsetsInsetRect(availableBounds, self.safeAreaInsets);
}
CGFloat availableWidth = CGRectGetWidth(availableBounds);
CGFloat requiredWidth = [self justifiedWidth];
BOOL canBeJustified = availableWidth >= requiredWidth;
self.containerView.distribution = canBeJustified ? UIStackViewDistributionFillEqually
: UIStackViewDistributionFillProportionally;
if (canBeJustified) {
[NSLayoutConstraint deactivateConstraints:self.scrollableLayoutConstraints];
self.containerView.distribution = UIStackViewDistributionFillEqually;
[NSLayoutConstraint activateConstraints:self.justifiedLayoutConstraints];
} else {
[NSLayoutConstraint deactivateConstraints:self.justifiedLayoutConstraints];
self.containerView.distribution = UIStackViewDistributionFillProportionally;
[NSLayoutConstraint activateConstraints:self.scrollableLayoutConstraints];
}

if (!self.initialScrollDone) {
self.initialScrollDone = YES;
Expand Down Expand Up @@ -424,12 +449,42 @@ - (void)updateConstraints {
return;
}

[self.containerView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor].active = YES;
[self.containerView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor].active = YES;
[self.containerView.widthAnchor constraintGreaterThanOrEqualToAnchor:self.widthAnchor].active =
YES;
[self.containerView.topAnchor constraintEqualToAnchor:self.topAnchor].active = YES;
[self.containerView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor].active = YES;
if (@available(iOS 11.0, *)) {
self.justifiedLayoutConstraints = @[
[self.safeAreaLayoutGuide.leadingAnchor
constraintEqualToAnchor:self.containerView.leadingAnchor],
[self.safeAreaLayoutGuide.topAnchor constraintEqualToAnchor:self.containerView.topAnchor],
[self.safeAreaLayoutGuide.trailingAnchor
constraintEqualToAnchor:self.containerView.trailingAnchor],
[self.safeAreaLayoutGuide.bottomAnchor
constraintEqualToAnchor:self.containerView.bottomAnchor],
];
self.scrollableLayoutConstraints = @[
[self.contentLayoutGuide.topAnchor constraintEqualToAnchor:self.containerView.topAnchor],
[self.contentLayoutGuide.bottomAnchor
constraintEqualToAnchor:self.containerView.bottomAnchor],
[self.contentLayoutGuide.leadingAnchor
constraintEqualToAnchor:self.containerView.leadingAnchor],
[self.contentLayoutGuide.trailingAnchor
constraintEqualToAnchor:self.containerView.trailingAnchor],
[self.contentLayoutGuide.widthAnchor constraintEqualToAnchor:self.containerView.widthAnchor],
[self.contentLayoutGuide.heightAnchor
constraintEqualToAnchor:self.containerView.heightAnchor],
// Ensures items are never larger than the bar.
[self.frameLayoutGuide.heightAnchor
constraintGreaterThanOrEqualToAnchor:self.containerView.heightAnchor],
];
} else {
self.justifiedLayoutConstraints = @[
[self.heightAnchor constraintEqualToAnchor:self.containerView.heightAnchor],
[self.widthAnchor constraintEqualToAnchor:self.containerView.widthAnchor],
];
self.scrollableLayoutConstraints = @[];
[self.leadingAnchor constraintEqualToAnchor:self.containerView.leadingAnchor].active = YES;
[self.topAnchor constraintEqualToAnchor:self.containerView.topAnchor].active = YES;
[self.trailingAnchor constraintEqualToAnchor:self.containerView.trailingAnchor].active = YES;
[self.bottomAnchor constraintEqualToAnchor:self.containerView.bottomAnchor].active = YES;
}
self.containerViewConstraintsActive = YES;

// Must always be called last according to the documentation.
Expand Down
118 changes: 118 additions & 0 deletions components/Tabs/tests/snapshot/TabBarView/MDCTabBarViewSnapshotTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,27 @@
/** The maximum width of a tab bar item. */
static const CGFloat kMaxItemWidth = 360;

/** A test class that allows setting safe area insets. */
@interface MDCTabBarViewSnapshotTestsSuperview : UIView
/** Allows overriding the safe area insets. */
@property(nonatomic, assign) UIEdgeInsets customSafeAreaInsets;
@end

@implementation MDCTabBarViewSnapshotTestsSuperview

- (void)setCustomSafeAreaInsets:(UIEdgeInsets)customSafeAreaInsets {
_customSafeAreaInsets = customSafeAreaInsets;
if (@available(iOS 11.0, *)) {
[self safeAreaInsetsDidChange];
}
}

- (UIEdgeInsets)safeAreaInsets {
return _customSafeAreaInsets;
}

@end

@interface MDCTabBarViewSnapshotTests : MDCSnapshotTestCase

/** The view being snapshotted. */
Expand Down Expand Up @@ -83,6 +104,9 @@ - (void)tearDown {
#pragma mark - Helpers

- (void)generateSnapshotAndVerifyForView:(UIView *)view {
// Needed so that the stack view can be constrained correctly and then allow any "scrolling" to
// take place for the selected item to be visible.
[self.tabBarView layoutIfNeeded];
UIView *snapshotView = [view mdc_addToBackgroundView];
[self snapshotVerifyView:snapshotView];
}
Expand Down Expand Up @@ -387,6 +411,100 @@ - (void)testLayoutBehaviorWhenBoundsExceedsIntrinsicContentSizeAndRemainsLessTha
[self generateSnapshotAndVerifyForView:self.tabBarView];
}

#pragma mark - Safe Area Support

- (void)testSafeAreaTopAndLeftInsetsForJustifiedLayoutStyle {
// Given
UITabBarItem *item1 = [[UITabBarItem alloc] initWithTitle:@"1" image:nil tag:0];
UITabBarItem *item2 = [[UITabBarItem alloc] initWithTitle:@"2" image:nil tag:2];
MDCTabBarViewSnapshotTestsSuperview *superview =
[[MDCTabBarViewSnapshotTestsSuperview alloc] init];
[superview addSubview:self.tabBarView];
self.tabBarView.items = @[ item1, item2 ];
[self.tabBarView setSelectedItem:item2 animated:NO];

// When
superview.customSafeAreaInsets = UIEdgeInsetsMake(16, 44, 0, 0);
[self.tabBarView sizeToFit];
superview.bounds = CGRectMake(0, 0, CGRectGetWidth(self.tabBarView.bounds),
CGRectGetHeight(self.tabBarView.bounds));
self.tabBarView.center =
CGPointMake(CGRectGetMidX(superview.bounds), CGRectGetMidY(superview.bounds));

// Then
[self generateSnapshotAndVerifyForView:superview];
}

- (void)testSafeAreaRightAndBottomInsetsForJustifiedLayoutStyle {
// Given
UITabBarItem *item1 = [[UITabBarItem alloc] initWithTitle:@"1" image:nil tag:0];
UITabBarItem *item2 = [[UITabBarItem alloc] initWithTitle:@"2" image:nil tag:1];
MDCTabBarViewSnapshotTestsSuperview *superview =
[[MDCTabBarViewSnapshotTestsSuperview alloc] init];
[superview addSubview:self.tabBarView];
self.tabBarView.items = @[ item1, item2 ];
[self.tabBarView setSelectedItem:item2 animated:NO];

// When
superview.customSafeAreaInsets = UIEdgeInsetsMake(0, 0, 16, 44);
[self.tabBarView sizeToFit];
superview.bounds = CGRectMake(0, 0, CGRectGetWidth(self.tabBarView.bounds),
CGRectGetHeight(self.tabBarView.bounds));
self.tabBarView.center =
CGPointMake(CGRectGetMidX(superview.bounds), CGRectGetMidY(superview.bounds));

// Then
[self generateSnapshotAndVerifyForView:superview];
}

- (void)testSafeAreaTopAndLeftInsetsForScrollableLayoutStyle {
// Given
UITabBarItem *item1 = [[UITabBarItem alloc] initWithTitle:@"1" image:nil tag:0];
UITabBarItem *item2 = [[UITabBarItem alloc] initWithTitle:@"2" image:nil tag:1];
UITabBarItem *item3 = [[UITabBarItem alloc] initWithTitle:@"3" image:nil tag:2];
UITabBarItem *item4 = [[UITabBarItem alloc] initWithTitle:@"4" image:nil tag:3];
MDCTabBarViewSnapshotTestsSuperview *superview =
[[MDCTabBarViewSnapshotTestsSuperview alloc] init];
[superview addSubview:self.tabBarView];
self.tabBarView.items = @[ item1, item2, item3, item4 ];
[self.tabBarView setSelectedItem:item2 animated:NO];

// When
superview.customSafeAreaInsets = UIEdgeInsetsMake(16, 44, 0, 0);
self.tabBarView.bounds = CGRectMake(0, 0, kMinItemWidth * 2.5, kExpectedHeightTitlesOrIconsOnly);
superview.bounds = CGRectMake(0, 0, CGRectGetWidth(self.tabBarView.bounds),
CGRectGetHeight(self.tabBarView.bounds));
self.tabBarView.center =
CGPointMake(CGRectGetMidX(superview.bounds), CGRectGetMidY(superview.bounds));

// Then
[self generateSnapshotAndVerifyForView:superview];
}

- (void)testSafeAreaRightAndBottomInsetsForScrollableLayoutStyle {
// Given
UITabBarItem *item1 = [[UITabBarItem alloc] initWithTitle:@"1" image:nil tag:0];
UITabBarItem *item2 = [[UITabBarItem alloc] initWithTitle:@"2" image:nil tag:1];
UITabBarItem *item3 = [[UITabBarItem alloc] initWithTitle:@"3" image:nil tag:2];
UITabBarItem *item4 = [[UITabBarItem alloc] initWithTitle:@"4" image:nil tag:3];
MDCTabBarViewSnapshotTestsSuperview *superview =
[[MDCTabBarViewSnapshotTestsSuperview alloc] init];
[superview addSubview:self.tabBarView];
self.tabBarView.items = @[ item1, item2, item3, item4 ];
[self.tabBarView setSelectedItem:item4 animated:NO];

// When
superview.customSafeAreaInsets = UIEdgeInsetsMake(0, 0, 16, 44);
self.tabBarView.bounds = CGRectMake(0, 0, kMinItemWidth * 2.5, kExpectedHeightTitlesOrIconsOnly);
superview.bounds = CGRectMake(0, 0, CGRectGetWidth(self.tabBarView.bounds),
CGRectGetHeight(self.tabBarView.bounds));
self.tabBarView.center =
CGPointMake(CGRectGetMidX(superview.bounds), CGRectGetMidY(superview.bounds));

// Then
[self generateSnapshotAndVerifyForView:superview];
}

#pragma mark - MDCTabBarView Properties

- (void)testSetTitleColorForExplicitItemStates {
Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 079c42e

Please sign in to comment.