A key step in learning to develop apps using SwiftUI is learning how to declare user interface layouts both by making use of the built-in SwiftUI views as well as building your own custom views. This chapter will introduce the basic concepts of SwiftUI views and outline the syntax used to declare user interface layouts and modify view appearance and behavior.
SwiftUI Views
User interface layouts are composed in SwiftUI by using, creating and combining views. An important first step is to understand what is meant by the term “view”. Views in SwiftUI are declared as structures that conform to the View protocol. In order to conform with the View protocol, a structure is required to contain a body property and it is within this body property that the view is declared.
SwiftUI includes a wide range of built-in views that can be used when constructing a user interface including text label, button, text field, menu, toggle and layout manager views. Each of these is a self-contained instance that complies with the View protocol. When building an app with SwiftUI you will use these views to create custom views of your own which, when combined, constitute the appearance and behavior of your user interface.
These custom views will range from subviews that encapsulate a reusable subset of view components (perhaps a secure text field and a button for logging in to screens within your app) to views that encapsulate the user interface for an entire screen. Regardless of the size and complexity of a custom view or the number of child views encapsulated within, a view is still just an instance that defines some user interface appearance and behavior.
Creating a Basic View
In Xcode, custom views are contained within SwiftUI View files. When a new SwiftUI project is created, Xcode will create a single SwiftUI View file containing a single custom view consisting of a single Text view component. Additional view files can be added to the project by selecting the File -> New -> File… menu option and choosing the SwiftUI View file entry from the template screen.
The default SwiftUI View file is named ContentView.swift and reads as follows:
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Code language: Swift (swift)
The view is named ContentView and is declared as conforming to the View protocol. It also includes the mandatory body property which, in turn contains an instance of the built-in Text view component which is initialized with a string which reads “Hello, world!”.
The second structure in the file is needed to create an instance of ContentView so that it appears in the preview canvas, a topic which will be covered in detail in later chapters.
Adding Additional Views
Additional views can be added to a parent view by placing them in the body. The body property, however, is configured to return a single view. Adding an additional view, as is the case in the following example, will cause Xcode to create a second preview containing just the “Goodbye, world!” text view:
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.padding()
Text("Goodbye, world!")
}
}
Code language: Swift (swift)
To correctly add additional views, those views must be placed in a container view such as a stack or form. The above example could, therefore, be modified to place the two Text views in a vertical stack (VStack) view which, as the name suggests, positions views vertically within the containing view:
struct ContentView: View {
var body: some View {
VStack {
Text("Hello, world!")
.padding()
Text("Goodbye, world!")
}
}
}
Code language: Swift (swift)
SwiftUI views are hierarchical by nature, starting with parent and child views. This allows views to be nested to multiple levels to create user interfaces of any level of complexity. Consider, for example, the following view hierarchy diagram:

