Developer / Designer Workflow

(You can find the Xcode project and the Sketch file for this post here.)

There are two approaches to building user interfaces: using a visual tool or doing everything in code. Each has its own advantages and disadvantages. Let's see what they are and find a way to combine the best of both when designing and building an iOS app.

Visual Tools vs Coding

The biggest advantage of a visual tool is that if you know the tool well, building the UI is usually very fast -- you select the elements that you need from the available set, arrange them on the screen, adjust some properties, and you are done. Another advantage is that you see what the UI will look like while you are working on it, so you can iterate quickly. For iOS, Interface Builder has a large selection of available UI elements -- almost any UI element that UIKit offers has an Interface Builder element that you can drag and configure. You can express the layout via constraints visually, and IB gives you a preview of what it would look like on different screens and even warns about ambiguous or impossible layouts. Storyboards allow you to express multiple screens in one place and describe the flow in the app visually. Actions and gesture recognizers allow you to express user interactions.

If you want to build a very basic iOS app that has very few screens and uses only iOS standard visual elements, this approach works well, but if you are building an app that uses a particular visual style and has more than a few screens, this approach becomes very error prone. Any change in style (for example, changing a font) requires looking through all XIB files to update elements that may have changed and manually update them. Rearranging the app flow (for example, changing the order of some screens) also becomes very error prone -- in addition to changing the storyboard, the code for segues also needs updates, and because segues are dynamic, the compiler can do very little to ensure that the code changes match the visual changes in storyboards.

The other extreme is to do everything in code. The main advantage there is that it becomes possible to build reusable code so that changes in style or app flow can be isolated and applied consistently and with much greater help from the compiler. In addition to greater control, coding everything also results in much better understanding of the APIs and how the app really works. Unfortunately, it often comes at the price of far greater code complexity, so the app is more difficult to maintain, especially for people who are new to the code. Coding is also much slower than dragging already made UI elements and adjusting their properties in inspectors.

The Middle Ground

Ideally, we'd like to combine the simplicity and speed of the visual tool approach with the reusability of the coding approach. One way to do this is to code everything related to visual elements and turn visual elements into IBDesignables. Then we can still use Interface Builder to position and layout elements on the screen with immediate preview of how the screen would really look but without repeatedly setting the same properties to achieve the same look across the app. Another way is to do something similar with UIAppearance or third party tools like Classy to style components at runtime. The advantage of coding IBDesignables is that you see the actual design in Interface Builder and don't have to run the app. You also get much more compiler / IDE help as you write the styling code.

Designing app screens involves solving the same problem of keeping screens consistent, and the solution is very similar too. For example, Sketch has symbols -- reusable objects that you make once and then reuse everywhere. Recently, Sketch introduced library symbols that share groups of symbols across designs, which could be useful for styling more than one app in the same way. Building those symbols in code and packaging that code as a framework provides a similar library for the actual app or a group of apps.

Building Reusable Components

The most basic building blocks for app components are colors and fonts. Here is an example of how the code for common colors might look:

extension UIColor {
    static let branded = UIColor(red:0.96, green:0.65, blue:0.14, alpha:1.0)
    static let appDarkGray = UIColor(red:0.27, green:0.27, blue:0.27, alpha:1.0)
    static let appGray = UIColor(red:0.41, green:0.45, blue:0.48, alpha:1.0)
}

And a similar example for fonts:

extension UIFont {
    enum FontWeight: String {
        case heavy, medium, light
        var appFontName: String {
            switch self {
            case .heavy:
                return "Avenir-Heavy"
            case .medium:
                return "Avenir-Medium"
            case .light:
                return "Avenir-Light"
            }
        }
    }
    
    static func appFont(weight: FontWeight, size: CGFloat) -> UIFont {
        guard let font = UIFont(name: weight.appFontName, size: size) else {
            fatalError("App font not found.")
        }
        return font
    }
    
    static let header1: UIFont = appFont(weight: .heavy, size: 36)
    static let header2: UIFont = appFont(weight: .medium, size: 30)
    static let body: UIFont = appFont(weight: .light, size: 18)
}

The most common reusable components are buttons, labels, and text fields. Other custom components are often made with these building blocks. Here is an example for buttons:

class StyledButton: UIButton {
    fileprivate var style: Style {
        return .primary
    }
    
    func configureStyle() {
        style.apply(to: self)
    }
    ...
 }

fileprivate extension UIButton {
    fileprivate enum Style {
        case primary, regular
        ...        
        var buttonHeight: CGFloat {
            switch self {
            case .primary:
                return 50
            case .regular:
                return 25
            }
        }
        
        var titleFont: UIFont {
            switch self {
            case .primary:
                return .appFont(weight: .medium, size: 30)
            case .regular:
                return .appFont(weight: .medium, size: 18)
            }
        }
        
        var titleColor: UIColor {
            switch self {
            case .primary:
                return .appDarkGray
            case .regular:
                return UIColor(red:0.42, green:0.29, blue:0.06, alpha:1.0)
            }
        }
        ...        
        func apply(to button: UIButton) {
            if isRounded {
                button.contentEdgeInsets = Style.contentEdgeInsets
                button.layer.cornerRadius = Style.cornerRadius
                button.layer.borderWidth = borderWidth
                button.layer.borderColor = borderColor.cgColor
            }
            
            button.titleLabel?.font = titleFont
            button.setTitleColor(titleColor, for: .normal)
            button.backgroundColor = backgroundColor
        }
    }
}

@IBDesignable
class PrimaryButton: StyledButton {
    fileprivate override var style: Style {
        return .primary
    }
}

@IBDesignable
class RegularButton: StyledButton {
    fileprivate override var style: Style {
        return .regular
    }
}

This approach scales well for different components and styles and makes it much easier to have a consistent design throughout the app while retaining much of the convenience of laying out the UI visually. This applies equally well to Xcode project content and Sketch files, as well as keeping the design and the implementation in sync. Beyond a single app, components can be further reused in multiple apps, which is especially valuable to convey the same design for branding.

Just like Sketch recently introduced symbol libraries for symbols that are reusable across Sketch files, components can be packaged as frameworks. This works well if the code depends only on UIKit / Foundation. If there are any other dependencies, it may be more practical to share the source code.