Статьи

Создание пользовательских ячеек табличного представления в разметке

Реализация пользовательских ячеек табличного представления традиционно была одним из наиболее сложных аспектов разработки под iOS. В более ранних версиях ОС разработчикам приходилось рассчитывать размеры ячеек и положения подпредставлений вручную, что занимало много времени и вызывало ошибки.

Со времени введения ограничений макета и размеров ячеек ячеек процесс стал проще, но  MarkupKit  делает его еще проще, позволяя разработчикам определять структуру ячейки полностью в разметке. Представления макета, такие как  LMColumnView и,  LMRowView могут использоваться для автоматического позиционирования подпредставлений ячейки и реагирования на изменения содержимого и ориентации, в результате чего сам класс ячейки отвечает просто за обеспечение поведения ячейки.

Например, следующий снимок экрана показывает табличное представление, которое представляет список результатов поиска аптек, имитирующих аптеку:

Пример пользовательского представления ячеек

Содержимое табличного представления определяется документом JSON, содержащим результаты поиска. В примере приложения эти результаты являются статическими. В реальном приложении они, вероятно, будут динамически генерироваться каким-либо веб-сервисом:

[
    {
      "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 theestimatedRowHeight 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. TheinitWithCoder: 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.