From 08100bd8e16678eb271011a5a0bbf9e1e3e280c2 Mon Sep 17 00:00:00 2001 From: Andrew Overton Date: Fri, 7 Jun 2019 11:39:40 -0400 Subject: [PATCH] [Tabs] Give MDCTabBarDelegate pass through methods for "willDisplayCell"/"didEndDisplayingCell" (#7518) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One of the solutions proposed in #6275 is to provide pass through methods for the UICollectionViewDelegate methods `-collectionView:willDisplayCell:forItemAtIndexPath:` and `-collectionView:didEndDisplayingCell:forItemAtIndexPath:`. This is the one I chose, mainly because it seemed the most straightforward. [One comment](https://github.com/material-components/material-components-ios/issues/6275#issuecomment-453650714) said we should be careful about this approach, and [another](https://github.com/material-components/material-components-ios/issues/6275#issuecomment-454204635) said we needed to look more into the "pre-fetching" behavior of these methods. It seems like a good approach to me... Scrolling through the tabs in the TabBarTextOnlyExample with print statements seems to work how I would expect it to. 👍 --- .../Tabs/examples/TabBarTextOnlyExample.m | 13 +++ .../TabBarTextOnlyExampleSupplemental.h | 3 +- components/Tabs/src/MDCTabBar.m | 5 ++ .../Tabs/src/MDCTabBarDisplayDelegate.h | 46 ++++++++++ components/Tabs/src/private/MDCItemBar.m | 15 ++++ .../unit/MDCTabBarDisplayDelegateTests.m | 86 +++++++++++++++++++ 6 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 components/Tabs/src/MDCTabBarDisplayDelegate.h create mode 100644 components/Tabs/tests/unit/MDCTabBarDisplayDelegateTests.m diff --git a/components/Tabs/examples/TabBarTextOnlyExample.m b/components/Tabs/examples/TabBarTextOnlyExample.m index 3a04e1f31e0..9ad80f5f827 100644 --- a/components/Tabs/examples/TabBarTextOnlyExample.m +++ b/components/Tabs/examples/TabBarTextOnlyExample.m @@ -14,6 +14,7 @@ #import +#import "MDCTabBarDisplayDelegate.h" #import "MaterialAppBar.h" #import "MaterialButtons.h" #import "MaterialCollections.h" @@ -72,6 +73,8 @@ - (void)loadTabBar { self.tabBar.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleBottomMargin; [self.tabBar sizeToFit]; + + self.tabBar.displayDelegate = self; } - (void)changeAlignment:(id)sender { @@ -132,4 +135,14 @@ - (void)collectionView:(UICollectionView *)collectionView } } +#pragma mark - MDCTabBarDisplayDelegate + +- (void)tabBar:(MDCTabBar *)tabBar willDisplayItem:(UITabBarItem *)item { + NSLog(@"Will display item: %@", item.title); +} + +- (void)tabBar:(MDCTabBar *)tabBar didEndDisplayingItem:(nonnull UITabBarItem *)item { + NSLog(@"Did end displaying item: %@", item.title); +} + @end diff --git a/components/Tabs/examples/supplemental/TabBarTextOnlyExampleSupplemental.h b/components/Tabs/examples/supplemental/TabBarTextOnlyExampleSupplemental.h index 6d1fb0907f9..0faa6fae9c8 100644 --- a/components/Tabs/examples/supplemental/TabBarTextOnlyExampleSupplemental.h +++ b/components/Tabs/examples/supplemental/TabBarTextOnlyExampleSupplemental.h @@ -19,12 +19,13 @@ #import +#import "MDCTabBarDisplayDelegate.h" #import "MaterialAppBar.h" #import "MaterialCollections.h" #import "MaterialColorScheme.h" #import "MaterialTabs.h" -@interface TabBarTextOnlyExample : MDCCollectionViewController +@interface TabBarTextOnlyExample : MDCCollectionViewController @property(nonatomic, nullable) MDCAppBarViewController *appBarViewController; @property(nonatomic, nullable) MDCSemanticColorScheme *colorScheme; diff --git a/components/Tabs/src/MDCTabBar.m b/components/Tabs/src/MDCTabBar.m index a5c161b6627..980b83b062f 100644 --- a/components/Tabs/src/MDCTabBar.m +++ b/components/Tabs/src/MDCTabBar.m @@ -16,6 +16,7 @@ #import +#import "MDCTabBarDisplayDelegate.h" #import "MDCTabBarExtendedAlignment.h" #import "MDCTabBarIndicatorTemplate.h" #import "MDCTabBarSizeClassDelegate.h" @@ -82,6 +83,10 @@ @interface MDCTabBar () @property(nonatomic, weak, nullable) id sizeClassDelegate; @end +@interface MDCTabBar () +@property(nonatomic, weak, nullable) id displayDelegate; +@end + @interface MDCTabBar () @end diff --git a/components/Tabs/src/MDCTabBarDisplayDelegate.h b/components/Tabs/src/MDCTabBarDisplayDelegate.h new file mode 100644 index 00000000000..0e9cc5de630 --- /dev/null +++ b/components/Tabs/src/MDCTabBarDisplayDelegate.h @@ -0,0 +1,46 @@ +// Copyright 2019-present the Material Components for iOS authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import +#import "MDCTabBar.h" + +/** + An additional delegate protocol for MDCTabBar that provides information about when UITabBarItems + are about to be displayed and when they stop being displayed. + */ +@protocol MDCTabBarDisplayDelegate + +/** + This method is called sometime before the tab's view is displayed. + */ +- (void)tabBar:(nonnull MDCTabBar *)tabBar willDisplayItem:(nonnull UITabBarItem *)item; + +/** + This method is called sometime after the tab's view has stopped being displayed. + */ +- (void)tabBar:(nonnull MDCTabBar *)tabBar didEndDisplayingItem:(nonnull UITabBarItem *)item; + +@end + +@interface MDCTabBar (MDCTabBarDisplayDelegate) + +/** + A delegate that allows implementers to receive updates on when UITabBarItems are about to be + displayed and when they stop being displayed. + + @note This property may be removed in a future version and should be used with that understanding. + */ +@property(nonatomic, weak, nullable) NSObject *displayDelegate; + +@end diff --git a/components/Tabs/src/private/MDCItemBar.m b/components/Tabs/src/private/MDCItemBar.m index f78a3fc0224..935d11ad380 100644 --- a/components/Tabs/src/private/MDCItemBar.m +++ b/components/Tabs/src/private/MDCItemBar.m @@ -18,6 +18,7 @@ #import "MDCItemBarCell.h" #import "MDCItemBarStyle.h" +#import "MDCTabBarDisplayDelegate.h" #import "MDCTabBarIndicatorAttributes.h" #import "MDCTabBarIndicatorTemplate.h" #import "MDCTabBarIndicatorView.h" @@ -408,6 +409,20 @@ - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView return itemCell; } +- (void)collectionView:(UICollectionView *)collectionView + willDisplayCell:(UICollectionViewCell *)cell + forItemAtIndexPath:(NSIndexPath *)indexPath { + UITabBarItem *item = [self itemAtIndexPath:indexPath]; + [self.tabBar.displayDelegate tabBar:self.tabBar willDisplayItem:item]; +} + +- (void)collectionView:(UICollectionView *)collectionView + didEndDisplayingCell:(UICollectionViewCell *)cell + forItemAtIndexPath:(NSIndexPath *)indexPath { + UITabBarItem *item = [self itemAtIndexPath:indexPath]; + [self.tabBar.displayDelegate tabBar:self.tabBar didEndDisplayingItem:item]; +} + #pragma mark - UICollectionViewDelegateFlowLayout - (CGSize)collectionView:(UICollectionView *)collectionView diff --git a/components/Tabs/tests/unit/MDCTabBarDisplayDelegateTests.m b/components/Tabs/tests/unit/MDCTabBarDisplayDelegateTests.m new file mode 100644 index 00000000000..53901f273e0 --- /dev/null +++ b/components/Tabs/tests/unit/MDCTabBarDisplayDelegateTests.m @@ -0,0 +1,86 @@ +// Copyright 2019-present the Material Components for iOS authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +#import "MDCItemBar.h" +#import "MDCTabBarDisplayDelegate.h" +#import "MaterialTabs.h" + +@interface MDCTabBarDisplayDelegate : NSObject +@property(nonatomic, assign) BOOL willDisplayItemWasCalled; +@property(nonatomic, assign) BOOL didEndDisplayingItemWasCalled; +@end + +@implementation MDCTabBarDisplayDelegate + +- (void)tabBar:(MDCTabBar *)tabBar willDisplayItem:(UITabBarItem *)item { + self.willDisplayItemWasCalled = YES; +} + +- (void)tabBar:(MDCTabBar *)tabBar didEndDisplayingItem:(UITabBarItem *)item { + self.didEndDisplayingItemWasCalled = YES; +} + +@end + +@interface MDCTabBarDisplayDelegateTests : XCTestCase +@end + +@implementation MDCTabBarDisplayDelegateTests + +- (void)testMDCTabBarDisplayDelegateTabBarWillDisplayItemWhenViewHasBeenLaidOut { + // Given + MDCTabBar *tabBar = [[MDCTabBar alloc] initWithFrame:CGRectZero]; + MDCTabBarDisplayDelegate *displayDelegate = [[MDCTabBarDisplayDelegate alloc] init]; + tabBar.displayDelegate = displayDelegate; + CGFloat tabBarHeight = [MDCTabBar defaultHeightForItemAppearance:tabBar.itemAppearance]; + tabBar.frame = CGRectMake(0, 0, 200, tabBarHeight); + + // When + UITabBarItem *item1 = [[UITabBarItem alloc] initWithTitle:@"first tab" image:nil tag:0]; + UITabBarItem *item2 = [[UITabBarItem alloc] initWithTitle:@"second tab" image:nil tag:0]; + tabBar.items = @[ item1, item2 ]; + [tabBar setNeedsLayout]; + [tabBar layoutIfNeeded]; + + // Then + XCTAssertTrue(displayDelegate.willDisplayItemWasCalled); +} + +- (void)testMDCTabBarDisplayDelegateTabBarDidEndDisplayingItemWhenViewHasBeenLaidOut { + // Given + MDCTabBar *tabBar = [[MDCTabBar alloc] initWithFrame:CGRectZero]; + MDCTabBarDisplayDelegate *displayDelegate = [[MDCTabBarDisplayDelegate alloc] init]; + tabBar.displayDelegate = displayDelegate; + CGFloat tabBarHeight = [MDCTabBar defaultHeightForItemAppearance:tabBar.itemAppearance]; + tabBar.frame = CGRectMake(0, 0, 200, tabBarHeight); + UITabBarItem *item1 = [[UITabBarItem alloc] initWithTitle:@"first tab" image:nil tag:0]; + UITabBarItem *item2 = [[UITabBarItem alloc] initWithTitle:@"second tab" image:nil tag:0]; + tabBar.items = @[ item1, item2 ]; + [tabBar setNeedsLayout]; + [tabBar layoutIfNeeded]; + + // When + UITabBarItem *item3 = [[UITabBarItem alloc] initWithTitle:@"third tab" image:nil tag:0]; + UITabBarItem *item4 = [[UITabBarItem alloc] initWithTitle:@"fourth tab" image:nil tag:0]; + tabBar.items = @[ item3, item4 ]; + [tabBar setNeedsLayout]; + [tabBar layoutIfNeeded]; + + // Then + XCTAssertTrue(displayDelegate.didEndDisplayingItemWasCalled); +} + +@end