Реализация пользовательских ячеек табличного представления традиционно была одним из наиболее сложных аспектов разработки под 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": "[email protected]",
"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": "[email protected]",
"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": "[email protected]",
"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.