ViewCode with UIKit

Raphael Martin - Feb 14 - - Dev Community

Introduction

ViewCode is a UI building strategy commonly used in the Apple ecosystem development (iOS, tvOS, macOS and more) that consists in creating the UI programmatically. Xcode offers you the possibility of creating user interfaces with a tool called Interface Builder, but often developers choose to not use it and do everything with ViewCode. Today we're going to explore this concept, reasons for using ViewCode, and how to implement it.


Why choosing ViewCode over Interface Builder

In Xcode, you have the possibility of creating UI using Interface Builder. It is a tool that handles storyboards and .xib files, with drag & drop features, in an attempt of making UI building an easy and intuitive task, especially for beginners.

Dragging and dropping a label with Interface Builder

Xcode creates self-managed XML files with the screen content you created. As you can imagine, the content of these files aren't much human-readable (since Apple doesn't expect you to directly edit this files). Also, these files bring some metadata, such as version of the Interface Builder, version of Xcode and plugins used.

All these characteristics can lead to problems:

1. Version Control Management

Working in teams on projects that uses storyboards and/or *.xib*s can be very painful. That's because merging these files in a VCS is generally hard. If you and a colleague are editing the same file, when pushing to a git repository, for example, it's difficult to solve conflicts in the changed files, since, as I mentioned previously, these files are not meant to be read by the engineers.

Changes in storyboard file made by Xcode

2. Modularity and Reusability

With ViewCode it's pretty simple to create reusable components, since you can create a custom class for a specific component, and then use it in several places. Also, changing the original class automatically impacts all the places that use it. This task is not possible with Interface Builder

3. Navigation

When using storyboards, you can use segues to create the navigation logic of your app. As your project grows, navigation logic complexity increases exponentially.

Creating navigation with Interface Builder

4. Multiplatform support

When working in a project that supports multiple platforms, for example, iOS and macOS, using storyboards can be a very difficult task. That's because the structure of the files is different for each platform, and the compiler fails to build a project for a specific platform if it contains storyboards for other platforms. To make that work, you'll need to increase build logic complexity by adding conditions to determine whether include a file in the app bundle.

Xcode build error due to macOS storyboard in iOS app


How to add UI programmatically

In UIKit, you can add UI components to your views by instantiating view objects and adding them through the UIView.addSubview() function.

class ViewController: UIViewController {
    override func viewDidAppear(animated: Bool) {
        let label = UILabel()
        label.text = "Hello World!"

        self.view.addSubview(label)
    }
}
Enter fullscreen mode Exit fullscreen mode

You can also create a hierarchy by nesting views:

let containerView = UIView()
containerView.backgroundColor = .red

let label = UILabel()
label.text = "Hello World!"

containerView.addSubview(label)

self.view.addSubview(container)
Enter fullscreen mode Exit fullscreen mode

But be aware: for some specific types of views, you may need to use a different function: addArrangedSubview.
In UIStackView objects, using this function is important since it doesn't just add your view as a child of the StackView, but it also arranges it. And by arranging, I mean positioning and sizing the child view based on the StackView properties, such as axis, alignment, distribution and spacing. Let's analyze a piece of code:

let stackView = UIStackView()
stackView.axis = .vertical
stackView.spacing = 10
stackView.alignment = .center
stackView.distribution = .equalSpacing  
stackView.backgroundColor = .yellow

for _ in 0..<4 {
    let label = UILabel()
    label.text = "Hello, World!"
    label.backgroundColor = .lightGray
    stackView.addArrangedSubview(label)
}

view.addSubview(stackView)
Enter fullscreen mode Exit fullscreen mode

Four labels correctly placed inside a vertical stack view

In the image above you can see that the labels are correctly vertically stacked, with a distance of 10p from each other, taking the same space. But if we change the addArrangedSubview function to addSubview, no label becomes visible inside the stack view:

Labels not appearing due to incorrect function

You can also add Constraints to your layout by using the NSLayoutConstraint api. Let's add some constraints to a label, making it centered in a view:

let label = UILabel()
label.text = "Hello, World!"
// This line is important
label.translatesAutoresizingMaskIntoConstraints = false

// First add the view to the hierarchy
view.addSubview(label)

// Then add the constraints
label.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
label.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
Enter fullscreen mode Exit fullscreen mode

To align a UILabel programmatically, you can use properties from UIView, such as centerXAnchor and centerYAnchor. They are of type NSLayoutAnchor, a type that has several constraint functions, allowing you anchoring your views where it's needed.

The constraint(equalTo:) function returns an object of type NSLayoutConstraint. We can hold this value in a variable, and conditionally enable/disable the constraint:

let horizontalLabelConstraint = label.centerXAnchor.constraint(equalTo: view.centerXAnchor)

if shouldAlignLabelOnCenter {
    horizontalLabelConstraint.isActive = true
}
Enter fullscreen mode Exit fullscreen mode

Notice also that, on our first example with constraints, we are adding them after adding the label as a subview of view. That's because UIKit requires the component to be in the view hierarchy before applying constraints. Otherwise, you'll receive a runtime error:

Runtime error due to adding constraints to view that is not in the hierarchy

If activating multiple constraints at same time, you can use the NSLayoutConstraint.activate() function, that receives an array of NSLayoutConstraints as argument, and set the isActive property to true in all of them:

