Scalable Material Design Cards with UICollectionViews

HammyAssassin
5 min readJan 27, 2020

--

Intro

Cards are an important part of mobile and web development. They allow us to group meaningful content or information together. When applications need to display large amounts of content to the user, Cards allow us to display it clearly and concisely and allow us to separate groups of information visually. It can help improve navigation in our applications, allowing the user to interact with either the Card or its contents in very customizable ways.

Two Cards with different numbers of Items

Cards can be deceivingly difficult to construct. In the screenshot above there are 4 distinct components.

  • Title text
  • Image and text
  • Text
  • Footer

Items can appear in a Card more than once — We could have N Image and text views in a Card. It has no fixed arrangement — the text could be above the Image and text. Our simple text can be of variable height. Each item is contained in a view with a border and a shadow. And finally, a Card can be of variable height with possibly a fixed max height. With these requirements in mind, we need to think about how to construct these items so they are reusable, have a flexible layout, and more importantly, easily extendable should a new item be designed.

With some initial investment in developing an architecture for your Cards, you can easily expand the UI when needed. We don't want to be modifying the flow every time a new part of the card shows up. We want to build modular, reusable components that are data-driven.

Protocols

In this project, I have defined a few helpful protocols.

Cell ViewModel CardViewModelProtocol CardItemViewModel

Protocol definitions to help define the interactions between objects.

These are simple protocols that help define some of the relationships between objects, and allow us to generalize some methods. For example, all table view and collection view cells that conform to cell implement updateWith(viewModel: ViewModel?) . In each of the implementations, we perform an optional cast to the expected type, otherwise return. This is one of the powerful uses of protocols and optionality in Swift.

Example use of updateWith(viewModel: ViewModel?) in a concrete cell type

A side note on MVVM

MVVM (Model-View-ViewModel) is a common iOS architecture. It's quite lightweight in terms of defined classes and objects and is a great way to remove some of the business logic from our views and View Controllers. These classes can get large with layout code, property definitions, and lifecycle method implementations let alone including business logic. If you’d like to learn more about MVVM, here are some great articles.

I’ve used it in this sample app but feel free to avoid it if you prefer.

CollectionTableCell

The CollectionTableCell is the UITableViewCell that will contain a UICollectionView. If we want to display a single Card — We will be displaying a UITableView with a single Cell.

A single CollectionTableCell, containing a UICollectionView

For every Card, we will display a CollectionTableCell. Their data source is a CardViewModel. CardViewModel contains an array of ItemViewModels — Its data-source. Each of these is displayed using a custom UICollectionViewCell. This is the root of our reusability and data-driven architecture. When the data-source changes, our Card will change. The order, number, and contents of each item is defined by this data-source.

Each ItemCell is registered in our UICollectionView. When a new ItemCell is created it needs to be registered with CollectionTableCell.

The estimatedItemSize in our CardFlowLayout is set to .automaticSize. This allows our cells to be sized with Auto Layout.

Contained in each Cell is a CollectionContainer UIView. This allows us to add a drop shadow to the Card. CollectionContainer has a bottom constraint to the ContentView of +20. This creates the spacing between cards.

This spacing constraint is reflected in systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize. Any constraints added to the Cell should be reflected in this method.

CardFlowLayout

Now is a good time to explore CardFlowLayout. The requirements for how the cells in the Collection View are laid out is simple. They need to fill the width of the Collection View regardless of landscape or portrait. Once the width of the item is set and using .automaticSize , Auto Layout will determine the height of each Item.

CardFlowLayout — lays out the collectionView in each TableCell

CardViewModel

CardViewModel is initialized with a Card model. For the example project, A TitleItemViewModel and an ImageItemViewModel is injected as the first and second objects in the data-source array. Each Item model in our Card is used to create anItemCellViewModel. Finally, a FooterItemViewmodel is appended.

CardViewModel — Constructs the dataSource for each Card.

CardItemCell

CardItemCell is a parent class of all the items in our CollectionView. It simply overrides preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes and returns the layout attributes for the cell.

Items

CardItems are simply custom UICollectionViewCells and their corresponding ViewModel. There's no limit to the number of Item types used in a Card. In the case of the sample application, 4 CardItems have been defined. TextItemCell, a TitleItemCell, FooterItemCell and an ImageItemCell.

Each Item has a Xib to help with Auto Layout. As each of these are subclasses of CardItemCell we don’t need to override preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes. Each Item implements the Cell protocol so that A ViewModel can easily be injected.

For example, the ImageItemCell and ViewModel are below.

ImageItemCell & ViewModel — An example of an Item used in a Card.

Full Project

You can find the full project here.

There are some useful protocols and extensions that are used that are not included in the code snippets above.

Future

There’s a lot more to be extended and improved here — the contents of each of the CardItemCells can be extracted to UIViews. So the layout can be used anywhere. Coupled with the fact that the views don’t contain any logic, we can see how we can quickly implement design changes by either quickly adding a new View, or by safely modifying an existing one. We would only need a single CardItemCell, and its contentView could be determined by the ViewModel.

The size of the CollectionTableCells could be cached and only invalidated when something changes their size such as a rotation.

--

--

HammyAssassin
HammyAssassin

Written by HammyAssassin

A surfer and a swimmer making his way as an Engineering Manager. Still likes to think of himself as an iOS engineer though.

No responses yet