Lists are continuous, vertical indexes of text or images.
Contents
- Using lists
- Single-line list
- Two-line list
- Three-line list
- Theming lists
- Building your own list item
In order to install lists with Cocoapods first add the List component subspec to your Podfile
:
pod 'MaterialComponents/List'
Then, run the following command:
pod install
From there, import the relevant target or file and use your list item like you would any other UICollectionViewCell
.
import MaterialComponents.MaterialList
#import "MaterialList.h"
We currently offer two UICollectionViewCell
subclasses that can be used to create Material Design lists: MDCBaseCell
and MDCSelfSizingStereoCell
.
The MDCBaseCell
is a list item in its simplest form, a UICollectionViewCell
subclass with ripple and elevation. The MDCBaseCell
provides a starting point to build anything demonstrated in the extensive design guidelines. To build a list using MDCBaseCell
simply treat it like you would any other UICollectionViewCell
.
The MDCSelfSizingStereoCell
is a subclass of MDCBaseCell
. It exposes two image views (trailing and leading) and two labels (title and detail) that the user can configure however they like.
Because the list items we provide inherit from UICollectionViewCell
, clients are not expected to instantiate them themselves. Rather, clients should register the cell classes with UICollectionViews
, and then cast the cells to the correct class in their implementations of -collectionView:cellForItemAtIndexPath:
.
// registering the cell
collectionView.register(MDCBaseCell.self, forCellWithReuseIdentifier: "baseCellIdentifier")
// casting the cell to the desired type within `-collectionView:cellForItemAtIndexPath:`
guard let cell = collectionView.cellForItem(at: indexPath) as? MDCBaseCell else { fatalError() }
// registering the cell
[self.collectionView registerClass:[MDCBaseCell class]
forCellWithReuseIdentifier:@"BaseCellIdentifier"];
// casting the cell to the desired type within `-collectionView:cellForItemAtIndexPath:`
MDCBaseCell *cell =
[collectionView dequeueReusableCellWithReuseIdentifier:@"BaseCellIdentifier"
forIndexPath:indexPath];
We recommend setting UICollectionViewCell
s (and UITableViewCell
s) as accessibilityElements
. That way, VoiceOver doesn't traverse the entire cell and articulate an overwhelming amount of accessibility information for each of its subviews.
cell.isAccessibilityElement = true
cell.isAccessibilityElement = YES;
The following is an anatomy diagram of a typical list that applies to single-line, two-line, and three-line lists:
This list item consists of the following attributes:
- Leading image view
- Title label and detail label
- Trailing label
NOTE: MDCSelfSizingStereoCell
currently only supports leading and trailing image views, so the trailing label would be represented by a UIImageView
.
An instance of MDCSelfSizingStereoCell
can be configured to be a single-line, two-line, or three-line list item. The features above map to the following propertieis and methods:
Attribute | Related methods | Default value | |
---|---|---|---|
Color | rippleColor |
-setRippleColor: -setRippleColor |
On surface color at 0.12 opacity |
Elevation | elevation |
-setElevation: -elevation |
0 |
Attribute | Related methods | Default value | |
---|---|---|---|
Leading image | leadingImageView |
N/A | N/A |
Trailing image | trailingImageView |
N/A | N/A |
Attribute | Related methods | Default value | |
---|---|---|---|
Title text | titleLabel |
N/A | N/A |
Detail text | titleLabel |
N/A | N/A |
Types
There are three list types: 1. Single-line list, 2. Two-line list 3. Three-line list
Single-line list items contain a maximum of one line of text.
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard cell = collectionView.dequeueReusableCell(
withReuseIdentifier: kSelfSizingStereoCellIdentifier,
for: indexPath)
as? MDCCollectionViewTextCell
else { return }
cell.titleLabel.text = "This is a single-line list"
return cell
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath {
MDCSelfSizingStereoCell *cell =
(MDCSelfSizingStereoCell *)[collectionView dequeueReusableCellWithReuseIdentifier:kSelfSizingStereoCellIdentifier
forIndexPath:indexPath];
cell.titleLabel.text = @"This is a single-line list";
return cell;
}
Two-line list items contain a maximum of two lines of text.
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard cell = collectionView.dequeueReusableCell(
withReuseIdentifier: kSelfSizingStereoCellIdentifier,
for: indexPath)
as? MDCCollectionViewTextCell
else { return }
cell.titleLabel.text = "This is a two-line list"
cell.detailLabel.text = "This is secondary text that occupies one line."
return cell
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath {
MDCSelfSizingStereoCell *cell =
[collectionView dequeueReusableCellWithReuseIdentifier:kSelfSizingStereoCellIdentifier
forIndexPath:indexPath];
cell.titleLabel.text = @"This is a two-line list";
cell.detailLabel.text = @"This is secondary text that occupies one line.";
return cell;
}
Three-line list items contains a maximum of three lines of text.
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard cell = collectionView.dequeueReusableCell(
withReuseIdentifier: kSelfSizingStereoCellIdentifier,
for: indexPath)
as? MDCCollectionViewTextCell
else { return }
cell.titleLabel.text = "This is a three-line list"
cell.detailLabel.text = "This is secondary text\nthat occupies two lines."
return cell
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath {
MDCSelfSizingStereoCell *cell =
[collectionView dequeueReusableCellWithReuseIdentifier:kSelfSizingStereoCellIdentifier
forIndexPath:indexPath];
cell.titleLabel.text = @"This is a three-line list";
cell.detailLabel.text = @"This is secondary text\nthat occupies two lines.";
return cell;
}
This is an example of a two-line list with Shrine theming:
To theme a list item in your own app, use the Material Theming extension. To do that, first add the
theming extension to your Podfile
:
pod `MaterialComponents/List+Theming`
Then run the installer:
pod install
From there, call the theming method from your UICollectionViewDelegate
code.
// Step 1: Import the theming extension
import MaterialComponents.MaterialList_Theming
// Step 2: Create a shared container scheme. A shared scheme should be created once in your app and
// shared with all components.
let containerScheme = MDCContainerScheme()
// Step 3: Apply the scheme to each cell - from within `collectionView(_:cellForItemAt:)`
cell.applyTheme(withScheme:containerScheme)
// Step 1: Import the theming extension
#import "MaterialList+Theming.h"
// Step 2: Create a shared container scheme. A shared scheme should be created once in your app and
// shared with all components.
id<MDCContainerScheming> containerScheme = [[MDCContainerScheme alloc] init];
// Step 3: Apply the scheme to each cell - from within `-collectionView:cellForItemAtIndexPath:`
[cell applyThemeWithScheme:containerScheme];
The example files can be found here
Our example consists of a custom UICollectionViewController
: examples/CollectionListCellExampleTypicalUse.m
and also of a custom UICollectionViewCell
: examples/supplemental/CollectionViewListCell.m.
The main focus will be on the custom cell as that's where all the logic goes in, whereas the collection view and its controller are using mostly boilerplate code of setting up a simple example and collection view.
For our example we will have a layout consisting of a left aligned
UIImageView
, a title text UILabel
and a details text UILabel
. The title
text will have a max of 1 line whereas the details text can be up to 3 lines.
It is important to note that neither the image nor the labels need to be set.
To see more of the spec guidelines for Lists please see here: https://material.io/go/design-lists
To create our layout we used auto layout constraints that are all set up in
the (void)setupConstraints
method in our custom cell. It is important to
make sure we set translatesAutoresizingMaskIntoConstraints
to NO
for all
the views we are applying constraints on.
Interactable Material components and specifically List Cells have an ink ripple when tapped on. To add ink to your cells there are a few steps you need to take:
-
Add an
MDCInkView
property to your custom cell. -
Initialize
MDCInkView
on init and add it as a subview:let inkView = MDCInkView(frame: bounds) inkView.usesLegacyInkRipple = false addSubview(inkView)
_inkView = [[MDCInkView alloc] initWithFrame:self.bounds]; _inkView.usesLegacyInkRipple = NO; [self addSubview:_inkView];
-
Initialize a
CGPoint
property in your cell (CGPoint _lastTouch;
) to indicate where the last tap was in the cell. -
Override the
UIResponder
'stouchesBegan
method in your cell to identify and save where the touches were so we can then start the ripple animation from that point:override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { let touch = touches.first let location = touch?.location(in: self) lastTouch = location }
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [touches anyObject]; CGPoint location = [touch locationInView:self]; _lastTouch = location; [super touchesBegan:touches withEvent:event]; }
-
Override the
setHighlighted
method for your cell and apply the start and stop ripple animations:override var isHighlighted: Bool { set { super.isHighlighted = newValue if (newValue) { inkView.startTouchBeganAnimation(at: lastTouch, completion: nil) } else { inkView.startTouchEndedAnimation(at: lastTouch, completion: nil) } } // get... }
- (void)setHighlighted:(BOOL)highlighted { [super setHighlighted:highlighted]; if (highlighted) { [_inkView startTouchBeganAnimationAtPoint:_lastTouch completion:nil]; } else { [_inkView startTouchEndedAnimationAtPoint:_lastTouch completion:nil]; } }
-
When the cell is reused we must make sure no outstanding ripple animations stay on the cell so we need to clear the ink before:
override func prepareForReuse() { inkView.cancelAllAnimations(animated: false) super.prepareForReuse() }
- (void)prepareForReuse { [_inkView cancelAllAnimationsAnimated:NO]; [super prepareForReuse]; }
Now there is ink in our cells!
In order to have cells self-size based on content and not rely on magic number constants to decide how big they should be, we need to follow these steps:
-
Apply autolayout constraints of our added subviews relative to each other and their superview (the cell's
contentView
).We need to make sure our constraints don't define static heights or widths but rather constraints that are relative or our cell won't calculate itself based on the dynamically sized content. You can see how it is achieved in the
setupConstraints
method in our example. If you'll notice there are some constraints that are set up to be accessible throughout the file:var imageLeftPaddingConstraint: NSLayoutConstraint var imageRightPaddingConstraint: NSLayoutConstraint var imageWidthConstraint: NSLayoutConstraint
NSLayoutConstraint *_imageLeftPaddingConstraint; NSLayoutConstraint *_imageRightPaddingConstraint; NSLayoutConstraint *_imageWidthConstraint;
This is in order to support the changing layout if an image is set or not.
-
Because our list cells need to fill the entire width of the collection view, we want to expose the cell's width to be settable by the view controller when the cell is set up. For that we expose a
setCellWidth
method that sets the width constraint of thecontentView
:func set(cellWidth: CGFloat) { cellWidthConstraint.constant = cellWidth cellWidthConstraint.isActive = true }
- (void)setCellWidth:(CGFloat)width { _cellWidthConstraint.constant = width; _cellWidthConstraint.active = YES; }
and then in the collection view's
cellForItemAtIndexPath
delegate method we set the width:var cellWidth = collectionView.bounds.width if #available(iOS 11.0, *) { cellWidth -= collectionView.adjustedContentInset.left + collectionView.adjustedContentInset.right } set(cellWidth: cellWidth)
CGFloat cellWidth = CGRectGetWidth(collectionView.bounds); if (@available(iOS 11.0, *)) { cellWidth -= (collectionView.adjustedContentInset.left + collectionView.adjustedContentInset.right); } [cell setCellWidth:cellWidth];
-
In our collection view's flow layout we must set an
estimatedItemSize
so the collection view will defer the size calculations to its content.Note: It is better to set the size smaller rather than larger or constraints might break in runtime.
flowLayout.estimatedItemSize = CGSize(width: kSmallArbitraryCellWidth, height: kSmallestCellHeight)
_flowLayout.estimatedItemSize = CGSizeMake(kSmallArbitraryCellWidth, kSmallestCellHeight);
For our example we use a typography scheme to apply the fonts to our cell's
UILabel
s. Please see Typography
Scheme for more information.
Dynamic Type allows users to indicate a system-wide preferred text size. To support it in our cells we need to follow these steps:
-
Set each of the label fonts to use the dynamically sized MDC fonts in their set/update methods:
func updateTitleFont() { if (_titleFont == nil) { _titleFont = defaultTitleFont } _titleLabel.font = _titleFont.mdc_fontSized(forMaterialTextStyle: .subheadline, scaledForDynamicType: mdc_adjustsFontForContentSizeCategory) }
- (void)updateTitleFont { if (!_titleFont) { _titleFont = defaultTitleFont(); } _titleLabel.font = [_titleFont mdc_fontSizedForMaterialTextStyle:MDCFontTextStyleSubheadline scaledForDynamicType:_mdc_adjustsFontForContentSizeCategory]; [self setNeedsLayout]; }
-
Add an observer in the cell to check for the
UIContentSizeCategoryDidChangeNotification
which tells us the a system-wide text size has been changed.NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange(notification:)), name: UIContentSizeCategory.didChangeNotification, object: nil)
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(contentSizeCategoryDidChange:) name:UIContentSizeCategoryDidChangeNotification object:nil];
In the selector update the font sizes to reflect the change:
func contentSizeCategoryDidChange(_: NSNotification) { updateTitleFont() updateDetailsFont() }
- (void)contentSizeCategoryDidChange:(__unused NSNotification *)notification { [self updateTitleFont]; [self updateDetailsFont]; }
-
Add an observer also in the
UIViewController
so we can reload the collection view once there is a change:func contentSizeCategoryDidChange(_: NSNotification) { collectionView.reloadData() }
- (void)contentSizeCategoryDidChange:(__unused NSNotification *)notification { [self.collectionView reloadData]; }
Our collection view needs to be aware of the safe areas when being presented on iPhone X. To do so need to set its
contentInsetAdjustmentBehavior
to be aware of the safe area:if #available(iOS 11.0, *) { collectionView.contentInsetAdjustmentBehavior = .always }
#if defined(__IPHONE_11_0) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0) if (@available(iOS 11.0, *)) { self.collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAlways; } #endif
Lastly, as seen in the self-sizing section on step 2, when setting the width of the cell we need to set it to be the width of the collection view bounds minus the adjustedContentInset that now insets based on the safe area.
In your view controller you need to invalidate the layout of your collection view when there is an orientation change. Please see below for the desired code changes to achieve that:
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) self.collectionView.collectionViewLayout.invalidateLayout() self.collectionView.reloadData() } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) self.collectionView.collectionViewLayout.invalidateLayout() coordinator.animate(alongsideTransition: nil) { (_) in self.collectionView.collectionViewLayout.invalidateLayout() } }
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection { [super traitCollectionDidChange:previousTraitCollection]; [self.collectionView.collectionViewLayout invalidateLayout]; [self.collectionView reloadData]; } - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator { [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; [self.collectionView.collectionViewLayout invalidateLayout]; [coordinator animateAlongsideTransition:nil completion:^(__unused id context) { [self.collectionView.collectionViewLayout invalidateLayout]; }]; }
To support right to left text we need to import MDFInternationalization
:
import MDFInternationalization
#import <MDFInternationalization/MDFInternationalization.h>
and for each of our cell's subviews me need to update the autoResizingMask
:
_titleLabel.autoresizingMask =
MDFTrailingMarginAutoresizingMaskForLayoutDirection(effectiveUserInterfaceLayoutDirection)
_titleLabel.autoresizingMask =
MDFTrailingMarginAutoresizingMaskForLayoutDirection(self.effectiveUserInterfaceLayoutDirection);