View Code’s Handbook for iOS Developers

An in-depth guide to best practices for your projects

Pedro Alvarez
Better Programming

--

Image from https://pt.best-wallpaper.net/Nature-landscape-mountains-flowers-rainbow_wallpapers.html

Almost one hundred percent of iOS developers, except those conceived through SwiftUI API, started learning to build UI interfaces with storyboards. I still remember all the magic I experienced in 2017 when I took my first lesson about Swift by creating a simple Hello World app with a storyboard by dragging labels, editing, placing background colors, and everything.

My first experience with View Code happened when I applied to a junior position where I wanted to challenge myself to create an app to consume the Marvel API with View Code. The problem was that I didn't know the good style guides for building a new screen by code, so everything was a mess. Luckily during my career, I was able to upgrade my software engineering skills by learning everything about reusable UI components and important concepts like intrinsic content size and constraints.

This article provides you with documentation regarding the main aspects of View Code, how to build reusable and reactive interfaces, and all those important concepts I mentioned before. I hope you enjoy it!

Why View Code? Aren’t Storyboards and XIBs a Reliable Solution?

I will start answering this question with a simple and concise no. Interface builders are a very attractive framework for building interfaces. However, they are only visually simple until you solve your first legacy bug.

During my career, I sincerely struggled to find interface bugs due to a view controller being recycled through different flows. It was impossible to debug which constraint, stack view, label, and IBOutlet were causing that. You always need to ensure your outlets are referenced outside the interface builder and check where exactly the configuration is being set, in the XML or the Swift file (you know, the storyboard is an XML file describing your interface).

Another thing I need to mention regards conflicts. Every time you face a conflict with another team member in the .storyboard file, you need to interpret exactly the impact of solving that conflict.

Basically, when you are dealing with View Code, everything is simpler, and you have much more control of your flow, what causes each change of layout, each button reaction, and the navigation. Please, do not ever use UISegue for navigation. It's the worst data-passing mechanism I have ever seen in any programming language and has too much coupling.

Now that you understand why View Code is the best path for building your screens and components with SwiftUI, I shall introduce you to the best practices most companies adopt when applying that. Some of them are very known, a few ones I adopted myself during my experiences, and most people might be unaware. If you wanna learn more about it, check this article. Now that I’ve said that, let's get started:

View Code Pipeline

There are three steps to go before showing your component on the screen. You may define them in almost any order except by two of them that must be preceded by each other:

1. Build hierarchy

Establishes the hierarchy for each of your subviews by adding each subview to its parent. It's a sequence of addSubview(:) method calls, or addArrangedSubview(:) if you are dealing with a UIStackView.

2. Define constraints

Here, you should define the relationships between each view by telling which is the height and width anchors and the distances between common children.

3. View configuration

This is the step where you should define additional configurations to your views. I know that most of it may have been defined in the view's initialization block, but some data (perhaps some view model content) needs to be filled in a little later. We will talk about it later.

The important thing regarding that is: Always apply the hierarchy step before defining constraints. If you don't, Xcode will be confused about how a component could be placed next to the other if they don't even have a common parent. This makes sense since you can only compare two elements if they belong to the same context(imagine defining constraints between components on different screens).

The View Code Protocol

It's always a good practice to provide an automatic way to trigger the view code pipeline, so in all my projects, one of the first things I define is a ViewCodeProtocol in some Common folder. This protocol contains four methods, three of them corresponding to each step of the pipeline and the fourth one being implemented apart to call the three in the proper order:

Notice that the configureViews method is optional and applyViewCode triggers the whole pipeline. When a new UIView or UIViewController subclass is instantiated, the only thing you need to do is calling applyViewCode in the init method, and the UI is done.

You can define this pipeline in a superclass to be inherited by all the views and declare some view code methods in there as well:

There is only one negative point for this approach: Since a protocol or a superclass defines each method, the methods can never be declared as private. This means there is no mechanism to avoid calling the pipeline from somewhere else, like a view model or the super view. It may compromise the UI encapsulation. Although it's still a good coding practice to keep the pipeline.

Declaring UI Properties

We are about to create a new component to be saved in our design system, and we will follow step by step from declaring its subviews until filling with content. The view we are about to create is a ProfileView which should contain five elements providing data about a user: one image and four labels regarding personal information, being it respectively name, age, occupation, and gender.

