Table View Controllers

Earlier we were talking about "containers" for our screens, and now we will talk about actual screens.

As you may remember, Table View Controllers are a blank View Controllers with a full-screen Table View inside. Also, we were talking that it is better not to use TableViewController, but to create a full-screen table inside of a plain screen. We already have it implemented in our MoodHistoryScreen.

I'm going to remind you how to create a full-screen table inside a plain screen. First, we would need to create a layout with UITablewView, and set its position on the screen and size:

        class SomeLayout < MotionKit::Layout
          def layout
            add UITableView, :table
          end

          def table_style
            constraints do
              x 0
              y 0
              right.equals(:superview, :right)
              bottom.equals(:superview, :bottom)
            end
          end
        end
      
Then we will need to tell our screen to use this layout:
        class SomeScreen < PM::Screen
          tab_bar_item system_item: UITabBarSystemItemHistory

          def load_view
            @layout = MoodHistoryLayout.new
            self.view = @layout.view
          end
        end
      

This is very similar to the code that we already have for our MoodHistoryScreen. However if you launch an app, our table will be empty because it does not have any data to show. So how to load some data into the table?

While we don't have any database, let's save all our data in an instance variable @data. Since the table can have different sections, and each section can have different cells set and title, I would suggest using an array with hashes inside of it. I'm going to create a method set_table_data to keep our on_load light:

        class MoodHistoryScreen < PM::Screen
          title "Mood History"
          tab_bar_item system_item: UITabBarSystemItemHistory

          def load_view
            @layout = MoodHistoryLayout.new
            self.view = @layout.view
          end

          def on_load
            self.view.backgroundColor = 0xffffff.uicolor
            set_table_data
          end

          def set_table_data
            @data = [
              # this is a table section:
              {
                title: "July 23",
                # these are section's cells:
                cells: [
                  {
                    date: "Day",
                    mood: "3",
                  }, {
                    date: "Morning",
                    mood: "4",
                  }
                ]
              }, {
                # here goes second section:
                title: "July 22",
                # these are section's cells:
                cells: [
                  {
                    date: "Evening",
                    mood: "4",
                  }, {
                    date: "Day",
                    mood: "3",
                  }, {
                    date: "Morning",
                    mood: "4",
                  }
                ]
              }
            ]
          end
        end
      

The only thing we have added here is a call to set_table_data inside on_load method. set_table_data initialized @data instance variable that will be accessible inside any instance method. In this example, we have two sections with two and three cells per section. However if we will launch the app now, nothing will be changed - the table does not know where to get the data and what to show. We will need to use Table View Delegate methods that will help our table to figure out how many sections and row to show, and how to configure the cell.

Table View Delegate has 3 main methods:

There are also lots of other useful methods that will help you to configure your table. For example tableView(table, titleForHeaderInSection: section) that should return title for a given section, or tableView(table, heightForRowAtIndexPath:index) that should return a cell's height for a given index.

Let's start with main delegate methods first. But in the beginning, we need to tell our table where to look for delegate methods:

        def on_load
          self.view.backgroundColor = 0xffffff.uicolor
          # get the table from layout:
          table = @layout.get(:table)

          # tell it where delegate methods can be found.
          # In our case they are inside of a current screen:
          table.dataSource = self
          table.delegate = self

          # tell table what cell class will be used:
          table.registerClass(MoodHistoryCell, forCellReuseIdentifier: "reuse_id")

          set_table_data
        end
      
Everything should be clear except registerClass line. Let's talk about it.

Table view may show different cells. You can show cells with images, text-only cells, cells with some switches, etc. However, each cell has its own class, and we need to tell table which cell classes it will need to render. It is similar to the way we tell screen what layout it should use.

Cells creation is an expensive procedure and may take some time. It may result in a bad table performance, in slow cells rendering during scrolling, etc. Therefore instead of creating new cells iOS caches already created ones and reuses them. Reuse Identifier here helps to load correct cell class for a given cell: you won't be happy if iOS would show a cell with text when it has to show a cell with an image. It may sound difficult at this point, but you will get it in a moment.

Now when we have the table set up, we can continue to tableView delegate methods. Next step is to tell how many sections table will have. We are going to use numberOfSectionsInTableView(table) method, which has to return a number of a section in a given table. You can return a hardcoded value:

        def numberOfSectionsInTableView(table)
          2
        end
      
Or you can use dynamic values:
        def numberOfSectionsInTableView(table)
          @data.count
        end
      
If you did follow our examples, both would work well. However latter one is preferable - if you change your @data variable in the future, the table will be able to show a correct number of sections - numberOfSectionsInTableView(table) method will be called each time table updates.

The third step is to set the number of rows (cells) for a given section. Delegate method tableView(table, numberOfRowsInSection: section) always passes section's number, and we can always use it to get a number of rows for a given sections. You can again use hardcoded or dynamic values, but it is a good idea to always use dynamic ones:

        def tableView(table, numberOfRowsInSection: section)
          @data[section][:cells].count
        end
      
This code may look difficult for a novice developer. Here we get our @data variable and trying to get an object on the index section. Do you remember how we worked with arrays?
        arr = ["a", "b", "c", "d"]
        arr[0] # will return object on index 0, which is "a"
        arr.first # will return first object in array, which is also "a"
        arr[3] # will return 4th object, which is "d"
        arr[-1] # will return last object, which is "d"
      
