How to Keep Clean Style Code in Swift

clean-style-code

Let’s talk about how to keep a clean style code in Swift. You’ve just finished your new app, and you go to show it off to a friend, and they say “Those red buttons look kind of harsh. What if you changed them all to be blue instead?” Your heart sinks, especially if you’re using xibs or storyboards. It’s going to take a while to go through them all and update the colors.

It’s even harder if you’re working with a designer since they have a real stake in wanting the app to look perfect. They will want to tweak the colors, fonts and overall look of the app until it looks just how they envisioned it. Neither of you can afford to have it take 30 minutes of coding between every small tweak that needs to be made. You need a way to update the style of the app quickly, easily and completely. You don’t want to have one accidentally red button in an app full of blue ones. Like the rest of your code, you want the look and feel of the app to be something that can be controlled all from one place, where you can update it once and have it work everywhere.

This is something that comes up in almost every project I’ve worked on. As a project grows bigger, it becomes more and more necessary, but the styling code can also grow bigger until the whole thing is a nasty mess. So what can we do?

Extensions

Adding extensions to UIColor and UIFont are a good way to get named app specific fonts and colors into your app.

extension UIColor {
    static var myAppRed: UIColor {
        return UIColor(red: 1, green: 0.1, blue: 0.1, alpha: 1)
    }
    static var myAppGreen: UIColor {
        return UIColor(red: 0, green: 1, blue: 0, alpha: 1)
    }
    static var myAppBlue: UIColor {
        return UIColor(red: 0, green: 0.2, blue: 0.9, alpha: 1)
    }
}

extension UIFont {
    static var myAppTitle: UIFont {
        return UIFont.systemFont(ofSize: 17)
    }
    static var myAppBody: UIFont {
        return UIFont.systemFont(ofSize: 14)
    }
}

This is a useful start but falls far short of what I’ve found that I typically need. If the designer decides they don’t want to use blue anymore but purple, you can’t just change myAppBlue to be a purple color, as this no longer makes sense. You end up having to make a new color myAppPurple, and replacing all of the myAppBlues with myAppPurples, which doesn’t meet the goals of having your code only needing to be changed in one place, and ensuring that its changed everywhere when it is. An even bigger problem occurs if the designer decides that the titles in the app should now all be black instead of green, but everything else that was green should stay the same color. Now not only do you need to change all of your files, you have to go back through and reevaluate which color is the right color to use in each place that used to be green. This isn’t just a huge pain, it’s also very error prone.

Style Objects

An option that helps to solve this problem is to make a separate Style or Theme type object that holds different fonts and colors, organized by their function, and in your view code you reference the different colors and fonts on your Style class instead of directly by name.

class MyAppStyle {
    static let titleColor = UIColor.myAppGreen
    static let subtitleColor = UIColor.myAppBlue
    static let bodyColor = UIColor.black
    static let backgroundColor = UIColor.white
    static let actionColor = UIColor.myAppRed
    static let accentColor = UIColor.myAppGreen

    static let headerFont = UIFont.myAppHeader
    static let bodyFont = UIFont.myAppBody
}

Now if you want to change the color of every title in your app, it’s as easy as just changing the titleColor. This ensures that the color is correctly updated everywhere in the app, and even if you are adding a new color you’re still just changing it in that one place.

This approach normally starts off great but has a few drawbacks. As an app grows, you are more likely to run into views that break the existing rules that you’ve set up, so you’re forced to add and name new colors. Maybe normally you want to have a dark background with light text, but in text-heavy areas, you want to fall back onto a more standard white background with black text. Maybe you want a different background color for table views. There almost always seem to be exceptions that pop up as a project grows. You want to try to keep the color names generic so that they are more reusable, but this is not always possible. Fairly often I find that by the end of a project, a Style object looks less like the code above and more like this:

class MyAppStyle {
    static let titleColor = UIColor.myAppGreen
    static let lightTitleColor = UIColor.lightGray
    static let darkTitleColor = UIColor.darkGray
    static let subtitleColor = UIColor.myAppBlue

    static let bodyColor = UIColor.black
    static let lightBodyColor = UIColor.white
    static let textFieldTextColor = UIColor.darkGray
    static let textFieldBackgroundColor = UIColor.lightGray

