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:
numberOfSectionsInTableView(table)
that specifies number of sections in a table;
tableView(table, numberOfRowsInSection: section)
that specifies number of cells (rows) in a given section;
tableView(table, cellForRowAtIndexPath: index)
that configures the cell for a given index;
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
Table Views are main UI element in iOS. To set up a screen with a table view, you will need:
delegate
and
dataSource
properties. Also,
don't forget to register what kind of cell you are going
to use with this table.