Scalable Material Design Cards with UICollectionViews
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.
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
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.
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
.
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.
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.
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.
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.