    static let backgroundColor = UIColor.white
    static let secondaryBackgroundColor = UIColor.myAppBlue
    static let bodyBackgroundColor = UIColor.white
    static let tableviewBackgroundColor = UIColor.lightGray
    static let tableviewCellBackgroundColor = UIColor.myAppGreen
    static let tableviewCellActionColor = UIColor.myAppRed

    static let actionColor = UIColor.myAppRed
    static let secondaryActionColor = UIColor.orange
    static let accentColor = UIColor.myAppGreen
    static let disabledColor = UIColor.gray

    ... // And on and on and on
}

There can end up being so many different colors and fonts that the names are no longer meaningful. When you add a new view, you have to check on your style object, hope that there is a color that already matches what you need and that the name makes sense for what you are doing. If it doesn’t you’re forced to add yet another color and come up with another name for it. It’s also confusing to know which colors go with which other colors, does the lightBodyColor go on the regular bodyBackgroundColor or the secondaryBackgroundColor? To answer you typically have to go back and check your Style object and see which color closer matches what it is you need. There are also problems with code reusability, as all of your view code is tied to a project-specific singleton and can’t just be dropped in and used in a new project. Dropping the Style in doesn’t help much since by this point the Style object has grown around the previous project’s requirements and most of the different color names don’t even make sense for the new project.

A More Generic Solution

In trying to work around these problems I wanted to come up with a more generic solution that could be used across projects and make view code more reusable. I wanted to have different TextStyles within a general Style, for example title or body, so we can make this a separate enum. And for every TextStyle, we can have a different set of attributes, using a TextAttributes struct to hold these.

Related: Creating a Custom UICollectionViewLayout in Swift

The Style object has a few general properties and then a closure which takes a TextStyle and returns TextAttributes for that style. This is the meat and potatoes of the Style object, most of the rest of the class uses this closure to get the correct attributes, including some convenience getters to make them easier to access from the outside. While you will probably want many more TextStyles and TextAttributes properties for your Style object, here is a simplified version of it:

class Style {
    enum TextStyle {
        case navigationBar
        case title
        case subtitle
        case body
        case button
    }

    struct TextAttributes {
        let font: UIFont
        let color: UIColor
        let backgroundColor: UIColor?

        init(font: UIFont, color: UIColor, backgroundColor: UIColor? = nil) {
            self.font = font
            self.color = color
            self.backgroundColor = backgroundColor
        }
    }

    // MARK: - General Properties
    let backgroundColor: UIColor
    let preferredStatusBarStyle: UIStatusBarStyle

    let attributesForStyle: (_ style: TextStyle) -> TextAttributes

    init(backgroundColor: UIColor,
         preferredStatusBarStyle: UIStatusBarStyle = .default,
         attributesForStyle: @escaping (_ style: TextStyle) -> TextAttributes)
    {
        self.backgroundColor = backgroundColor
        self.preferredStatusBarStyle = preferredStatusBarStyle
        self.attributesForStyle = attributesForStyle
    }

    // MARK: - Convenience Getters
    func font(for style: TextStyle) -> UIFont {
        return attributesForStyle(style).font
    }

    func color(for style: TextStyle) -> UIColor {
        return attributesForStyle(style).color
    }

    func backgroundColor(for style: TextStyle) -> UIColor? {
        return attributesForStyle(style).backgroundColor
    }
}

For the style specific to your app, make a separate file Style+MyApp.swift where you declare a static Style object for the project as well as the app color and font extensions:

extension Style {
    static var myApp: Style {
        return Style(
            backgroundColor: .black,
            preferredStatusBarStyle: .lightContent,
            attributesForStyle: { $0.myAppAttributes }
        )
    }
}

private extension Style.TextStyle {
    var myAppAttributes: Style.TextAttributes {
        switch self {
        case .navigationBar:
            return Style.TextAttributes(font: .myAppTitle, color: .myAppGreen, backgroundColor: .black)
        case .title:
            return Style.TextAttributes(font: .myAppTitle, color: .myAppGreen)
        case .subtitle:
            return Style.TextAttributes(font: .myAppSubtitle, color: .myAppBlue)
        case .body:
            return Style.TextAttributes(font: .myAppBody, color: .black, backgroundColor: .white)
        case .button:
            return Style.TextAttributes(font: .myAppSubtitle, color: .white, backgroundColor: .myAppRed)
        }
    }
}

