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 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.