How KVC Converts Strings to Numeric Types

Thanks to this discussion on Stack Overflow, I now understand how KVC is able to automatically convert string values to numeric types. As documented by Apple:

…setters like setValue:forKey: determine the data type required by a property’s accessor or instance variable, given a particular key. If the data type is not an object, the setter first sends an appropriate <type>Value message to the incoming value object to extract the underlying data, and stores that instead.

For example, if the type of the property being set is int:

@property int index;

KVC will attempt to invoke the intValue method on the provided value to convert it to the appropriate type before setting the property value:

[self setValue:@"10" forKey:@"index"]; // calls intValue on "10"

Thanks very much to Stack Overflow user CRD for providing the answer!

Creating Custom Table View Cells in Markup

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 in iOS 8, 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:

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": "CVS",
      "address1": "263 Washington Street",
      "city": "Boston",
      "state": "MA",
      "zipCode": "02108",
      "phone": "6177427035",
      "email": "store04@cvs.com",
      "fax": "6177420001",
      "distance": 0.42
    },
    {
      "name": "Walgreens",
      "address1": "70 Summer Street",
      "city": "Boston",
      "state": "MA",
      "zipCode": "02108",
      "phone": "6172657488",
      "email": "store20@walgreens.com",
      "fax": "6177420001",
      "distance": 0.64
    },

    ...
]

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: [[String: AnyObject]]!

    override func viewDidLoad() {
        super.viewDidLoad()

        title = "Custom Cell View"

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

        // Load pharmacy list from JSON
        let pharmacyListURL = Bundle.main.url(forResource: "pharmacies", withExtension: "json")

        pharmacies = try! JSONSerialization.jsonObject(with: try! Data(contentsOf: pharmacyListURL!)) as! [[String: AnyObject]]
    }

    override func numberOfSections(in 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 {
    @IBOutlet var nameLabel: UILabel!
    @IBOutlet var distanceLabel: UILabel!
    @IBOutlet var addressLabel: UILabel!
    @IBOutlet var phoneLabel: UILabel!
    @IBOutlet var faxLabel: UILabel!
    @IBOutlet var emailLabel: UILabel!

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

        LMViewBuilder.view(withName: "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 init(style:reuseIdentifier:), it loads the custom view hiearchy from the document, named PharmacyCell.xml. The init(coder:) method, though unused, is required by Swift. No other logic is necessary.

PharmacyCell.xml is defined as follows:

<LMColumnView spacing="4" layoutMarginBottom="8">
    <LMRowView alignToBaseline="true" 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="phone_icon"/>
            <UILabel id="phoneLabel" weight="1" font="System 12"/>
        </LMRowView>
        <LMRowView>
            <UIImageView image="fax_icon"/>
            <UILabel id="faxLabel" weight="1" font="System 12"/>
        </LMRowView>
        <LMRowView>
            <UIImageView image="email_icon"/>
            <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. 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. The labels will be aligned to baseline and will have a 4-pixel gap between them.

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 also 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 view controller overrides tableView(_:cellForRowAt:) 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 then 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, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        // Get pharmacy data
        let index = (indexPath as NSIndexPath).row

        let pharmacy = pharmacies[index]

        // Configure cell with pharmacy data
        let cell = tableView.dequeueReusableCell(withIdentifier: 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? String
        cell.phoneLabel.text = (phone == nil) ? nil : phoneNumberFormatter.string(for: phone!)

        let fax = pharmacy["fax"] as? String
        cell.faxLabel.text = (fax == nil) ? nil : phoneNumberFormatter.string(for: fax!)

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

        return cell
    }
}

The PhoneNumberFormatter class is defined as follows:

class PhoneNumberFormatter: Formatter {
    override func string(for obj: Any?) -> String? {
        let val = obj as! NSString

        return String(format: "(%@) %@-%@",
            val.substring(with: NSMakeRange(0, 3)),
            val.substring(with: NSMakeRange(3, 3)),
            val.substring(with: 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.

The complete source code for this example can be found here:

Implementing Radio Button-Like Behavior in iOS Applications

One of the things I have always found a bit frustrating about iOS development is the lack of selection controls like radio buttons and drop-down lists. These controls are ubiquitous on other platforms, and most users are comfortable with their use and behavior.

Fortunately, it is possible to approximate the behavior of these controls using UITableView. Apple recommends using checkmarks to represent selection state; however, this is not something you get for free – it must be implemented by the application for each table view or table view section that requires it.

Because it is such a common metaphor, MarkupKit's LMTableView class provides support for implementing radio button-like behavior automatically. Setting the selection mode of any section in an LMTableView to "singleCheckmark" ensures that only a single row will be checked at any given time.

MarkupKit adds a value property to the UITableViewCell class to allow an application to associate an optional value with a cell, similar to the "value" attribute of an <option> tag in an HTML <select> element. It also adds a boolean checked property to UITableViewCell so the application can easily manage the cell's selection state.

For example, the following markup creates a table view that allows a user to choose one value from a list of size options. The "Large" option is checked by default:

<LMTableView style="groupedTableView">
    <?sectionSelectionMode singleCheckmark?>

    <UITableViewCell textLabel.text="Small" value="S"/>
    <UITableViewCell textLabel.text="Medium" value="M"/>
    <UITableViewCell textLabel.text="Large" value="L" checked="true"/>
    <UITableViewCell textLabel.text="Extra-Large" value="XL"/>
</LMTableView>

This markup produces output similar to the following:

The following Swift code produces the same result, albeit a bit more verbosely:

let tableView = LMTableView.grouped()

tableView.setSelectionMode(LMTableViewSelectionMode.singleCheckmark, forSection: 0)

// Small
let smallCell = UITableViewCell()
smallCell.textLabel!.text = "Small"
smallCell.value = "S"

tableView.insert(smallCell, forRowAt: IndexPath(row: 0, section: 0))

// Medium
let mediumCell = UITableViewCell()
mediumCell.textLabel!.text = "Medium"
mediumCell.value = "M"

tableView.insert(mediumCell, forRowAt: IndexPath(row: 1, section: 0))

// Large
let largeCell = UITableViewCell()
largeCell.textLabel!.text = "Large"
largeCell.value = "L"

largeCell.checked = true

tableView.insert(largeCell, forRowAt: IndexPath(row: 2, section: 0))

// Extra-large
let extraLargeCell = UITableViewCell()
extraLargeCell.textLabel!.text = "Extra-Large"
extraLargeCell.value = "XL"

tableView.insert(extraLargeCell, forRowAt: IndexPath(row: 3, section: 0))

Setting a section's selection mode to "multipleCheckmarks" enables selection behavior similar to a collection of checkboxes on other platforms. For example, the following markup creates a list of pet choices from which the user may select zero or more values. "Cat" and "Turtle" are checked by default:

<LMTableView style="groupedTableView">
    <?sectionSelectionMode multipleCheckmarks?>

    <UITableViewCell textLabel.text="Dog"/>
    <UITableViewCell textLabel.text="Cat" checked="true"/>
    <UITableViewCell textLabel.text="Fish"/>
    <UITableViewCell textLabel.text="Rabbit"/>
    <UITableViewCell textLabel.text="Turtle" checked="true"/>
</LMTableView>

This markup produces output similar to the following:

These selection management features of LMTableView make it much easier to handle some common usage scenarios in iOS application design.

The complete source code for these examples can be found here: