There is a lot of customization that you get "for free" by studying the UITableViewCell, which I recommend doing. Between the textLabel, detailTextLabel, imageView, and accessoryView, you can solve a lot of problems.

But if we went that route, this would not be a very interesting post! Let's build a custom view cell from scratch, and see what fun can be had with it.

The cell we're gonna lay out ends up like this:

  • tableview_cell_example.png

    A custom UITableViewCell

Brief aside

Here's a weird thing that surprised me while building this demo. I am using view tags, as you'll see, to reference the subviews of my custom UITableViewCell. I, being a programmer, numbered these "0" to "N-1", N being the number of subviews.

When run this way, the 0th view was ALWAYS at 0,0, and always stretched to the height of the cell. O_o?

Changing the numbering from 1 to N fixed this. Double ¿O_o?!

If I don't use a tag at all, and reference the UIImageView using cell.contentView.subviews[0], it also doesn't have the weird behavior.

The reason is simple: 0 is the default tag number, all the views have that number! So beware tag 0.

Moving on

We will load up a UITableView with some new data, because we're gonna need some more complicated examples than ol' firsty McLasty

app/my_application_controller.rb

# these will be used to reference our cell's subviews.
ICON_TAG = 1
TITLE_TAG = 2
DESCRIPTION_TAG = 3
LOGO1_TAG = 4
LOGO2_TAG = 5

class MyApplicationController < UIViewController

  def viewDidLoad
    @data = [
      { icon: 'niftywow',
        project: 'NiftyWow',
        description: "Oh you just can't imagine.",
        logos: ['apple', 'github']
        },
      { icon: 'amazingwhizbang',
        project: 'AmazingWhizBang',
        description: 'Much cooler than lame stuff.',
        logos: ['apple', 'twitter']
        },
      { icon: 'woahsupercool',
        project: 'WoahSuperCool',
        description: 'No beer and no TV make Colin something something. No beer and no TV make Colin something something. No beer and no TV make Colin something something.',
        logos: ['twitter', 'github']
        },
    ]
    @table = UITableView.alloc.initWithFrame [[0, 0], [320, 480]], style: UITableViewStylePlain
    @table.dataSource = self
    @table.delegate = self
    view.addSubview(@table)
  end

  def tableView(tableView, cellForRowAtIndexPath:indexPath)
    # dequeue or create a cell

    return cell
  end

  def cell_identifier
    @@cell_identifier ||= 'Cell'
  end

  def tableView(tableView, numberOfRowsInSection: section)
    case section
    when 0
      @data.length
    else
      0
    end
  end

Our cell is going to have 5 subviews:

  1. The icon on the left
  2. The title
  3. The description
  4. Random logo 1
  5. Random logo 2

The frames look like this:

  • tableview_cell_example_frames.png

    Highlighting the frames

In code:

def icon_frame
  [[5, 5], [40, 40]]
end

def title_frame
  [[50, 0], [245, 20]]
end

def description_frame
  [[50, 20], [245, 30]]
end

def logo1_frame
  [[295,  5], [20, 20]]
end

def logo2_frame
  [[295, 25], [20, 20]]
end

With those ready, we can jump into building and styling our views, and if we need to make changes to the positions, we've got those separated off.

First, dequeue a cell, or create a new one if that is unsuccessul.

def tableView(tableView, cellForRowAtIndexPath:indexPath)
  cell = tableView.dequeueReusableCellWithIdentifier(cell_identifier)

  if not cell
    cell = UITableViewCell.alloc.initWithStyle( UITableViewCellStyleDefault,
                        reuseIdentifier: cell_identifier)

Next create the fives views, and attach them to the cell.contentView. We are only creating the views, we are not assigning the label, image, etc.

The dequeueReusableCellWithIdentifier method is important for memory management. If you had a table with 1000+ rows, memory would become an issue.

But you only ever have 11-ish rows visible at a time, you don't need to allocate the memory for all these rows at one time, you can reuse existing cells.