This is what our component should look like:

According to our design specification, these are the requirements for our component, no matter the screen size:

  • ProfileView has 98 of height
  • imageView is vertically centered in ProfileView
  • imageView has 80 of height and width.
  • imageView has 16 of distance from leading.
  • All four labels are distant from the imageView by 48
  • All four labels are vertically aligned and have 16 of distance from trailing
  • The four labels have a distance of 8 from the top and bottom.

With all this information, we are certain that we have six UI elements in our component that shall become variables: an image view, four UILabels, and one UIStackView to hold all the labels. When dealing with multiple elements aligned across some axis, it's strongly recommended to keep them in a stack view to simplify constraints and assign a single behavior. Now that I’ve said that, let's code:

As we can see, instead of creating some IBOutlets, we dragged from some pre-configured components in a storyboard or xib. We just instantiated some UI properties and declared them as lazy. They are retrieved by an initialization block of code that creates a new view, configures its properties, and return it. This way, we only execute this initialization block when we need the UI element. I strongly recommend declaring them as lazy all the time to optimize our memory. Some of the components may be initialized very later when we are about to add it, like an error view, which we may need only if we get some failure from an API.

You might be wondering why we are setting this translatesAutoresizingMaskIntoConstraints to false. That's because you want your interface to rely on constraints to set its place in the super view instead of its own frame layout (in case you created the object with a CGRect frame). We will talk about constraints later.

Building Hierarchy

Now that we listed all of our elements in the view, it's time to define which hierarchy they will follow. By this time, we should make our View class to implement the ViewCodeProtocol and declare our pipeline.

First of all, we need to clarify which view should contain the other so as not to have mistakes in our hierarchy definition. If we set a wrong hierarchy and then set the constraints, it shall be harder to solve bugs. This is what we have:

Now let's reflect it in our code:

To simplify this step, we could always implement a method in a UIView extension that receives an array of views and adds them all at once. The same should happen for the UIStackView addArrangedSubview function.

Constraints

Now we are about to describe the relationship between each of the elements belonging to the same parent in our hierarchy. According to our specification, let's define a nested enum for constants containing all of them that we shall assign to our constraints:

Constraints in Swift consist of five important elements (there are more, but we shall describe them in another article later):

1 . First element of the relation

2. Second element of the relation

3. Anchor of the first element

4. Anchor of the second element

5. Constant

An anchor describes which dimension of the element we are talking about: is that the top, bottom, leading, trailing, left, right, height or width?

Basically, when we declare a constraint, we say the following:

The second element's [anchor] is [constant] points of distance from the first element's [anchor]. We may also define height and width anchors as a constant.

There is also the priority of the constraint in case there is some other constraint that may conflict, but it's another topic for later.

So, this is what the object of an active constraint consists of. There are a few ways to declare constraints with Swift code, and I shall cover all of them with their advantages and disadvantages:

NSLayoutConstraint activate

For me, it's the most reliable one since it's native and simpler to write. For your information, the Swift UIKit class to define a constraint is NSLayoutConstraint, and it's defined by all the properties we described above. For example, let's describe a constraint that establishes a vertical relationship between two labels:

firstLabel.bottomAnchor.constraint(equalTo: secondLabel.topAnchor, constant: 12)

It says: "The first label's bottom anchor is distant from the second label's top anchor by 12 points."

Each anchor of a view has a constraint method that takes another view's anchor as a parameter and a constant.

If we just wanna list all of those relationships, just rely on the activate static method from NSLayoutConstraint :

All of our constraints are based on the measures from the design specification (Figma, Zeplin, or whatever). This is the best native way of declaring the constraints all at once. I strongly recommend using it in most contexts, except for the one we are about to check.

NSLayoutConstraint is active?

Imagine the following scenario: There is a constraint that should be valid only if a condition is followed in a specific state of our view. Since the first way of declaring constraints takes an array of constraints, that would be very boring to add and remove our constraint each time. So, we have a mechanism to define separately if it's valid or not as if we are handling a dynamic constraint:

Third-party libraries: SnapKit

