Skip to content

Latest commit

 

History

History
789 lines (608 loc) · 25.6 KB

File metadata and controls

789 lines (608 loc) · 25.6 KB

Lists

Open bugs badge

Lists are continuous, vertical indexes of text or images.

Contents


Using lists

Installing

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.

Swift

import MaterialComponents.MaterialList

Objective-C

#import "MaterialList.h"

List classes

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

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

  Attribute Related methods Default value
Color rippleColor -setRippleColor:
-setRippleColor
On surface color at 0.12 opacity
Elevation elevation -setElevation:
-elevation
0

Icon attributes

  Attribute Related methods Default value
Leading image leadingImageView N/A N/A
Trailing image trailingImageView N/A N/A

Text label attributes

  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

Composite image of the three list types

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

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
}

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

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

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
}

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

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

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
}

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

Theming

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

pod `MaterialComponents/List+Theming`

Then run the installer:

pod install

From there, call the theming method from your UICollectionViewDelegate code.

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:

    Swift

    let inkView = MDCInkView(frame: bounds)
    inkView.usesLegacyInkRipple = false
    addSubview(inkView)

    Objective-C

    _inkView = [[MDCInkView alloc] initWithFrame:self.bounds];
    _inkView.usesLegacyInkRipple = NO;
    [self 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:

    Swift

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
      let touch = touches.first
      let location = touch?.location(in: self)
      lastTouch = location
    }

    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];
    }
  5. Override the setHighlighted method for your cell and apply the start and stop ripple animations:

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

    Objective-C

    - (void)setHighlighted:(BOOL)highlighted {
      [super setHighlighted:highlighted];
      if (highlighted) {
        [_inkView startTouchBeganAnimationAtPoint:_lastTouch completion:nil];
      } else {
        [_inkView startTouchEndedAnimationAtPoint:_lastTouch completion:nil];
      }
    }
  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:

    Swift

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

    Objective-C

    - (void)prepareForReuse {
      [_inkView cancelAllAnimationsAnimated:NO];
      [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:

    Swift

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

    Objective-C

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

    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:

    Swift

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

    Objective-C

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

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

    Swift

    var cellWidth = collectionView.bounds.width
    if #available(iOS 11.0, *) {
      cellWidth -= collectionView.adjustedContentInset.left + collectionView.adjustedContentInset.right
    }
    set(cellWidth: cellWidth)

    Objective-C

    CGFloat cellWidth = CGRectGetWidth(collectionView.bounds);
    if (@available(iOS 11.0, *)) {
      cellWidth -=
        (collectionView.adjustedContentInset.left + collectionView.adjustedContentInset.right);
    }
    [cell setCellWidth: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.

    Swift

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

    Objective-C

    _flowLayout.estimatedItemSize = CGSizeMake(kSmallArbitraryCellWidth, 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:

    Swift

    func updateTitleFont() {
      if (_titleFont == nil) {
        _titleFont = defaultTitleFont
      }
      _titleLabel.font = 
          _titleFont.mdc_fontSized(forMaterialTextStyle: .subheadline, 
                                   scaledForDynamicType: mdc_adjustsFontForContentSizeCategory)
    }

    Objective-C

    - (void)updateTitleFont {
      if (!_titleFont) {
        _titleFont = defaultTitleFont();
      }
      _titleLabel.font =
        [_titleFont mdc_fontSizedForMaterialTextStyle:MDCFontTextStyleSubheadline
                                 scaledForDynamicType:_mdc_adjustsFontForContentSizeCategory];
      [self setNeedsLayout];
    }
  2. Add an observer in the cell to check for the UIContentSizeCategoryDidChangeNotification which tells us the a system-wide text size has been changed.

    Swift

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

    Objective-C

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

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

    Swift

    func contentSizeCategoryDidChange(_: NSNotification) {
      updateTitleFont()
      updateDetailsFont()
    }

    Objective-C

    - (void)contentSizeCategoryDidChange:(__unused NSNotification *)notification {
      [self updateTitleFont];
      [self updateDetailsFont];
    }
  3. Add an observer also in the UIViewController so we can reload the collection view once there is a change:

    Swift

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

    Objective-C

    - (void)contentSizeCategoryDidChange:(__unused NSNotification *)notification {
      [self.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:

    Swift

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

    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

    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:

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

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

Right to left text support

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

Swift

import MDFInternationalization

Objective-C

#import <MDFInternationalization/MDFInternationalization.h>

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

Swift

_titleLabel.autoresizingMask =
  MDFTrailingMarginAutoresizingMaskForLayoutDirection(effectiveUserInterfaceLayoutDirection)

Objective-C

_titleLabel.autoresizingMask =
    MDFTrailingMarginAutoresizingMaskForLayoutDirection(self.effectiveUserInterfaceLayoutDirection);