Figure 20-1
The equivalent view declaration for the above view would read as follows:
struct ContentView: View {
var body: some View {
VStack {
VStack {
Text("Text 1")
Text("Text 2")
HStack {
Text("Text 3")
Text("Text 4")
}
}
Text("Text 5")
}
}
}
Code language: Swift (swift)
A notable exception to the requirement that multiple views be embedded in a container is that multiple Text views count as a single view when concatenated. The following, therefore, is a valid view declaration:
struct ContentView: View {
var body: some View {
Text("Hello, ") + Text("how ") + Text("are you?")
}
}
Code language: Swift (swift)
Note that in the above examples the closure for the body property does not have a return statement. This is because the closure essentially contains a single expression (implicit returns from single expressions were covered in the chapter entitled “Swift Functions, Methods and Closures”). As soon as extra expressions are added to the closure, however, it will be necessary to add a return statement, for example:
struct ContentView: View {
var body: some View {
var myString: String = "Welcome to SwiftUI"
return VStack {
Text("Hello, world!")
.padding()
Text("Goodbye, world")
}
}
}
Code language: Swift (swift)
Working with Subviews
Apple recommends that views be kept as small and lightweight as possible. This promotes the creation of reusable components, makes view declarations easier to maintain and results in more efficient layout rendering.
If you find that a custom view declaration has become large and complex, identify areas of the view that can be extracted into a subview. As a very simplistic example, the HStack view in the above example could be extracted as a subview named “MyHStackView” as follows:
struct ContentView: View {
var body: some View {
VStack {
VStack {
Text("Text 1")
Text("Text 2")
MyHStackView()
}
Text("Text 5")
}
}
}
struct MyHStackView: View {
var body: some View {
HStack {
Text("Text 3")
Text("Text 4")
}
}
}
Code language: Swift (swift)
Views as Properties
In addition to creating subviews, views may also be assigned to properties as a way to organize complex view hierarchies. Consider the following example view declaration:
struct ContentView: View {
var body: some View {
VStack {
Text("Main Title")
.font(.largeTitle)
HStack {
Text("Car Image")
Image(systemName: "car.fill")
}
}
}
}
Code language: Swift (swift)
Any part of the above declaration can be moved to a property value, and then referenced by name. In the following declaration, the HStack has been assigned to a property named carStack which is then referenced within the VStack layout:
struct ContentView: View {
let carStack = HStack {
Text("Car Image")
Image(systemName: "car.fill")
}
var body: some View {
VStack {
Text("Main Title")
.font(.largeTitle)
carStack
}
}
}
Code language: Swift (swift)
Modifying Views
It is unlikely that any of the views provided with SwiftUI will appear and behave exactly as required without some form of customization. These changes are made by applying modifiers to the views.
All SwiftUI views have sets of modifiers which can be applied to make appearance and behavior changes. These modifiers take the form of methods that are called on the instance of the view and essentially wrap the original view inside another view which applies the necessary changes. This means that modifiers can be chained together to apply multiple modifications to the same view. The following, for example, changes the font and foreground color of a Text view:
Text("Text 1")
.font(.headline)
.foregroundColor(.red)
Code language: Swift (swift)
Similarly, the following example uses modifiers to configure an Image view to be resizable with the aspect ratio set to fit proportionally within the available space:
Image(systemName: "car.fill")
.resizable()
.aspectRatio(contentMode: .fit)
Code language: Swift (swift)
Modifiers may also be applied to custom subviews. In the following example, the font for both Text views in the previously declared MyHStackView custom view will be changed to use the large title font style:
MyHStackView()
.font(.largeTitle)
Code language: Swift (swift)
Working with Text Styles
In the above example the font used to display text on a view was declared using a built-in text style (in this case the large title style).
iOS provides a way for the user to select a preferred text size which applications are expected to adopt when displaying text. The current text size can be configured on a device via the Settings -> Display & Brightness -> Text Size screen which provides a slider to adjust the font size as shown below:

Figure 20-2
If a font has been declared on a view using a text style, the text size will dynamically adapt to the user’s preferred font size. Almost without exception, the built-in iOS apps adopt the preferred size setting selected by the user when displaying text and Apple recommends that third-party apps also conform to the user’s chosen text size. The following text style options are currently available:
- Large Title
- Title, Title2, Title 3
- Headline
- Subheadline
- Body
- Callout
- Footnote
- Caption1, Caption2
If none of the text styles meet your requirements, it is also possible to apply custom fonts by declaring the font family and size. Although the font size is specified in the custom font, the text will still automatically resize based on the user’s preferred dynamic type text size selection:
Text("Sample Text")
.font(.custom("Copperplate", size: 70))
Code language: Swift (swift)
The above custom font selection will render the Text view as follows:

Figure 20-3
Modifier Ordering
When chaining modifiers, it is important to be aware that the order in which they are applied can be significant. Both border and padding modifiers have been applied to the following Text view.
Text("Sample Text")
.border(Color.black)
.padding()
Code language: Swift (swift)
The border modifier draws a black border around the view and the padding modifier adds space around the view. When the above view is rendered it will appear as shown in Figure 20-4:

Figure 20-4
Given that padding has been applied to the text, it might be reasonable to expect there to be a gap between the text and the border. In fact, the border was only applied to the original Text view. Padding was then applied to the modified view returned by the border modifier. The padding is still applied to the view, but outside of the border. For the border to encompass the padding, the order of the modifiers needs to be changed so that the border is drawn on the view returned by the padding modifier:
Text("Sample Text")
.padding()
.border(Color.black)
Code language: Swift (swift)
With the modifier order switched, the view will now be rendered as follows:

Figure 20-5
If you don’t see the expected effects when working with chained modifiers, keep in mind this may be because of the order in which they are being applied to the view.
Custom Modifiers
SwiftUI also allows you to create your own custom modifiers. This can be particularly useful if you have a standard set of modifiers that are frequently applied to views. Suppose that the following modifiers are a common requirement within your view declarations:
Text("Text 1")
.font(.largeTitle)
.background(Color.white)
.border(Color.gray, width: 0.2)
.shadow(color: Color.black, radius: 5, x: 0, y: 5)
Code language: Swift (swift)
Instead of applying these four modifiers each time text with this appearance is required, a better solution is to group them into a custom modifier and then reference that modifier each time the modification is needed. Custom modifiers are declared as structs that conform to the ViewModifier protocol and, in this instance, might be implemented as follows:
struct StandardTitle: ViewModifier {
func body(content: Content) -> some View {
content
.font(.largeTitle)
.background(Color.white)
.border(Color.gray, width: 0.2)
.shadow(color: Color.black, radius: 5, x: 0, y: 5)
}
}
Code language: Swift (swift)
The custom modifier is then applied when needed by passing it through to the modifier() method:
Text("Text 1")
.modifier(StandardTitle())
Text("Text 2")
.modifier(StandardTitle())
Code language: Swift (swift)
With the custom modifier implemented, changes can be made to the StandardTitle implementation and those changes will automatically propagate through to all views that use the modifier. This avoids the need to manually change the modifiers on multiple views.
Basic Event Handling
Although SwiftUI is described as being data driven, it is still necessary to handle the events that are generated when a user interacts with the views in the user interface. Some views, such as the Button view, are provided solely for the purpose of soliciting user interaction. In fact, the Button view can be used to turn a variety of different views into a “clickable” button. A Button view needs to be declared with the action method to be called when a click is detected together with the view to act as the button content. It is possible, for example, to designate an entire stack of views as a single button. In most cases, however, a Text view will typically be used as the Button content. In the following implementation, a Button view is used to wrap a Text view which, when clicked, will call a method named buttonPressed():
struct ContentView: View {
var body: some View {
Button(action: buttonPressed) {
Text("Click Me")
}
}
func buttonPressed() {
// Code to perform action here
}
}
Code language: Swift (swift)
Instead of specifying an action function, the code to be executed when the button is clicked may also be specified as a closure in-line with the declaration:
Button(action: {
// Code to perform action here
}) {
Text("Click Me")
}
Code language: Swift (swift)
Another common requirement is to turn an Image view into a button, for example:
Button(action: {
print("Button clicked")
}) {
Image(systemName: "square.and.arrow.down")
}
Code language: Swift (swift)
Building Custom Container Views
As outlined earlier in this chapter, subviews provide a useful way to divide a view declaration into small, lightweight and reusable blocks. One limitation of subviews, however, is that the content of the container view is static. In other words, it is not possible to dynamically specify the views that are to be included at the point that a subview is included in a layout. The only children included in the subview are those that are specified in the original declaration.
Consider the following subview which consists of three TextViews contained within a VStack and modified with custom spacing and font settings.
struct MyVStack: View {
var body: some View {
VStack(spacing: 10) {
Text("Text Item 1")
Text("Text Item 2")
Text("Text Item 3")
}
.font(.largeTitle)
}
}
Code language: Swift (swift)
To include an instance of MyVStack in a declaration, it would be referenced as follows:
MyVStack()
Code language: Swift (swift)
Suppose, however, that a VStack with a spacing of 10 and a large font modifier is something that is needed frequently within a project, but in each case, different child views are required to be contained within the stack. While this flexibility isn’t possible using subviews, it can be achieved using the SwiftUI ViewBuilder closure attribute when constructing custom container views.
A ViewBuilder takes the form of a Swift closure which can be used to create a custom view comprised of multiple child views, the content of which does not need to be declared until the view is used within a layout declaration. The ViewBuilder closure takes the content views and returns them as a single view which is, in effect, a dynamically built subview.
The following is an example of using the ViewBuilder attribute to implement our custom MyVStack view:
struct MyVStack<Content: View>: View {
let content: () -> Content
init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}
var body: some View {
VStack(spacing: 10) {
content()
}
.font(.largeTitle)
}
}
Code language: Swift (swift)
Note that this declaration still returns an instance that complies with the View protocol and that the body contains the VStack declaration from the previous subview. Instead of including static views to be included in the stack, however, the child views of the stack will be passed to the initializer, handled by ViewBuilder and embedded into the VStack as child views. The custom MyVStack view can now be initialized with different child views wherever it is used in a layout, for example:
MyVStack {
Text("Text 1")
Text("Text 2")
HStack {
Image(systemName: "star.fill")
Image(systemName: "star.fill")
Image(systemName: "star")
}
}
Code language: Swift (swift)
Working with the Label View
The Label view is different from most other SwiftUI views in that it comprises two elements in the form of an icon and text positioned side-by-side. The image can take the form of any image asset, a SwiftUI Shape rendering or an SF Symbol.
SF Symbols is a collection of over 1500 scalable vector drawings available for use when developing apps for Apple platforms and designed to complement Apple’s San Francisco system font.
The full set of symbols can be searched and browsed by installing the SF Symbols macOS app available from the following URL:
https://developer.apple.com/design/downloads/SF-Symbols.dmg
The following is an example of the Label view using an SF Symbol together with a font() modifier to increase the size of the icon and text:
Label("Welcome to SwiftUI", systemImage: "person.circle.fill")
.font(.largeTitle)
Code language: Swift (swift)
The above view will be rendered as shown in Figure 20-6 below:

Figure 20-6
By referencing systemImage: in the Label view declaration we are indicating that the icon is to be taken from the built-in SF Symbol collection. To display an image from the app’s asset catalog, the following syntax would be used instead:
Label("Welcome to SwiftUI", image: "myimage")
Code language: Swift (swift)
Instead of specifying a text string and an image, the Label may also be declared using separate views for the title and icon. The following Label view declaration, for example, uses a Text view for the title and a Circle drawing for the icon:
Label(
title: {
Text("Welcome to SwiftUI")
.font(.largeTitle)
},
icon: { Circle()
.fill(Color.blue)
.frame(width: 25, height: 25)
}
)
Code language: Swift (swift)
When rendered, the above Label view will appear as shown in Figure 20-7:

Figure 20-7
Summary
SwiftUI user interfaces are declared in SwiftUI View files and are composed of components that conform to the View protocol. To conform with the View protocol a structure must contain a property named body which is itself a View.
SwiftUI provides a library of built-in components for use when designing user interface layouts. The appearance and behavior of a view can be configured by applying modifiers, and views can be modified and grouped together to create custom views and subviews. Similarly, custom container views can be created using the ViewBuilder closure property.
When a modifier is applied to a view, a new modified view is returned and subsequent modifiers are then applied to this modified view. This can have significant implications for the order in which modifiers are applied to a view.