SnapKit is a library provided as a CocoaPod which gives us a much simpler way of writing constraints with fewer lines of code. Its advantage is that you can merge two constraints of a view with the same constant and same anchor in a single line, group all a view's constraints in a single closure, and write relations to the parent without knowing what it is. Here’s what that looks like:

Notice that each descendant of UIView has a snp object that manages the anchors and provides methods for writing constraints via closure. Each of the constraints, like height and width in our image view, that have the same constants can be placed in a single line by setting them at once for the make object.

Also, instead of writing the parent in our constraints, like the leading of our imageView, we say it's equal to the superview. I think it's the most important aspect of SnapKit because this abstraction of the parent allows us to create a lot of reusable code for constraining some types of views.

However, SnapKit is still a third-party library, which should always be up to date with our latest code and may have bugs and some deprecated components. Because of that, I would recommend using the native ways as much as possible.

Additional Configuration

Now that we defined our hierarchy together with constraints, there is a third step in our pipeline: configuring the UI elements. You must be thinking: "But haven't we configured the elements in their own initialization block? Why do we need to do it again?". That is quite a good question, but there is a simple answer: data is dynamic and may change during the lifecycle.

Let's say our view receives a view model as an input through the init and we need to fill our views with its properties. By the time we declared our views, we still didn't have any viewModel since it was only injected in the initialization method.

First, declare a nested view model that should fill only our component. Here’s the code:

Now, let's create the method that takes the view model parameter and fill our elements:

Notice that we are also setting some of the properties of our root view itself, as we don't have a more proper place for that.

We just forgot to create an initializer for our view, passing the view model and triggering the View Code pipeline.

Now our pipeline is complete, but there is only one thing that we forgot to mention to make our code work as expected in the design specification.

Layout Subviews

As we discussed earlier, constraints are just objects we create to describe how our components should behave. But they are only valid once our layout is validated, which occurs after creation or some layout update, or better saying, it's applied in the layoutSubviews method.

Remember, we want our image to be circular. For that, its corner radius shall be half of its height and width. But if we declare that in the initialization block, when there are no constraints and the layout is not defined, it won’t work because its height and width are zero.

Implement the layoutSubviews method that is called once the layout is already defined and see how it works:

Now we have a circular image:

If during your view's lifecycle you need some update of the layout, you should notify that a layout change is needed and call setNeedsLayout function, but if you need an immediate change of layout, call layoutIfNeeded directly. This will check if some constraint or dimension is changed and then force a "redraw." We are about to look into it deeper now.

Animating Constraints

Let's say we have a view that updates its height once we tap a button. We have a variable for its constraint declaring even the constant, which is initially 10. We want an animation to grow up to 100. You rely on the animate method from UIView for that, but you notice that changing the constraint's constant doesn't work.

That's because you didn't force a layout change when changing this parameter in the animation block. You can define your constraint change outside the animation method and then force the layout change inside the animation block. That's what will change the height. Here’s the code to do that:

Intrinsic Content Size

When dealing with constraints in view code, intrinsic content size is one of the most important aspects. Let's say we have the following view:

What we have is the following:

Notice that to present the green and yellow views following the constraints, we require 12 + 60 + 12 + 60 + 12 = 156 points. So the size of the super view is defined by its internal content, and there is no need to define a constraint for its height. We don't need a constraint for the width since the width is the width of the internal views (100) plus the spaces from the borders. This is called intrinsic content size, which is the size of a view that fits its inner content. The intrinsicContentSize is a computed property of UIView that calculates the size based on its subviews and constraints.

If you have a view that already has an intrinsic content size and declare a constraint for its size, you shall face a warning of a conflict between constraints since height is defined twice, so we have an ambiguity.

According to Apple's documentation:

"The natural size for the receiving view, considering only properties of the view itself."

If you have a custom view that you want to define its intrinsic content size instead of size constraints, fill free to override its property since it's open.

Conclusion

In this article, we documented how View Code works and what the best coding practices are to create reusable components. We also covered some important concepts like constraint animations and intrinsic content size. If you wanna learn more about building pure reusable UI components independent of the project and business rules, you should check out my other article about Design System.

I hope it answered most of your questions and you enjoyed the read :).

--

--

iOS | Android Developer - WWDC19 scholarship winner- Blockchain enthusiast