The curve ball is that you might get handed a cell that already has content on it, so you have to be diligent about resetting dequeued cells.

God help you if you need to add/remove a view depending on some content. Better to show/hide the view than add/remove it. I won't be doing this kind of tom-foolery here.

Did I say we are going to create views? Here they are. We're still in the if not cell block:

  # the image view, on the left side
  icon_image_view = UIImageView.new
  icon_image_view.frame = icon_frame
  icon_image_view.tag = ICON_TAG
  cell.contentView.addSubview(icon_image_view)

  # the product title view, at the top; bigger and black
  title_view = UILabel.alloc.initWithFrame(title_frame)
  title_view.font = UIFont.systemFontOfSize(17)
  title_view.tag = TITLE_TAG
  cell.contentView.addSubview(title_view)

  # the description, with word wrap on so that it can extend to
  # the second line, and smaller and gray.
  description_view = UILabel.alloc.initWithFrame(description_frame)
  description_view.font = UIFont.systemFontOfSize(12)
  description_view.textColor = UIColor.grayColor
  description_view.lineBreakMode = UILineBreakModeWordWrap
  description_view.numberOfLines = 0
  description_view.tag = DESCRIPTION_TAG
  cell.contentView.addSubview(description_view)

  # the top logo
  logo1_view = UIImageView.alloc.initWithFrame(logo1_frame)
  logo1_view.tag = LOGO1_TAG
  cell.contentView.addSubview(logo1_view)

  # the bottom logo
  logo2_view = UIImageView.alloc.initWithFrame(logo2_frame)
  logo2_view.tag = LOGO2_TAG
  cell.contentView.addSubview(logo2_view)
else  # the cell *did* exist
  # assign the variable names using the tags we assigned above
  icon_image_view = cell.viewWithTag(ICON_TAG)
  title_view = cell.viewWithTag(TITLE_TAG)
  description_view = cell.viewWithTag(DESCRIPTION_TAG)
  logo1_view = cell.viewWithTag(LOGO1_TAG)
  logo2_view = cell.viewWithTag(LOGO2_TAG)
end

All the view building & styling is done, we'll assign this row's values to those views.

A quick note about the view tags. View tags are just integers that you can set on subviews, and on the parent view you fetch it with viewWithTag. The only number that is off limits is 0, as mentioned above (you'll get the first non-tagged view, hardly useful). In other words, setting the tag number to 0 is the same as untagging the view.

Sorry I'm being so verbose, I'm trying to make sure everything "sticks". I hate it when I read a blog and I'm like "wait, what are you doing HERE!? (pointing to what to many must be a simple, obvious lines of code)"

Now we assign our product properties.

project = @data[indexPath.row]

icon_image_view.image = UIImage.imageNamed(project[:icon])
title_view.text = project[:project]
description_view.text = project[:description]
description_view.frame = description_frame
description_view.sizeToFit
# if the height is too large, it will exceed the height I want it to be, so
# I will manually just cut it off.  comment these out to SEE WHAT HAPPENS :-|
frame = description_view.frame
frame.size.height = description_frame[1][1]
description_view.frame = frame
logo1_view.image = UIImage.imageNamed(project[:logos][0])
logo2_view.image = UIImage.imageNamed(project[:logos][1])

The last thing we do is set the height of the cell to a constant 50 pixels.

def tableView(tableView, heightForRowAtIndexPath:indexPath)
  50
end

When you click these, they just highlight and stay that way until you touch another row. That is the *cough* default behavior. Thanks Apple! I will implement the bare minimum to get rid of this behavior; this is code that you will want on all your UITableViews didSelectRowAtIndexPath delegate method. I used this in my last thought, too, but over there I followed it up with doing something useful.

def tableView(tableView, didSelectRowAtIndexPath:indexPath)
  tableView.deselectRowAtIndexPath(indexPath, animated:true)
end

The challenge

teacup is great for building a view that has lots of subviews, and for consistently styling an iOS application, and I'll post about it more after it is officially announced, but I wanted to share an experiment that I tried after Mark Villacamp shared his trials and tribulations.

I had a fair share of troubles getting everything plugged into teacup. Namely, I forgot to declare what stylesheet to use, and then I overwrote viewDidLoad without calling super (teacup implements viewDidLoad to load the stylesheet and create the view hierarchy).

Now that it's all setup correctly, it all worked great!

Setup

In your UIViewController, you usually assign a stylesheet using the stylesheet class method. The stylesheet can be changed, but it's not usually necessary (not using the new system, which has support for multiple orientations). You should, though, be checking for the correct device if that's appropriate. Read up on teacup styling for more information about that.

class MyApplicationController < UIViewController

  stylesheet  :cell_sheet  # I like adding '_sheet', otherwise this looks like a style name

teacup implements a UIViewController#viewDidLoad method, as I mentioned above. You can either call super in that method, or, the "teacup way" of doing it is to implement an alternative method layoutDidLoad. In this case it really doesn't matter, but teacup has a UIViewController class method layout that can be used kind of like a nib file - you build your root view in that method, and it will get instantiated before layoutDidLoad is called.

# change method name
def layoutDidLoad
  @data = [
    { icon: 'niftywow',
  # ... the rest stays the same

Refactor UIViewController

The bulk of the code we will change is in the tableView:cellForRowAtIndexPath: method. Instead of doing our styling there, we will just instantiate our views, putting them in the cell parent view, and give them style names. After this, we will create a stylesheet (and call it ":cell_sheet", since that's what we're using above) and put our styles in there.

This is teacup code and I'm just gonna throw it at you and hopefully you see what's going on. I'll explain after.

cell = UITableViewCell.alloc.initWithStyle( UITableViewCellStyleDefault,
                    reuseIdentifier: cell_identifier)

layout(cell.contentView, :cell) do
  icon_image_view = subview(UIImageView, :icon, tag: ICON_TAG)
  title_view = subview(UILabel, :title, tag: TITLE_TAG, font: UIFont.systemFontOfSize(17))
  description_view = subview(UILabel, :description, tag: DESCRIPTION_TAG, font: UIFont.systemFontOfSize(12))
  logo1_view = subview(UIImageView, :logo1, tag: LOGO1_TAG)
  logo2_view = subview(UIImageView, :logo2, tag: LOGO2_TAG)
end

The layout method accepts a view object to "layout", and a style name to apply to that view. The block is used to add subviews to the layout.

subview adds a view (¡surprise!). The style method accepts a view class or instance (a class name is instantiated using Class.new), a style name, and any additional properties you want to add to the view.

I'm using that third arg to assign tag: not only as an example, but because tag is not a style property, it is specific to my controller logic, so assigning it in my controller actually makes sense.

If I used this stylesheet on another view, for example, having the tag set could really mess things up. You picking up what I'm throwing down?

Style

OH, how I love moving code around. It feels like I'm getting so much done, and I usually don't break anything in doing so! "Huzzah!" for lateral movement.

Here's all the styling code, but refactored into a stylesheet. Things to note:

  • I'm using sugarcube here. Don't be scared by :gray.uicolor
  • the two logos share a little bit of code, so I use the extends: key to let them share that code.
Teacup::Stylesheet.new :cell_sheet do

  style :icon,
    frame: [[5, 5], [40, 40]]

  style :title,
    font: :system.uifont(17),
    frame: [[50, 0], [245, 20]]

  style :description,
    frame: [[50, 20], [245, 30]],
    font: :system.uifont(12),
    textColor: :gray.uicolor,
    lineBreakMode: UILineBreakModeWordWrap,
    numberOfLines: 0

  style :logo,
    left: 295,
    width: 20,
    height: 20

  style :logo1, extends: :logo,
    top: 5

  style :logo2, extends: :logo,
    top: 25

end