NSLayoutConstraint.activate([
    label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
    label.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
Enter fullscreen mode Exit fullscreen mode

Autoresizing Mask

We disable the translatesAutoresizingMaskIntoConstraints property (also known as tamic, a shorthand Xcode autocompletes when typing label.tamic) to ensure our explicit constraints work properly. Even though all developers use it, many of them don't know exactly why it's needed:

  1. Before Auto Layout: In the early days of iOS development, screen sizes were fixed, making it feasible to position views using absolute X/Y coordinates. To provide some flexibility, UIKit introduced the Autoresizing Mask, a system that allowed child views to resize or reposition dynamically when their parent view changed size.

  2. The Introduction of Auto Layout: With the launch of the iPhone 5, Apple introduced Auto Layout to support multiple screen sizes and aspect ratios. Instead of relying on autoresizing behavior, developers could now define explicit constraints to determine how views should be sized and positioned. To bridge the gap between the old and new systems, Apple introduced translatesAutoresizingMaskIntoConstraints, which automatically converts the Autoresizing Mask into Auto Layout constraints.

  3. Why You Rarely Notice This in Interface Builder: When using Interface Builder, Xcode automatically sets translatesAutoresizingMaskIntoConstraints = false because it assumes you’ll define constraints manually. However, when creating views programmatically, this property is true by default to maintain compatibility with older UIKit behavior.

Summary:

The translatesAutoresizingMaskIntoConstraints property exists to allow the older Autoresizing Mask system to work with Auto Layout. However, when defining constraints programmatically, this conversion creates constraints that conflict with our custom constraints, leading to unintended behavior.


Best practices

Removing "completely" the dependency on Storyboards

The first thing you should do when starting a new ViewCode app is remove any storyboard usage from the default Xcode project. I mean, any possible usage. That's because for a very specific use case you may need to keep a storyboard: Launch Screen. It's recommendable to keep the LaunchScreen.storyboard for configuring a responsive launch screen to your app. In past you could use an image as launch screen, by adding it to your app assets catalog (.xcassets), but this option is deprecated in recent versions of Xcode.

Deprecated Launch Image assets option

Let take a brand new iOS app as an example. When creating one, select Storyboard as Interface (that will make our app use UIKit and not SwiftUI).

Creating new Storyboard project

Now, you can delete the Main.storyboard file

Main.storyboard file location in a new project

On your Info.plist file, under Application Scene Manifest > Scene Configuration > Window Application Session Role > Item 0, delete the Storyboard Name entry.

Main.storyboard Info.plist entry

In your target settings, go to Build Settings > Info.plist Values and remove the UIKit Main Storyboard File Base Name entry value (just select it and press Delete).

Main.storyboard Build Settings entry

Finally, you need to programmatically set the first UIViewController of your app. For that, you should go to the SceneDelegate class, and in the scene(willConnectTo:) function, replace its content with the following piece of code:

guard let windowScene = (scene as? UIWindowScene) else { return }
self.window = UIWindow(windowScene: windowScene)
self.window?.rootViewController = ViewController()
self.window?.makeKeyAndVisible()
Enter fullscreen mode Exit fullscreen mode

This creates a UIWindow object with the given scene, sets a root view controller, and makes it visible.

Before building and running your app, add some styling to the initial ViewController class to you recognize it and know that everything is working:

override func viewDidLoad() {
    super.viewDidLoad()

    view.backgroundColor = .white

    let label = UILabel()
    label.text = "Hello, World!"

    view.addSubview(label)

    NSLayoutConstraint.activate([
        label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
        label.centerYAnchor.constraint(equalTo: view.centerYAnchor),
    ])
}
Enter fullscreen mode Exit fullscreen mode

You should see a screen like that:

Initial page correctly appears

Lazy vars for UI Components

It's a good and common practice to use lazy vars initialized with an immediately invoked closure for your UI components when working with ViewCode. That's because with lazy vars, you consume the computing resources of initializing the view objects only when it's called, and with the closure you can perform some setup in the component when initialing it:

class ViewController: UIViewController {
    private lazy var label: UILabel = {
        let label = UILabel()
        label.text = "Hello, World!"
        label.textColor = .black
        label.translatesAutoresizingMaskIntoConstraints = false

        return label
    }()

    // ..
}
Enter fullscreen mode Exit fullscreen mode

Note that I set several attributes to the label property when initializing, centralizing this setup logic in the object creation.

Work with protocols

You can also create a protocol to keep your UI code organized and concise. As we previously covered, constraints need to be set after adding the views to the hierarchy. A protocol can help you, for example, assure that you are doing these things in the correct order.

protocol ViewCode {
    func addViewHierarchy()
    func setupConstraints()

    func buildView()
}

extension ViewCode {
    func buildView() {
        addViewHierarchy()
        setupConstraints()
    }
}
Enter fullscreen mode Exit fullscreen mode

Then you can adjust your ViewController class to make it conform to ViewCode, calling the buildView() function only, being sure that the views are being added first, and then the constraints:

class ViewController: UIViewController {
    private lazy var label: UILabel = {
        //..
    }()

    private lazy var button: UIButton = {
       //..
    }()

    private lazy var stackView: UIStackView = {
        //..
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        buildView()
    }
}

// MARK: ViewCode conformance
extension ViewController: ViewCode {
    func addViewHierarchy() {
        stackView.addArrangedSubview(label)
        stackView.addArrangedSubview(button)

        view.addSubview(stackView)
    }

    func setupConstraints() {
        NSLayoutConstraint.activate([
            stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            stackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
        ])
    }
}
Enter fullscreen mode Exit fullscreen mode

In the example above, the protocol helps us to follow some of the best practices: assuring the correct order of adding views and adding constraints; separating responsibilities; improving readability.

You can also adapt your protocol to your project reality, as adding a function to styling, for example. This is just an initial idea.


Conclusion

In this article, we explored ViewCode, a UI-building approach that relies on writing layout code instead of using Interface Builder. We discussed why many developers prefer ViewCode over storyboards, highlighting its advantages in version control management, modularity, navigation, and multiplatform support.

We also covered the basics of creating UI programmatically in UIKit, including:

  • Adding views to the hierarchy using addSubview() and addArrangedSubview().
  • Applying constraints with Auto Layout and NSLayoutConstraint.
  • Understanding the role of translatesAutoresizingMaskIntoConstraints.

Additionally, we reviewed best practices for fully removing storyboards from an app, ensuring a clean ViewCode setup from project creation.

While ViewCode requires more manual effort than Interface Builder, it provides better scalability, maintainability, and flexibility for complex projects. By mastering these techniques, you can gain greater control over your UI and improve your development workflow.

. . . . . . . .