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.
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.
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.
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.
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)
}
}
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)
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)
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:
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
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
}
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:
If activating multiple constraints at same time, you can use the NSLayoutConstraint.activate()
function, that receives an array of NSLayoutConstraint
s 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),
])
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:
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.
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.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 istrue
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.
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).
Now, you can delete the Main.storyboard
file
On your Info.plist
file, under Application Scene Manifest > Scene Configuration > Window Application Session Role > Item 0
, delete the Storyboard Name
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
).
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()
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),
])
}
You should see a screen like that:
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
}()
// ..
}
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()
}
}
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)
])
}
}
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()
andaddArrangedSubview()
. - 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.