Atelier Clockwork

Doing It All in UIAppearance

Exploring the Line Between Possible and a Good Idea

In putting together code examples for my talk on making working with UITableView swift-ier, I’m attempting to remove all of the styling code from the example view controllers, and that led to a lot of testing out just how much I can set up in UIAppearance proxies without breaking everything.

private extension AppStyle {

    func setupAppearance() {
        setupNavBarAppearance()
        setupTableViewCellAppearance()
        setupTableAppearance()
    }

    func setupNavBarAppearance() {

        let navBarAppearance = UINavigationBar.appearanceWhenContainedInInstancesOfClasses([NavigationController.self])

        navBarAppearance.translucent = false
        navBarAppearance.shadowImage = UIImage.solidImage(color: colors.tintColor)

        let barImage = UIImage.solidImage(color: colors.navigationBackgroundColor)
        navBarAppearance.setBackgroundImage(barImage, forBarMetrics: .Default)
        let textStyle: [String:AnyObject] = [
            NSForegroundColorAttributeName: colors.navbarTextColor
        ]
        navBarAppearance.titleTextAttributes = textStyle
    }

    func setupTableViewCellAppearance() {
        let cellAppearance = UITableViewCell.appearanceWhenContainedInInstancesOfClasses([NavigationController.self])
        cellAppearance.backgroundColor = colors.backgroundColor
        let selectedView =  UIView()
        selectedView.backgroundColor = colors.selectedBackgroundColor
        cellAppearance.selectedBackgroundView = selectedView
        cellAppearance.forceInsets(UIEdgeInsets())

        let nestedClasses: [AnyObject.Type] = [UITableViewCell.self, NavigationController.self]
        let cellLabelAppearance = UILabel.appearanceWhenContainedInInstancesOfClasses(nestedClasses)
        cellLabelAppearance.forceFont(UIFont.preferredFontForTextStyle(UIFontTextStyleBody))
        cellLabelAppearance.highlightedTextColor = colors.selectedTextColor
        cellLabelAppearance.forceTextColor(colors.textColor)
    }

    func setupTableAppearance() {
        let tableViewAppearance = UITableView.appearanceWhenContainedInInstancesOfClasses([NavigationController.self])
        tableViewAppearance.backgroundColor = colors.backgroundColor
        tableViewAppearance.tableFooterView = UIView()
        tableViewAppearance.separatorColor = colors.textColor
        tableViewAppearance.forceInsets(UIEdgeInsets())
    }
}

//MARK: - Functions to force configuration of properties not exposed to UIApperarance by default

private extension UILabel {
    @objc func forceTextColor(color: UIColor) {
        textColor = color
    }

    @objc func forceFont(font: UIFont) {
        self.font = font
    }
}

private extension UITableView {
    @objc func forceInsets(edgeInsets: UIEdgeInsets) {
        separatorInset = edgeInsets
        layoutMargins = edgeInsets
    }
}

private extension UITableViewCell {
    @objc func forceInsets(edgeInsets: UIEdgeInsets) {
        separatorInset = edgeInsets
        layoutMargins = edgeInsets
    }
}

This is by no means a complete style for my application, but it’s getting styled text into the table view cells, custom cell selection highlights, making the table cell insets extend to full bleed, and a few other things.

The first thing to notice is that pretty much everything is only styled if it’s wrapped in a NavigationController which is a custom UINavigationController subclass. That’s because UIAppearance can bleed over into some but not all elements of certain system view controllers like the contacts picker and email composer.

After that, notice all of the private extensions, those are an end-run around properties that aren’t exposed to UIAppearance. Those are a big part of why I’m being very careful with just where I set my proxies. So far I’ve run into issues when forcing text colors on labels too broadly doing strange things to the system navigation bar animations, and I’m sure I’ll run into more interesting edge cases along the way. If I can make this work for the entire table demo project, I’m hoping to pull the pattern into actual production projects to reduce certain bits of repetitive and mistake prone code, particularly things like setting the cell insets for table views.