To customize a view in a UIKit or AppKit application, you update one or more of the view's properties. For example, to change the text color of a UILabel instance, you update the label's textColor property. That is an imperative approach to user interface development.

Remember that SwiftUI takes a declarative approach. To customize a view in a SwiftUI application, you simply describe what the view should look like using view modifiers. In this episode, you learn what view modifiers are and how they can be used to customize views.

View Modifiers

In the previous episode, we invoked the font(_:) method on a Text view to change the font of the text the Text view displays. The font(_:) method is a view modifier. Views and modifiers work in tandem to define the user interface you have in mind. Let's take a closer look at how that works.

Revisit the Notes project and open ContentView.swift. Click the Resume button in the top right or press Command + Option + P to resume the preview on the right.

View modifiers, or modifiers for short, are methods you invoke on views to customize them. The result is another view. Because a view modifier returns another view, view modifiers can chained. This makes it straightforward to combine several view modifiers to create the view you have in mind.

We can change the text color of the bottom Text view by invoking the foregroundColor(_:) method on the view the font(_:) method returns. The foregroundColor(_:) method accepts an argument of type Color?. Color is itself a view. We work with the Color struct many more times in this series. Let's change the text color of the bottom Text view to a green color.

var body: some View {
    VStack {
        Text(title)
        Text("A place to collect your notes.")
            .font(.title)
            .foregroundColor(.green)
    }
}

A view modifier is applied to a view and returns another view. It wraps the view it is applied to in another view and returns the result. By applying the font modifier to the bottom Text view, we describe that the font of the Text view should be title. The same applies to the foregroundColor modifier. By applying the foregroundColor modifier to the view the font modifier returns, we describe that the text color of the Text view should be green.

It is important to emphasize that we are describing the user interface of the ContentView struct. Don't worry about the number of views and view modifiers you need to describe the user interface. The views we create aren't rendered as is. SwiftUI inspects the declaration of ContentView and translates that declaration into a data structure it uses to render the user interface. It performs a range of optimizations to make this efficient and performant. Remember that views are lightweight.

I want to make sure you understand how SwiftUI's declarative approach differs from UIKit's and AppKit's imperative approach. Using UIKit and AppKit, you keep the view hierarchy as simple as possible to make sure your application is performant. That part is handled by SwiftUI. You don't need to worry about efficiency or performance. You only describe the user interface you have in mind using SwiftUI's declarative API. I cannot stress enough how important that difference is.

Container Views

SwiftUI's declarative API is designed to create complex user interfaces with ease. The API is flexible and efficient. For example, if we want the text color of both Text views to be green, we don't need to apply the foregroundColor modifier to both Text views. To accomplish this, you apply the foregroundColor modifier to the parent of the Text views, the vertical stack. We move the foregroundColor modifier up one level. Take a look at the preview to see the result.

var body: some View {
    VStack {
        Text(title)
        Text("A place to collect your notes.")
            .font(.title)
    }
    .foregroundColor(.green)
}

Any of the child views of the vertical stack can override what it inherits from its parent. Let's set the text color of the bottom Text view to red to make it more prominent. We override the effect of the foregroundColor modifier of the parent by applying the foregroundColor modifier to the bottom Text view.

var body: some View {
    VStack {
        Text(title)
        Text("A place to collect your notes.")
            .font(.title)
            .foregroundColor(.red)
    }
    .foregroundColor(.green)
}

Order Matters

The order in which view modifiers are applied to a view is important. Let me show you an example. We customize the appearance of the top Text view. We apply the background modifier to add a black background and the padding modifier to add padding to the Text view.

Text(title)
    .background(Color.black)
    .padding()

Take a look at the preview on the right. Does the result surprise you? Let's change the order of the view modifiers. Apply the padding modifier first, followed by the background modifier.

Text(title)
    .padding()
    .background(Color.black)

The result illustrates that the order of the view modifiers you apply matters. This isn't always true, but it is in this example. Let me explain what is happening.

In the first example, we add a black background color to the Text view. The padding modifier adds padding to the view the background modifier returns. In the second example, we first add padding to the Text view. The background modifier adds a black background to the view the padding modifier returns. The preview illustrates that the order of the view modifiers matters. Changing the order can result in a significantly different user interface.

Incompatible View Modifiers

The View protocol defines a wide range of modifiers, many of which we use in this series. Some views define modifiers of their own. To underline the text of a Text view, we apply the underline modifier.

Remove the background modifier from the top Text view. Let's underline the text of the top Text view by applying the underline modifier. The compiler doesn't like this change and throws an error to make that clear.

Text(title)
    .padding()
    .underline()

The order of view modifiers matters in SwiftUI.

The problem is quite simple. The return type of the padding(_:) method is some View, an opaque type. The underline(_:color:) method is defined on the Text struct so the compiler has every right to complain. We are attempting to invoke a method of the Text struct on an object that conforms to the View protocol. Because the padding modifier hides the concrete type of the view it is applied to, we can't apply the underline modifier to the view the padding modifier returns.

The solution is simple. We apply the underline modifier on the Text view and apply the padding modifier on the view the underline modifier returns. The underline(_:color:) method returns a Text view. It doesn't hide the concrete type of the view it is applied to. This means we can apply the bold modifier to the view the underline modifier returns even though the bold() method is also defined on the Text struct.

Text(title)
    .underline()
    .bold()
    .padding()

Custom View Modifiers

SwiftUI defines a wide range of view modifiers to customize the look and feel of a view. That said, you are not limited to the view modifiers SwiftUI defines. You can define your own view modifiers to reduce code duplication and build user interfaces that are consistent and easy to maintain. You learn more about custom view modifiers later in this series.

What's Next?

Views and view modifiers are the building blocks of every user interface built with SwiftUI. Remember that you don't need to worry about the number of views and view modifiers you need to describe the user interface of your application. SwiftUI simply uses the declaration of a view to build the view hierarchy of your application, performing optimizations under the hood to guarantee a responsive and performant user experience.