As an iOS developer, it is very common to face situations where you must deal with collections to display multiple content widgets in the same screen. I'm talking about tableviews and collection views. They are very useful to handle different contents that should be displayed as a list or a some kind of grid UI structure.
Apple really did a fantastic way to feed tableviews(and collection views) by giving us the power to define exactly which item should be filled in a given row and section, how many items does the table have, which height by each cell, how should a section identified by a given index be customised, etc. All you need is an object that implements UITableViewDataSource and UITableViewDelegate protocols defining the internal behaviour of your table view.
Respectively, the protocols described above care about what is the content to be displayed with which appearance and what should happen when some predefined events are triggered in our tableview(such as a cell was tapped at an index path, a cell disappeared or maybe the table just bounced).
This framework works good when you have a totally homogeneous collection of cells just displaying analogous instances of some kind of data, because you just need a list to be accessed in each index and map to the a tableview position at an index path:
As you can see in the image, a UITableView can consume the content of other indexed data structures like arrays and multidimensional arrays to display in its UI. The given tableview has the exact same number of rows in its single unique sections as the array "array" and each cell(which are default UITableViewCell instances) has the element of the array correspondent of its index path. The same for the heights that are taken from the "heights" array to map each cell's height.
This model works fine to homogeneous tableviews, as each method only uses an index path and a tableview to determine its info, but what about if we had to display different types of cell in a single table view? If each set of cells has a different source of information? That would be complicated, as long as we need to code nested "ifs" to check an index path and determine which type of UITableViewCell should be filled in that position.
I know you were excited when you first learned about tableviews because you thought it would be simple as that, but in real life, you may deal with a lot of complex screens that use tableviews to display lots of different contents. It is time to learn about some architecture to build a simple way of building a tableview exactly with the types of cell and data you are supposed to display. Fortunately, i am here to teach you a great solution I learned while working in a bank app: the UITableView factory pattern.
There are some protocols I will teach you in this tutorial, but the basic idea is to delegate all the UITableView protocol methods to a single object that will have an array of sections, also defined as protocols, each one also having an array of objects that shall each one create a tableview cell. The creation of the array of sections is delegated to an object known as the Factory.
Well, enough of talking, let's talk about the special protocols we are aiming in this tutorial which guarantee the flexible building of an heterogeneous tableview:
This protocol corresponds to the most basic data structure which must instantiate an UITableViewCell according to its index path and makes its setup. It is responsible for defining the particular properties of a tableview cell such as its heigh. The builder must also implement a method for registering the UITableViewCell subtype to its respective tableview(s).
Please, take a look at its appearance:
Note that we have three mandatory methods:
- registerCell: takes the respective type of the tableview cell we are dealing and registers in a tableview
- cellHeight: returns the height the cell in case must have
- cellAt: creates the tableview cell dequeuing from a tableview with an index path
It is important to mention that a cell builder is only meant to be an interface to that cell, so, it only builds everything we need to see that cell in the tableview. Each builder is associated with only with a cell type.
Let me illustrate with an example:
This builder was built in one of my projects, which works with a tableview listing characters. Note that it has an image URL and a name for presenting the character and a height property to be the cell height. The builder registers its associated UITableViewCell in the tableview and when instantiating the cell, it makes its setup before returning it. It is the basic mechanism of a TVCBuilder.
As the name tells, this protocol is responsible for a tableview section data source. It must tell us about how a section is built and which kind of cells it must have. It must have an array of cell builders, which can be totally heterogeneous and every time we want to retrieve a cell or a cell's property which comes from a builder, we retrieve it having an index path.
See that each tableview section which implements this protocol is composed by an array of cell builders and other properties describing how we see this section UI on the device screen. Here we have a bunch methods that we usually access when defining a tableview section in a UITableViewDataSource instance.
For a better notion what i am talking about, let's define the most basic type of a tableview section bellow:
We created a BaseSection type, which implements the TVSectionProtocol. As you can see, this is supposed to only hold the builders of tableview cells and when requested, retrieve specific information a cell through its builder and index path.
We can split those methods into two different categories:
- Header information: A tableview section may have a header view which defines some appearance to that section(Like a title or something like that). We have two methods to define that header: headerView, which returns a UIView corresponding to the header and heightForHeader, defining its height. As you can see above, or BaseSection does not have a header as we are returning nil to its view and 0 to its height
- Cells information: Methods which define the cells content in this section. We have three methods: first, numberOfRows, which tells us how many cells we have by retrieving the array's size. The other two depend on an index path to access a cell's builder and retrieve its information through the builder protocol methods.
It is important to note that we created a mechanism that always avoid cell registering errors. As we call the register method for each builder in the section initialiser, we are always handling cells which are known by the tableview.
You must be used to make the tableview's own view controller the instance of its data source and delegate. Almost every developer does that, but I would rather create another type for implementing those protocols since I like to follow the SOLID principles. What i like to do is merge the two tableview protocols into a single one called TableViewOutput and then create a new type for implementing those methods for a specific tableview.
Take a look at what I did:
I defined a new type named TableViewOutput, which implements both UITableViewDelegate and UITableViewDataSource. The DefaultTVOutput that we created above must handle all the tableview delegate events and at the same time describe the content of its sections and cells.
The data about a specific section is retrieved by accessing the section through the indexPath.section and the data about a section's cell is defined by accessing the section and then the builder of that cell through the indexPath.row.
Building our TableViewFactory
With all this we created a hierarchy for our architecture:
The tasks sequence shall be:
- The UIViewController, which holds a UITableView must create a tableview factory giving as parameters the tableview, the data models to be filled in the cells, and any kinds of delegates the cells must have to notify their interactions.
- The factory creates the sections through its method buildSections defined in its protocol
- When creating a section, the factory must send the tableview cell builders in its initialiser.
- When being initialised, the section instance register all the cells associated with the builders.
- When the data source with the sections and cells is finally done, it is sent to the tableview output by the view controller. Don't forget to define the DefaultTableViewOutput instance as the delegate of your tableview.
We haven't defined the core of our factory yet, it would be like that:
Observe that the only needed method is buildSections. Everything else is defined as a private method or computed variable defining fixed sections or builders to be accessed by the section builder. In this factory we have one single section which is composed by the character builders and loading builders. The parameters of the factory are a character model array which shall feed the builders, a selected index, the tableview and some cell constants to be sent to the builders. It is an old project of mine which I won't discuss details now. Which is really important is the building process of a tableview.
Now that the factory has created all the data source our tableview will consume, we need to send the sections array to the tableview output, which will define exactly the shape of our tableview:
As you can see, the two methods that are called in sequence create the sections through the factory and send it to the default output assigning it as the tableview output. Now everything is ready to be displayed on the screen
In this article we described about a very important architecture to making easy to create heterogenous tableviews by defining some core protocols and some retrieving mechanisms to find exactly the kind of cell we want at the right index path. With all this, we don't need to worry about conditionals for each index path in the tableview methods. It is important to say that a collection view factory would behave in a very similar way.
I hope you enjoyed, and if you are interested in seeing how I built my characters project, leave your clap here. See ya ;)