# Lists [![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) [Lists](https://material.io/components/lists/) are continuous, vertical indexes of text or images. **Contents** * [Using lists](#using-lists) * [Single-line list](#single-line-list) * [Two-line list](#two-line-list) * [Three-line list](#three-line-list) * [Theming lists](#theming) * [Building your own list item](#building-your-own-list-item) - - - ## Using lists ### Installing In order to install lists with Cocoapods first add the List component subspec to your `Podfile`: ```bash pod 'MaterialComponents/List' ``` Then, run the following command: ```bash pod install ``` From there, import the relevant target or file and use your list item like you would any other `UICollectionViewCell`. #### Swift ```swift import MaterialComponents.MaterialList ``` #### Objective-C ```objc #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](https://material.io/go/design-lists). 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](docs/assets/list_base_cell_animated.gif) #### `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](docs/assets/list_stereo_cell_animated.gif) 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 ```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]; ``` ### Making lists accessible #### Setting `-isAccessibilityElement` 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. #### Swift ```swift cell.isAccessibilityElement = true ``` #### Objective-C ```objc 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](docs/assets/list-content-anatomy.png) This list item consists of the following attributes: 1. Leading image view 1. Title label and detail label 1. 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](#single-line-list), 2\. [Two-line list](#two-line-list) 3\. [Three-line list](#three-line-list) ![Composite image of the three list types](docs/assets/lists-types.png) ## 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](docs/assets/single-line-list-example.png) #### Swift ```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 ```objc - (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](docs/assets/two-line-list-example.png) #### Swift ```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 ```objc - (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](docs/assets/three-line-list-example.png) #### Swift ```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 ```objc - (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](docs/assets/shrine-list.png) 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`: ```bash pod `MaterialComponents/List+Theming` ``` Then run the installer: ```bash pod install ``` From there, call the theming method from your `UICollectionViewDelegate` code. #### 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 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](docs/assets/listcellexample.gif) 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. 1. Initialize `MDCInkView` on init and add it as a subview: #### Swift ```swift let inkView = MDCInkView(frame: bounds) inkView.usesLegacyInkRipple = false addSubview(inkView) ``` #### Objective-C ```objc _inkView = [[MDCInkView alloc] initWithFrame:self.bounds]; _inkView.usesLegacyInkRipple = NO; [self addSubview:_inkView]; ``` 1. Initialize a `CGPoint` property in your cell (`CGPoint _lastTouch;`) to indicate where the last tap was in the cell. 1. 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 ```swift override func touchesBegan(_ touches: Set, with event: UIEvent?) { let touch = touches.first let location = touch?.location(in: self) lastTouch = location } ``` #### 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]; } ``` 1. Override the `setHighlighted` method for your cell and apply the start and stop ripple animations: #### 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... } ``` #### Objective-C ```objc - (void)setHighlighted:(BOOL)highlighted { [super setHighlighted:highlighted]; if (highlighted) { [_inkView startTouchBeganAnimationAtPoint:_lastTouch completion:nil]; } else { [_inkView startTouchEndedAnimationAtPoint:_lastTouch completion:nil]; } } ``` 1. 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 ```swift override func prepareForReuse() { inkView.cancelAllAnimations(animated: false) super.prepareForReuse() } ``` #### Objective-C ```objc - (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 ```swift var imageLeftPaddingConstraint: NSLayoutConstraint var imageRightPaddingConstraint: NSLayoutConstraint var imageWidthConstraint: NSLayoutConstraint ``` #### Objective-C ```objc NSLayoutConstraint *_imageLeftPaddingConstraint; NSLayoutConstraint *_imageRightPaddingConstraint; NSLayoutConstraint *_imageWidthConstraint; ``` This is in order to support the changing layout if an image is set or not. 1. 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 ```swift func set(cellWidth: CGFloat) { cellWidthConstraint.constant = cellWidth cellWidthConstraint.isActive = true } ``` #### Objective-C ```objc - (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 ```swift var cellWidth = collectionView.bounds.width if #available(iOS 11.0, *) { cellWidth -= collectionView.adjustedContentInset.left + collectionView.adjustedContentInset.right } set(cellWidth: cellWidth) ``` #### Objective-C ```objc CGFloat cellWidth = CGRectGetWidth(collectionView.bounds); if (@available(iOS 11.0, *)) { cellWidth -= (collectionView.adjustedContentInset.left + collectionView.adjustedContentInset.right); } [cell setCellWidth:cellWidth]; ``` 1. 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 ```swift flowLayout.estimatedItemSize = CGSize(width: kSmallArbitraryCellWidth, height: kSmallestCellHeight) ``` #### Objective-C ```objc _flowLayout.estimatedItemSize = CGSizeMake(kSmallArbitraryCellWidth, kSmallestCellHeight); ``` ### Typography 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 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 ```swift func updateTitleFont() { if (_titleFont == nil) { _titleFont = defaultTitleFont } _titleLabel.font = _titleFont.mdc_fontSized(forMaterialTextStyle: .subheadline, scaledForDynamicType: mdc_adjustsFontForContentSizeCategory) } ``` #### Objective-C ```objc - (void)updateTitleFont { if (!_titleFont) { _titleFont = defaultTitleFont(); } _titleLabel.font = [_titleFont mdc_fontSizedForMaterialTextStyle:MDCFontTextStyleSubheadline scaledForDynamicType:_mdc_adjustsFontForContentSizeCategory]; [self setNeedsLayout]; } ``` 1. Add an observer in the cell to check for the `UIContentSizeCategoryDidChangeNotification` which tells us the a system-wide text size has been changed. #### Swift ```swift NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange(notification:)), name: UIContentSizeCategory.didChangeNotification, object: nil) ``` #### Objective-C ```objc [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(contentSizeCategoryDidChange:) name:UIContentSizeCategoryDidChangeNotification object:nil]; ``` In the selector update the font sizes to reflect the change: #### Swift ```swift func contentSizeCategoryDidChange(_: NSNotification) { updateTitleFont() updateDetailsFont() } ``` #### Objective-C ```objc - (void)contentSizeCategoryDidChange:(__unused NSNotification *)notification { [self updateTitleFont]; [self updateDetailsFont]; } ``` 1. Add an observer also in the `UIViewController` so we can reload the collection view once there is a change: #### Swift ```swift func contentSizeCategoryDidChange(_: NSNotification) { collectionView.reloadData() } ``` #### Objective-C ```objc - (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 ```swift if #available(iOS 11.0, *) { collectionView.contentInsetAdjustmentBehavior = .always } ``` #### 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 ``` 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 ```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 ```objc - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection { [super traitCollectionDidChange:previousTraitCollection]; [self.collectionView.collectionViewLayout invalidateLayout]; [self.collectionView reloadData]; } - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)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 ```swift import MDFInternationalization ``` #### Objective-C ```objc #import ``` and for each of our cell's subviews me need to update the `autoResizingMask`: #### Swift ```swift _titleLabel.autoresizingMask = MDFTrailingMarginAutoresizingMaskForLayoutDirection(effectiveUserInterfaceLayoutDirection) ``` #### Objective-C ```objc _titleLabel.autoresizingMask = MDFTrailingMarginAutoresizingMaskForLayoutDirection(self.effectiveUserInterfaceLayoutDirection); ```