Knowing that section variable is always a number, it is safe to say that @data[section] will return a correct object. However in our case @data is not an array of characters, but an array of hashes. Therefore @data[section] will return a Hash that represents section. In our example section Hash looks similar to { title: "Section Title", cells: [ ... ] }. Since we have section Hash, we just need to get cells count now. To do it, we use: @data[section][:cells].count.

Now app knows how many sections and cells has to be rendered, but the app still does not know how to show a cell. We will use tableView(table, cellForRowAtIndexPath: index) method, that should return a valid cell into the table:

        def tableView(table, cellForRowAtIndexPath: index)
          # remember we were talking about reuse identifiers?
          # here we get a cell by given reuse_id:
          cell = table.dequeueReusableCellWithIdentifier("reuse_id", forIndexPath: index)

          # get our cell's data:
          item = @data[index.section][:cells][index.row]

          # set cell's title to our item's title
          cell.main_text = item[:date]
          cell.mood_text = item[:mood]

          # return cell to the table so that it can render it:
          cell
        end
      

Only one thing left - we need to create our MoodHistoryCell that will be a subclass of UITableViewCell, and will have two labels: main with a large text, and secondary with a smaller text. Also, we will need two methods that will set this text. In our previous example we used main_text and mood_text, so let's use these method names. First, create a folder /app/cells and /app/layouts/cell. I will also create /app/layouts/screens, and will put all our current screen layouts there. Then you will need to create one file for a cell, and one for a cell layout. For example: /app/cells/mood_history_cell.rb and /app/layouts/cells/mood_history_layout.rb.

MoodHistoryCell code should be pretty straightforward. We will use cell's method initWithStyle(style, reuseIdentifier:reuseIdentifier) to init our layout. Then we will also create two methods that will set the text to our main and secondary labels. Remember that all cells should subclass from UITableViewCell class:

        class MoodHistoryCell < UITableViewCell
          def initWithStyle(style, reuseIdentifier:reuseIdentifier)
            super

            # we have to render our layout inside cell's contentView.
            # we pass it as a `root` parameter, which will be later
            # accessible in the Layout:

            @layout = MoodHistoryCellLayout.new(root: self.contentView).build
            @main_text = @layout.get(:title)
            @mood_text = @layout.get(:mood)

            self
          end

          def main_text=(text)
            @main_text.text = text
          end

          def mood_text=(text)
            @mood_text.text = text
          end
        end
      
You can find new keyword super. We are calling it because we are overriding default cell's method: initWithStyle(style, reuseIdentifier:reuseIdentifier). By calling super, we allow its superclass, which is UITableViewCell to launch its own initWithStyle(style, reuseIdentifier:reuseIdentifier) first, and then goes our part of this method. Basically, we just add some code the end of the already existed method.

Then we are adding our layout. This code is a little bit different from what we use for screens: we tell motion-kit that all layout should be inside contentView of a cell. After we are creating variables for both our labels to use them in the future.

Now goes cell's layout. We just want to add two labels - one with a smaller text in the right side of the cell, and second with a larger text in the right side of the cell. We don't want our labels to start right from the border of the cell, so we will add 15 points padding on both sides:

        class MoodHistoryCellLayout < MotionKit::Layout
          def layout
            # remember we talked that cell's layout should be inside its
            # contentView? We passed it as a `root` parameter. Here
            # we give our root view name `cell`. We can set its style
            # just like any other view. In our case, it would be inside
            # `cell_style` method.
            root :cell do
              add UILabel, :title
              add UILabel, :mood
            end
          end

          def title_style
            font          "Helvetica".uifont(16)
            text_color    0x000000.uicolor
            numberOfLines 1

            constraints do
              center_y.equals(:superview, :center_y)
              left.equals(:superview, :left).plus(15)
              right.equals(:mood, :left).minus(15)
            end
          end

          def mood_style
            font          "Helvetica".uifont(20)
            text_color    0x000000.uicolor
            numberOfLines 1

            constraints do
              center_y.equals(:superview, :center_y)
              right.equals(:superview, :right).minus(15)
            end
          end
        end
      

Finally, we can launch the app. Table knows where to look for its delegate methods, delegate methods return a correct number of sections and rows, table knows how to render cells. We also have our custom MoodHistoryCell with its layout ready. When launching the app, we will see something like this in a History tab:

Cool! We finally have a working table view that shows some data. It even has some custom cells that we just made. You can try to change @data variable by adding/removing new sections or cells, and the table will use your updated table data automatically on each launch.

There is only one thing left - we need to show section titles. Remember tableView(table, titleForHeaderInSection: section) method? Let's try to make it return section's title:

        def tableView(table, titleForHeaderInSection: section)
          @data[section][:title]
        end
      
With this simple method iOS will understand that you want to show sections, and will start render for you. Try to launch the app. Looks good, eh?

At this moment, we are using plain table view. However, there are two table types: plain and grouped. Since we group user's mood history by dates, grouped would look better here. Let's change our MoodHistoryLayout to use grouped UITablewView instead. Unfortunately, there is no way to do it without dirtying the code - Apple does not have any style property that would change table's style. We have to provide it during initialization:

        class MoodHistoryLayout < MotionKit::Layout
          def layout
            grouped_table = UITableView.alloc.initWithFrame CGRectZero, style: UITableViewStyleGrouped
            add grouped_table, :table
          end

          # rest of MoodHistoryLayout code ...
        end
      
Now our table will have a grouped style:

As usual, table view docs can be found at Apple Developer Website. There you can also find Table View Delegate Protocol and Table View DataSource Protocol

Summary

Table Views are main UI element in iOS. To set up a screen with a table view, you will need:

Book Index | Next