blob: 701a4bc3884edda4e0c2cf774f58a4c6afd32a8b [file] [view] [edit]
<!--docs:
title: "List"
layout: detail
section: components
excerpt: "Material Design Lists are used to show continuous groups of images or text."
iconId: <#icon_id#>
path: /catalog/list/
api_doc_root: true
-->
<!-- This file was auto-generated using ./scripts/generate_readme List -->
# List
[![Open bugs badge](https://img.shields.io/badge/dynamic/json.svg?label=open%20bugs&url=https%3A%2F%2Fapi.github.com%2Fsearch%2Fissues%3Fq%3Dis%253Aopen%2Blabel%253Atype%253ABug%2Blabel%253A%255BList%255D&query=%24.total_count)](https://github.com/material-components/material-components-ios/issues?q=is%3Aopen+is%3Aissue+label%3Atype%3ABug+label%3A%5BList%5D)
Material Design Lists are continuous groups of text and/or images. The [Material guidelines](https://material.io/go/design-lists) for Lists are extensive, and there is no class at this time for implementing any one of them, let alone all of them. However, we are starting to add classes that represent individual List Items. We currently offer two List Item Cells:
### MDCBaseCell
The MDCBaseCell is a List Item at its simplest--a basic UICollectionViewCell subclass with Material Ink Ripple and Elevation. The MDCBaseCell provides a starting point to build anything the guidelines provide. To build a List using the MDCBaseCell simply treat it like you would any other UICollectionViewCell.
Below is an example:
<div class="article__asset article__asset--screenshot">
<img src="docs/assets/list_base_cell_animated.gif" alt="Animation showing a list of MDCBaseCell views with Ripple effects." width="356">
</div>
### MDCSelfSizingStereoCell
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.
Below is an example:
<div class="article__asset article__asset--screenshot">
<img src="docs/assets/list_stereo_cell_animated.gif" alt="Animation showing a list of MDCStereoCell views scrolling up and down." width="480">
</div>
## Design & API documentation
<ul class="icon-list">
<li class="icon-list-item icon-list-item--link"><a href="https://material.io/go/lists">Material Design guidelines: Lists</a></li>
<li class="icon-list-item icon-list-item--link">Class: <a href="https://material.io/components/ios/catalog/list/api-docs/Classes/MDCBaseCell.html">MDCBaseCell</a></li>
<li class="icon-list-item icon-list-item--link">Class: <a href="https://material.io/components/ios/catalog/list/api-docs/Classes/MDCSelfSizingStereoCell.html">MDCSelfSizingStereoCell</a></li>
</ul>
## Table of contents
- [Installation](#installation)
- [Installation with CocoaPods](#installation-with-cocoapods)
- [Importing](#importing)
- [Usage](#usage)
- [Typical use](#typical-use)
- [Theming](#theming)
- [Accessibility](#accessibility)
- [Setting `-isAccessibilityElement`](#setting-`-isaccessibilityelement`)
- [How to implement your own List Cell](#how-to-implement-your-own-list-cell)
- [Layout](#layout)
- [Ink Ripple](#ink-ripple)
- [Self Sizing](#self-sizing)
- [Typography](#typography)
- [Dynamic Type](#dynamic-type)
- [iPhone X Safe Area Support](#iphone-x-safe-area-support)
- [Landscape Support](#landscape-support)
- [Right to Left Text Support](#right-to-left-text-support)
## Installation
<!-- Extracted from docs/../../../docs/component-installation.md -->
### Installation with CocoaPods
Add the following to your `Podfile`:
```bash
pod 'MaterialComponents/List'
```
<!--{: .code-renderer.code-renderer--install }-->
Then, run the following command:
```bash
pod install
```
### Importing
To import the component:
<!--<div class="material-code-render" markdown="1">-->
#### Swift
```swift
import MaterialComponents.MaterialList
```
#### Objective-C
```objc
#import "MaterialList.h"
```
<!--</div>-->
## Usage
<!-- Extracted from docs/typical-use.md -->
### Typical use
Because List Items ultimately inherit from UICollectionViewCell, clients are not expected to instantiate them themselves. Rather, cell classes are registered with UICollectionViews. Then, in `-collectionView:cellForItemAtIndexPath:`, the client is expected to cast the cell to a List Item class.
<!--<div class="material-code-render" markdown="1">-->
#### Swift
```swift
// 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() }
```
#### Objective-C
```objc
// 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];
```
<!--</div>-->
<!-- Extracted from docs/theming.md -->
## Theming
You can theme a List Item with your app's shared scheme using the MaterialKist Theming extension.
You must first add the Theming extension to your project:
```bash
pod `MaterialComponents/List+Theming`
```
<!--<div class="material-code-render" markdown="1">-->
#### Swift
```swift
// 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)
```
#### Objective-C
```objc
// 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];
```
<!--</div>-->
<!-- Extracted from docs/accessibility.md -->
## Accessibility
To help ensure your Lists are accessible to as many users as possible, please be sure to review the following
recommendations:
### Setting `-isAccessibilityElement`
It is generally recommended to set UICollectionViewCells (and UITableViewCells) as accessibilityElements. That way, VoiceOver doesn't traverse the entire cell and articulate an overwhelming amount of accessibility information for each of its subviews.
<!--<div class="material-code-render" markdown="1">-->
#### Swift
```swift
cell.isAccessibilityElement = true
```
#### Objective-C
```objc
cell.isAccessibilityElement = YES;
```
<!--</div>-->
## How to implement your own List Cell
<!-- Extracted from docs/create-your-own.md -->
The example files can be found <a href="examples/">here</a>
<img src="docs/assets/listcellexample.gif" alt="List Cell Example" width="300">
Our example consists of a custom `UICollectionViewController`: <a
href="examples/CollectionListCellExampleTypicalUse.m">examples/CollectionListCellExampleTypicalUse.m</a>
and also of a custom `UICollectionViewCell`: <a
href="examples/supplemental/CollectionViewListCell.m">examples/supplemental/CollectionViewListCell.m</a>.
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.
### Layout
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: <a
href="https://material.io/go/design-lists">https://material.io/go/design-lists</a>
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.
### Ink Ripple
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:
1. Add an `MDCInkView` property to your custom cell.
2. Initialize `MDCInkView` on init and add it as a subview:
<!--<div class="material-code-render" markdown="1">-->
#### Objective-C
```objc
_inkView = [[MDCInkView alloc] initWithFrame:self.bounds];
_inkView.usesLegacyInkRipple = NO;
[self addSubview:_inkView];
```
#### Swift
```swift
let inkView = MDCInkView(frame: bounds)
inkView.usesLegacyInkRipple = false
addSubview(inkView)
```
<!--</div>-->
3. Initialize a `CGPoint` property in your cell (`CGPoint _lastTouch;`) to
indicate where the last tap was in the cell.
4. Override the `UIResponder`'s `touchesBegan` method in your cell to identify
and save where the touches were so we can then start the ripple animation from
that point:
<!--<div class="material-code-render" markdown="1">-->
#### Objective-C
```objc
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInView:self];
_lastTouch = location;
[super touchesBegan:touches withEvent:event];
}
```
#### Swift
```swift
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
let location = touch?.location(in: self)
lastTouch = location
}
```
<!--</div>-->
5. Override the `setHighlighted` method for your cell and apply the start and
stop ripple animations:
<!--<div class="material-code-render" markdown="1">-->
#### Objective-C
```objc
- (void)setHighlighted:(BOOL)highlighted {
[super setHighlighted:highlighted];
if (highlighted) {
[_inkView startTouchBeganAnimationAtPoint:_lastTouch completion:nil];
} else {
[_inkView startTouchEndedAnimationAtPoint:_lastTouch completion:nil];
}
}
```
#### Swift
```swift
override var isHighlighted: Bool {
set {
super.isHighlighted = newValue
if (newValue) {
inkView.startTouchBeganAnimation(at: lastTouch, completion: nil)
} else {
inkView.startTouchEndedAnimation(at: lastTouch, completion: nil)
}
}
// get...
}
```
<!--</div>-->
6. 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:
<!--<div class="material-code-render" markdown="1">-->
#### Objective-C
```objc
- (void)prepareForReuse {
[_inkView cancelAllAnimationsAnimated:NO];
[super prepareForReuse];
}
```
#### Swift
```swift
override func prepareForReuse() {
inkView.cancelAllAnimations(animated: false)
super.prepareForReuse()
}
```
<!--</div>-->
Now there is ink in our cells!
### Self Sizing
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:
1. 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:
<!--<div class="material-code-render" markdown="1">-->
#### Objective-C
```objc
NSLayoutConstraint *_imageLeftPaddingConstraint;
NSLayoutConstraint *_imageRightPaddingConstraint;
NSLayoutConstraint *_imageWidthConstraint;
```
#### Swift
```swift
var imageLeftPaddingConstraint: NSLayoutConstraint
var imageRightPaddingConstraint: NSLayoutConstraint
var imageWidthConstraint: NSLayoutConstraint
```
<!--</div>-->
This is in order to support the changing layout if an image is set or not.
2. 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 the `contentView`:
<!--<div class="material-code-render" markdown="1">-->
#### Objective-C
```objc
- (void)setCellWidth:(CGFloat)width {
_cellWidthConstraint.constant = width;
_cellWidthConstraint.active = YES;
}
```
#### Swift
```swift
func set(cellWidth: CGFloat) {
cellWidthConstraint.constant = cellWidth
cellWidthConstraint.isActive = true
}
```
<!--</div>-->
and then in the collection view's `cellForItemAtIndexPath` delegate method we
set the width:
<!--<div class="material-code-render" markdown="1">-->
#### Objective-C
```objc
CGFloat cellWidth = CGRectGetWidth(collectionView.bounds);
if (@available(iOS 11.0, *)) {
cellWidth -=
(collectionView.adjustedContentInset.left + collectionView.adjustedContentInset.right);
}
[cell setCellWidth:cellWidth];
```
#### Swift
```swift
var cellWidth = collectionView.bounds.width
if #available(iOS 11.0, *) {
cellWidth -= collectionView.adjustedContentInset.left + collectionView.adjustedContentInset.right
}
set(cellWidth: cellWidth)
```
<!--</div>-->
3. 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.
<!--<div class="material-code-render" markdown="1">-->
#### Objective-C
```objc
_flowLayout.estimatedItemSize = CGSizeMake(kSmallArbitraryCellWidth, kSmallestCellHeight);
```
#### Swift
```swift
flowLayout.estimatedItemSize = CGSize(width: kSmallArbitraryCellWidth,
height: kSmallestCellHeight)
```
<!--</div>-->
### Typography
For our example we use a typography scheme to apply the fonts to our cell's
`UILabel`s. Please see <a href="../schemes/Typography">Typography
Scheme</a> for more information.
### Dynamic Type
<a
href="https://developer.apple.com/ios/human-interface-guidelines/visual-design/typography/">Dynamic
Type</a> allows users to indicate a system-wide preferred text size. To
support it in our cells we need to follow these steps:
1. Set each of the label fonts to use the dynamically sized MDC fonts in their
set/update methods:
<!--<div class="material-code-render" markdown="1">-->
#### Objective-C
```objc
- (void)updateTitleFont {
if (!_titleFont) {
_titleFont = defaultTitleFont();
}
_titleLabel.font =
[_titleFont mdc_fontSizedForMaterialTextStyle:MDCFontTextStyleSubheadline
scaledForDynamicType:_mdc_adjustsFontForContentSizeCategory];
[self setNeedsLayout];
}
```
#### Swift
```swift
func updateTitleFont() {
if (_titleFont == nil) {
_titleFont = defaultTitleFont
}
_titleLabel.font =
_titleFont.mdc_fontSized(forMaterialTextStyle: .subheadline,
scaledForDynamicType: mdc_adjustsFontForContentSizeCategory)
}
```
<!--</div>-->
2. Add an observer in the cell to check for the
`UIContentSizeCategoryDidChangeNotification` which tells us the a system-wide
text size has been changed.
<!--<div class="material-code-render" markdown="1">-->
#### Objective-C
```objc
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(contentSizeCategoryDidChange:)
name:UIContentSizeCategoryDidChangeNotification
object:nil];
```
#### Swift
```swift
NotificationCenter.default.addObserver(self,
selector: #selector(contentSizeCategoryDidChange(notification:)),
name: UIContentSizeCategory.didChangeNotification,
object: nil)
```
<!--</div>-->
In the selector update the font sizes to reflect the change:
<!--<div class="material-code-render" markdown="1">-->
#### Objective-C
```objc
- (void)contentSizeCategoryDidChange:(__unused NSNotification *)notification {
[self updateTitleFont];
[self updateDetailsFont];
}
```
#### Swift
```swift
func contentSizeCategoryDidChange(_: NSNotification) {
updateTitleFont()
updateDetailsFont()
}
```
<!--</div>-->
3. Add an observer also in the `UIViewController` so we can reload the
collection view once there is a change:
<!--<div class="material-code-render" markdown="1">-->
#### Objective-C
```objc
- (void)contentSizeCategoryDidChange:(__unused NSNotification *)notification {
[self.collectionView reloadData];
}
```
#### Swift
```swift
func contentSizeCategoryDidChange(_: NSNotification) {
collectionView.reloadData()
}
```
<!--</div>-->
### iPhone X Safe Area Support
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:
<!--<div class="material-code-render" markdown="1">-->
#### Objective-C
```objc
#if defined(__IPHONE_11_0) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0)
if (@available(iOS 11.0, *)) {
self.collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAlways;
}
#endif
```
#### Swift
```swift
if #available(iOS 11.0, *) {
collectionView.contentInsetAdjustmentBehavior = .always
}
```
<!--</div>-->
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.
### Landscape Support
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:
<!--<div class="material-code-render" markdown="1">-->
#### Objective-C
```objc
- (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];
}];
}
```
#### Swift
```swift
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()
}
}
```
<!--</div>-->
### Right to Left Text Support
To support right to left text we need to import `MDFInternationalization`:
<!--<div class="material-code-render" markdown="1">-->
#### Objective-C
```objc
#import <MDFInternationalization/MDFInternationalization.h>
```
#### Swift
```swift
import MDFInternationalization
```
<!--</div>-->
and for each of our cell's subviews me need to update the `autoResizingMask`:
<!--<div class="material-code-render" markdown="1">-->
#### Objective-C
```objc
_titleLabel.autoresizingMask =
MDFTrailingMarginAutoresizingMaskForLayoutDirection(self.mdf_effectiveUserInterfaceLayoutDirection);
```
#### Swift
```swift
_titleLabel.autoresizingMask =
MDFTrailingMarginAutoresizingMaskForLayoutDirection(mdf_effectiveUserInterfaceLayoutDirection)
```
<!--</div>-->