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/myapplicationcontroller.rb

“`ruby

these will be used to reference our cell’s subviews.

ICONTAG = 1 TITLETAG = 2 DESCRIPTIONTAG = 3 LOGO1TAG = 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 cellidentifier @@cellidentifier ||= ‘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:

”`ruby 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.

”`ruby 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:

”`ruby # the image view, on the left side iconimageview = UIImageView.new iconimageview.frame = iconframe iconimageview.tag = ICONTAG cell.contentView.addSubview(iconimageview)

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

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

# the top logo logo1view = UIImageView.alloc.initWithFrame(logo1frame) logo1view.tag = LOGO1TAG cell.contentView.addSubview(logo1_view)

# the bottom logo logo2view = UIImageView.alloc.initWithFrame(logo2frame) logo2view.tag = LOGO2TAG cell.contentView.addSubview(logo2view) else # the cell did exist # assign the variable names using the tags we assigned above iconimageview = cell.viewWithTag(ICONTAG) titleview = cell.viewWithTag(TITLETAG) descriptionview = cell.viewWithTag(DESCRIPTIONTAG) logo1view = cell.viewWithTag(LOGO1TAG) logo2view = cell.viewWithTag(LOGO2TAG) 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.

“`ruby project = @data[indexPath.row]

iconimageview.image = UIImage.imageNamed(project[:icon]) titleview.text = project[:project] descriptionview.text = project[:description] descriptionview.frame = descriptionframe 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 = descriptionview.frame frame.size.height = descriptionframe[1][1] descriptionview.frame = frame logo1view.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.

ruby 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.

ruby 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.

“`ruby class MyApplicationController < UIViewController

stylesheet :cellsheet # 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.

“`ruby

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.

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

layout(cell.contentView, :cell) do iconimageview = subview(UIImageView, :icon, tag: ICONTAG) titleview = subview(UILabel, :title, tag: TITLETAG, font: UIFont.systemFontOfSize(17)) descriptionview = subview(UILabel, :description, tag: DESCRIPTIONTAG, font: UIFont.systemFontOfSize(12)) logo1view = subview(UIImageView, :logo1, tag: LOGO1TAG) logo2view = 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.

“`ruby 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 ”`