extension UIColor {
    static var myAppRed: UIColor {
        return UIColor(red: 1, green: 0.1, blue: 0.1, alpha: 1)
    }
    static var myAppGreen: UIColor {
        return UIColor(red: 0, green: 1, blue: 0, alpha: 1)
    }
    static var myAppBlue: UIColor {
        return UIColor(red: 0, green: 0.2, blue: 0.9, alpha: 1)
    }
}

extension UIFont {
    static var myAppTitle: UIFont {
        return UIFont.systemFont(ofSize: 17)
    }
    static var myAppSubtitle: UIFont {
        return UIFont.systemFont(ofSize: 15)
    }
    static var myAppBody: UIFont {
        return UIFont.systemFont(ofSize: 14)
    }
}

This keeps all of the project specific styling code in the same file and grouped together by the different TextStyles. It is painless to update across the entire app and easy to read. There is also nothing stopping you from having multiple Styles in your app if you need them.

The static var on Style makes it simple to get your style:

let style = Style.myApp

and getting attributes from a style and text style looks like this:

let titleColor = Style.myApp.color(for: .title)
let bodyFont = Style.myApp.font(for: .body)

Applying Style to UI Elements

We want to make it easier to apply a style to different UI elements, instead of needing to look up all of the attributes individually and adding them to every object. So inside the Style object add some functions to do this:

func apply(textStyle: TextStyle, to label: UILabel) {
    let attributes = attributesForStyle(textStyle)
    label.font = attributes.font
    label.textColor = attributes.color
    label.backgroundColor = attributes.backgroundColor
}

func apply(textStyle: TextStyle = .button, to button: UIButton) {
    let attributes = attributesForStyle(textStyle)
    button.setTitleColor(attributes.color, for: .normal)
    button.titleLabel?.font = attributes.font
    button.backgroundColor = attributes.backgroundColor
}

func apply(textStyle: TextStyle = .navigationBar, to navigationBar: UINavigationBar) {
    let attributes = attributesForStyle(textStyle)
    navigationBar.titleTextAttributes = [
        NSFontAttributeName: attributes.font,
        NSForegroundColorAttributeName: attributes.color
    ]

    if let color = attributes.backgroundColor {
        navigationBar.barTintColor = color
    }

    navigationBar.tintColor = attributes.color
    navigationBar.barStyle = preferredStatusBarStyle == .default ? .default : .black
}

Since buttons and navigation bars already have corresponding TextStyles, we can give them default values to make styling them even easier, but not restrict them to those styles.

View Controllers

Having my view code tied to a project-specific style singleton makes it not as reusable, so when making a view controller you can give it a Style property that can be set so that different projects can set this to different styles. Even better, the view controllers are immediately reusable in other projects.

Related: Avoiding Complex View Controllers

In practice, a view controller using Style looks something like this:

class StylishViewController: UIViewController {
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var subtitleLabel: UILabel!
    @IBOutlet weak var bodyLabel: UILabel!
    @IBOutlet weak var actionButton: UIButton!

    let style: Style

    init(style: Style) {
        self.style = style
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override var preferredStatusBarStyle: UIStatusBarStyle {
        return style.preferredStatusBarStyle
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        applyStyle()
    }

    func applyStyle() {
        view.backgroundColor = style.backgroundColor

        style.apply(textStyle: .title, to: titleLabel)
        style.apply(textStyle: .subtitle, to: subtitleLabel)
        style.apply(textStyle: .body, to: bodyLabel)
        style.apply(to: actionButton)

        if let navBar = navigationController?.navigationBar {
            style.apply(to: navBar)
        }
    }
}

Note: I’m using a xib, your code would look slightly different if doing it all in code or using storyboards.

The styling code is short, easy to read and easily reusable with different styles!

I like to break the styling code out into its own applyStyle function, so that I can use accessible fonts and update the fonts and styles when the accessibility font sizes change.

In Conclusion

Clean style code can save you a lot of time. The generic Style class is reusable and makes your view code reusable. It is concise and easy to understand at the call site, as well as in the app-specific Style extension. The app-specific Style can also be tweaked easily without needing to change code anywhere else in your app, and makes questions like “What if we made the titles bold and one point bigger?” or “Can we change all of the buttons to blue?” much less terrifying.

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *