tree: f225993e4b5ab801aafe450b0e99ca7ef123ed50 [path history] [tgz]
  1. docs/
  2. examples/
  3. src/
  4. tests/
  5. .jazzy.yaml
  6. .vars
  7. README.md
components/List/README.md

Lists

Open bugs badge

Lists are continuous, vertical indexes of text or images.

There are three list types:

  1. Single-line list
  2. Two-line list
  3. Three-line list

Composite image of the three list types

Contents

Using lists

We currently offer two UICollectionViewCell subclasses that can be used to create Material Design lists: MDCBaseCell and MDCSelfSizingStereoCell.

MDCBaseCell

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.

Animation showing a list of MDCBaseCell views with Ripple effects

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.

Animation showing a list of stereo cell scrolling up and down

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:.

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

// 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];

Installing lists

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 component:

Swift

import MaterialComponents.MaterialList

Objective-C

#import "MaterialList.h"

Making lists accessible

Setting -isAccessibilityElement

We recommend setting 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.

Swift

cell.isAccessibilityElement = true

Objective-C

cell.isAccessibilityElement = YES;

List anatomy

The following is an anatomy diagram of a typical list that applies to single-line, two-line, and three-line lists:

List anatomy diagram showing list, row, and list item content

This list item consists of the following attributes:

  1. Leading image view
  2. Title label and detail label
  3. 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:

Container attributes

 AttributeRelated methodsDefault value
ColorrippleColor-setRippleColor:
-setRippleColor
On surface color at 0.12 opacity
Elevationelevation-setElevation:
-elevation
0

Icon attributes

 AttributeRelated methodsDefault value
Leading imageleadingImageViewN/AN/A
Trailing imagetrailingImageViewN/AN/A

Text label attributes

 AttributeRelated methodsDefault value
Title texttitleLabelN/AN/A
Detail texttitleLabelN/AN/A

Types of list

Single-line list

Single-line list items contain a maximum of one line of text.

Single-line list example

Image of three single-line list items with sample text

Objective-C

- (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;
}

Swift

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
}

Two-line list

Two-line list items contain a maximum of two lines of text.

Two-line list example

Image of three two-line list items with sample text

Objective-C

- (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;
}

Swift

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
}

Three-line list

Three-line list items contains a maximum of three lines of text.

Three-line list example

Image of three three-line list items with sample text

Objective-C

- (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;
}

Swift

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
}

Theming lists

This is an example of a two-line list with Shrine theming:

A two-line list item with example text and 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 project:

pod `MaterialComponents/List+Theming`

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

// 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];

Building your own list item

The example files can be found here

List Cell Example

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.

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: 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.

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:

    Objective-C

    _inkView = [[MDCInkView alloc] initWithFrame:self.bounds];
    _inkView.usesLegacyInkRipple = NO;
    [self addSubview:_inkView];
    

    Swift

    let inkView = MDCInkView(frame: bounds)
    inkView.usesLegacyInkRipple = false
    addSubview(inkView)
    
  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:

    Objective-C

    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
      UITouch *touch = [touches anyObject];
      CGPoint location = [touch locationInView:self];
      _lastTouch = location;
    
      [super touchesBegan:touches withEvent:event];
    }
    

    Swift

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
      let touch = touches.first
      let location = touch?.location(in: self)
      lastTouch = location
    }
    
  5. Override the setHighlighted method for your cell and apply the start and stop ripple animations:

    Objective-C

    - (void)setHighlighted:(BOOL)highlighted {
      [super setHighlighted:highlighted];
      if (highlighted) {
        [_inkView startTouchBeganAnimationAtPoint:_lastTouch completion:nil];
      } else {
        [_inkView startTouchEndedAnimationAtPoint:_lastTouch completion:nil];
      }
    }
    

    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...
    }
    
  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:

    Objective-C

    - (void)prepareForReuse {
      [_inkView cancelAllAnimationsAnimated:NO];
      [super prepareForReuse];
    }
    

    Swift

    override func prepareForReuse() {
      inkView.cancelAllAnimations(animated: false)
      super.prepareForReuse()
    }
    

    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:

    Objective-C

    NSLayoutConstraint *_imageLeftPaddingConstraint;
    NSLayoutConstraint *_imageRightPaddingConstraint;
    NSLayoutConstraint *_imageWidthConstraint;
    

    Swift

    var imageLeftPaddingConstraint: NSLayoutConstraint
    var imageRightPaddingConstraint: NSLayoutConstraint
    var imageWidthConstraint: NSLayoutConstraint
    

    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:

    Objective-C

    - (void)setCellWidth:(CGFloat)width {
      _cellWidthConstraint.constant = width;
      _cellWidthConstraint.active = YES;
    }
    

    Swift

    func set(cellWidth: CGFloat) {
      cellWidthConstraint.constant = cellWidth
      cellWidthConstraint.isActive = true
    }
    

    and then in the collection view's cellForItemAtIndexPath delegate method we set the width:

    Objective-C

    CGFloat cellWidth = CGRectGetWidth(collectionView.bounds);
    if (@available(iOS 11.0, *)) {
      cellWidth -=
        (collectionView.adjustedContentInset.left + collectionView.adjustedContentInset.right);
    }
    [cell setCellWidth:cellWidth];
    

    Swift

    var cellWidth = collectionView.bounds.width
    if #available(iOS 11.0, *) {
      cellWidth -= collectionView.adjustedContentInset.left + collectionView.adjustedContentInset.right
    }
    set(cellWidth: cellWidth)
    
  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.

    Objective-C

    _flowLayout.estimatedItemSize = CGSizeMake(kSmallArbitraryCellWidth, kSmallestCellHeight);
    

    Swift

    flowLayout.estimatedItemSize = CGSize(width: kSmallArbitraryCellWidth, 
                                         height: kSmallestCellHeight)
    

Typography

For our example we use a typography scheme to apply the fonts to our cell's UILabels. Please see Typography Scheme for more information.

Dynamic Type

Dynamic Type 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:

    Objective-C

    - (void)updateTitleFont {
      if (!_titleFont) {
        _titleFont = defaultTitleFont();
      }
      _titleLabel.font =
        [_titleFont mdc_fontSizedForMaterialTextStyle:MDCFontTextStyleSubheadline
                                 scaledForDynamicType:_mdc_adjustsFontForContentSizeCategory];
      [self setNeedsLayout];
    }
    

    Swift

    func updateTitleFont() {
      if (_titleFont == nil) {
        _titleFont = defaultTitleFont
      }
      _titleLabel.font = 
          _titleFont.mdc_fontSized(forMaterialTextStyle: .subheadline, 
                                   scaledForDynamicType: mdc_adjustsFontForContentSizeCategory)
    }
    
  2. Add an observer in the cell to check for the UIContentSizeCategoryDidChangeNotification which tells us the a system-wide text size has been changed.

    Objective-C

    [[NSNotificationCenter defaultCenter]
        addObserver:self
           selector:@selector(contentSizeCategoryDidChange:)
               name:UIContentSizeCategoryDidChangeNotification
             object:nil];
    

    Swift

    NotificationCenter.default.addObserver(self, 
                                           selector: #selector(contentSizeCategoryDidChange(notification:)), 
                                           name: UIContentSizeCategory.didChangeNotification, 
                                           object: nil)
    

    In the selector update the font sizes to reflect the change:

    Objective-C

    - (void)contentSizeCategoryDidChange:(__unused NSNotification *)notification {
      [self updateTitleFont];
      [self updateDetailsFont];
    }
    

    Swift

    func contentSizeCategoryDidChange(_: NSNotification) {
      updateTitleFont()
      updateDetailsFont()
    }
    
  3. Add an observer also in the UIViewController so we can reload the collection view once there is a change:

    Objective-C

    - (void)contentSizeCategoryDidChange:(__unused NSNotification *)notification {
      [self.collectionView reloadData];
    }
    

    Swift

    func contentSizeCategoryDidChange(_: NSNotification) {
      collectionView.reloadData()
    }
    

    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:

    Objective-C

    #if defined(__IPHONE_11_0) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0)
    if (@available(iOS 11.0, *)) {
      self.collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAlways;
    }
    #endif
    

    Swift

    if #available(iOS 11.0, *) {
      collectionView.contentInsetAdjustmentBehavior = .always
    }
    

    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:

    Objective-C

    - (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

    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()
      }
    }
    

Right to left text support

To support right to left text we need to import MDFInternationalization:

Objective-C

#import <MDFInternationalization/MDFInternationalization.h>

Swift

import MDFInternationalization

and for each of our cell's subviews me need to update the autoResizingMask:

Objective-C

_titleLabel.autoresizingMask =
    MDFTrailingMarginAutoresizingMaskForLayoutDirection(self.mdf_effectiveUserInterfaceLayoutDirection);

Swift

_titleLabel.autoresizingMask =
  MDFTrailingMarginAutoresizingMaskForLayoutDirection(mdf_effectiveUserInterfaceLayoutDirection)