Over a million developers have joined DZone.

Creating Custom Table View Cells in Markup

Tables are the most common UI element in mobile apps. Here's how to build them using MarkupKit

· Mobile Zone

Implementing custom table view cells has traditionally been one of the more challenging aspects of iOS development. In earlier versions of the OS, developers had to calculate cell sizes and position subviews manually, a time-consuming and error-prone process.

Since the introduction of layout constraints and self-sizing cells, the process has become simpler, but MarkupKit makes it even easier by allowing developers to define a cell's structure entirely in markup. Layout views such as LMColumnView and LMRowView can be used to automatically position the cell's subviews and respond to content and orientation changes, leaving the cell class itself responsible simply for providing the cell's behavior.

For example, the following screen shot shows a table view that presents a list of simulated pharmacy search results:

Custom Cell View Example

The table view's contents are defined by a JSON document containing the search results. In the example application, these results are static. In an actual application, they would probably be dynamically generated by some kind of web service:

[
    {
      "name": "Green Cross Pharmacy",
      "address1": "393 Hanover Street",
      "city": "Boston",
      "state": "MA",
      "zipCode": "02108",
      "latitude": 42.365142822266,
      "longitude": -71.052879333496,
      "phone": "6172273728",
      "email": "pharmacy@greencross.com",
      "fax": "6177420001",
      "distance": 0.15821025961609
    },
    {
      "name": "CVS",
      "address1": "263 Washington Street",
      "city": "Boston",
      "state": "MA",
      "zipCode": "02108",
      "latitude": 42.357696533203,
      "longitude": -71.058090209961,
      "phone": "6177427035",
      "email": "store01@cvs.com",
      "fax": "6177420001",
      "distance": 0.42181156854188
    },
    {
      "name": "Walgreens",
      "address1": "70 Summer Street",
      "city": "Boston",
      "state": "MA",
      "zipCode": "02108",
      "latitude": 42.354225158691,
      "longitude": -71.05818939209,
      "phone": "6172657488",
      "email": "store04@walgreens.com",
      "fax": "6177420001",
      "distance": 0.64790764418076
    },

    ...
]

The example table view controller loads the simulated result data in viewDidLoad and stores it in an instance variable named pharmacies. It also sets the estimatedRowHeight property of the table view to 2. Setting this property to a non-zero value is necessary to enable self-sizing cell behavior for a table view:

class CustomCellViewController: UITableViewController {
    var pharmacies: NSArray!

    override func viewDidLoad() {
        super.viewDidLoad()

        title = "Custom Cell View"

        // Configure table view
        tableView.registerClass(PharmacyCell.self, forCellReuseIdentifier: PharmacyCell.self.description())
        tableView.estimatedRowHeight = 2

        // Load pharmacy list from JSON
        let path = NSBundle.mainBundle().pathForResource("pharmacies", ofType: "json")
        let data = NSData(contentsOfFile: path!)

        pharmacies = NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.allZeros, error: nil) as! [[String: AnyObject]]
    }

    override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return pharmacies.count
    }

    ...
}

The custom cell class itself is defined as follows:

class PharmacyCell: LMTableViewCell {
    weak var nameLabel: UILabel!
    weak var distanceLabel: UILabel!
    weak var addressLabel: UILabel!
    weak var phoneLabel: UILabel!
    weak var faxLabel: UILabel!
    weak var emailLabel: UILabel!

    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)

        LMViewBuilder.viewWithName("PharmacyCell", owner: self, root: self)
    }

    required init(coder decoder: NSCoder) {
        super.init(coder: decoder);
    }
}

The class extends LMTableViewCell, a subclass of UITableViewCell that facilitates the definition of custom cell content in markup, and declares a number of outlets for views that will be defined in the markup document. In initWithStyle:reuseIdentifier:, it loads the custom view hiearchy from the document, named PharmacyCell.xml. The initWithCoder: method, though unused, is required by Swift. No other logic is necessary.

PharmacyCell.xml is defined as follows:

<LMColumnView spacing="4" layoutMarginBottom="8">
    <LMRowView alignment="baseline" spacing="4">
        <UILabel id="nameLabel" weight="1" font="System-Bold 16"/>
        <UILabel id="distanceLabel" font="System 14" textColor="#808080"/>
    </LMRowView>

    <UILabel id="addressLabel" numberOfLines="0" font="System 14"/>

    <LMColumnView spacing="4">
        <LMRowView>
            <UIImageView image="IMG_Icon_Pharmacy_Phone"/>
            <UILabel id="phoneLabel" weight="1" font="System 12"/>
        </LMRowView>
        <LMRowView>
            <UIImageView image="IMG_Icon_Pharmacy_Fax"/>
            <UILabel id="faxLabel" weight="1" font="System 12"/>
        </LMRowView>
        <LMRowView>
            <UIImageView image="IMG_Icon_Pharmacy_Email"/>
            <UILabel id="emailLabel" weight="1" font="System 12"/>
        </LMRowView>
    </LMColumnView>
</LMColumnView>

The root element is an instance of LMColumnView, a layout view that automatically arranges its subviews in a vertical line. The “spacing” attribute specifies that the column view should leave a 4-pixel gap between subviews, and the “layoutMarginBottom” attribute specifies that there should be an 8-pixel gap between the last subview and the bottom of the cell.

The column's first subview is an instance of LMRowView, a layout view that arranges its subviews in a horizontal line. The row's subviews will be aligned to baseline and will have a 4-pixel gap between them. It contains two UILabel instances, one for displaying the name of the pharmacy and another that displays the distance to the pharmacy from the user's current location. Both labels are assigned ID values, which map their associated view instances to the similarly-named outlets declared by the document's owner (in this case, the custom cell class). The labels are also styled to appear in 16-point bold and 14-point normal text, respectively, using the current system font.

Another label is created for the pharmacy's mailing address, and another column view containing icons and labels for the pharmacy's phone number, fax number, and email address. These labels are also assigned IDs that associate them with the outlets defined by the cell class. The labels for the phone, fax, and email rows are assigned a “weight” value of 1, which tells the row view to allocate 100% of its unallocated space to the label; this ensures that the icon will appear on the left and the label will fill the remaining space in the row.

The table view controller overrides tableView:cellForRowAtIndexPath: to produce instances of PharmacyCell for each row in the search results. It retrieves the dictionary instance representing the row from the pharmacies array and populates the cell using the cell's outlets. It performs some formatting on the raw data retrieved from the JSON document to make the cell's contents more readable:

class CustomCellViewController: UITableViewController {
    ...

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        // Get pharmacy data
        var index = indexPath.row
        var pharmacy = pharmacies.objectAtIndex(index) as! [String: AnyObject]

        // Configure cell with pharmacy data
        let cell = tableView.dequeueReusableCellWithIdentifier(PharmacyCell.self.description()) as! PharmacyCell

        cell.nameLabel.text = String(format: "%d. %@", index + 1, pharmacy["name"] as! String)
        cell.distanceLabel.text = String(format: "%.2f miles", pharmacy["distance"] as! Double)

        cell.addressLabel.text = String(format: "%@\n%@ %@ %@",
            pharmacy["address1"] as! String,
            pharmacy["city"] as! String, pharmacy["state"] as! String,
            pharmacy["zipCode"] as! String)

        let phoneNumberFormatter = PhoneNumberFormatter()

        let phone = pharmacy["phone"] as? NSString
        cell.phoneLabel.text = (phone == nil) ? nil : phoneNumberFormatter.stringForObjectValue(phone!)

        let fax = pharmacy["fax"] as? NSString
        cell.faxLabel.text = (fax == nil) ? nil : phoneNumberFormatter.stringForObjectValue(fax!)

        cell.emailLabel.text = pharmacy["email"] as? String

        return cell
    }
}

The PhoneNumberFormatter class is defined as follows:

class PhoneNumberFormatter: NSFormatter {
    override func stringForObjectValue(obj: AnyObject) -> String? {
        var val = obj as! NSString

        return String(format:"(%@) %@-%@",
            val.substringWithRange(NSMakeRange(0, 3)),
            val.substringWithRange(NSMakeRange(3, 3)),
            val.substringWithRange(NSMakeRange(6, 4))
        )
    }
}

So, using markup to lay out a cell's contents can significantly simplify the process of creating custom table view cells. It also makes it easy to modify the cell's layout as the needs of the application evolve.

Topics:
markupkit ,ios

Published at DZone with permission of Greg Brown, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}