A SwiftUI Example Tutorial

Now that many of the fundamentals of SwiftUI development have been covered, this chapter will begin to put this theory into practice through the design and implementation of an example SwiftUI based project.

The objective of this chapter is to demonstrate the use of Xcode to design a simple interactive user interface, making use of views, modifiers, state variables and some basic animation techniques. Throughout the course of this tutorial a variety of different techniques will be used to add and modify views. While this may appear to be inconsistent, the objective is to gain familiarity with the different options available.

1.1 Creating the Example Project

Start Xcode and select the option to create a new project. On the template selection screen, make sure iOS is selected and choose the Single View App option before proceeding to the next screen as shown in Figure 22‑1 below:

Figure 22‑1

On the project options screen, name the project SwiftUIDemo and change the User Interface menu to SwiftUI as highlighted in Figure 22‑2:

Figure 22‑2

Click Next to proceed to the final screen, choose a suitable filesystem location for the project and click on the Create button.

1.2 Reviewing the Project

Once the project has been created it will contain the standard AppDelegate and SceneDelegate classes along with a SwiftUI View file named ContentView.swift which should have loaded into the editor and preview canvas ready for modification (if it has not loaded, simply select it in the project navigator panel). If the preview canvas is in the paused state, click on the Resume button to build the project and display the preview:

Figure 22‑3

By default, Xcode has created a single content view and is configured to launch this view as the initial view when the project is run (otherwise known as the root view controller). As outlined on the chapter entitled The Anatomy of a Basic SwiftUI Project, the code that makes this the root view controller is located in the willConnectTo method located in the SceneDelegate.swift file and reads as follows:

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, 
           options connectionOptions: UIScene.ConnectionOptions) {

    let contentView = ContentView()

    if let windowScene = scene as? UIWindowScene {
        let window = UIWindow(windowScene: windowScene)
        window.rootViewController = 
                    UIHostingController(rootView: contentView)
        self.window = window
        window.makeKeyAndVisible()
    }
}

If you find while developing a project with multiple view files that a different view file needs to serve as the content of the root view controller, simply modify this code accordingly to reference the other view.

1.3 Adding a VStack to the Layout

The view body currently consists of a single Text view of which we will make use in the completed project. A container view now needs to be added so that other views can be included in the layout. For the purposes of this example, the layout will be stacked vertically so a VStack needs to be added to the layout.

Within the code editor, select the Text view entry, hold down the Command key on the keyboard and perform a left-click. From the resulting menu, select the Embed in VStack option:

Figure 22‑4

Once the Text view has been embedded into the VStack the declaration will read as follows:

struct ContentView: View {

    var body: some View {

        VStack {
            Text("Hello World")
        }
    }
}

1.4 Adding a Slider View to the Stack

The next item to be added to the layout is a Slider view. Within Xcode, display the Library panel by clicking on the ‘+’ button highlighted in Figure 22‑5, locate the Slider in the View list and drag it over the top of the existing Text view within the preview canvas. Make sure the notification panel (also highlighted in Figure 22‑5) indicates that the view is going to be inserted into the existing stack (as opposed to being placed in a new vertical stack) before dropping the view into place.

Figure 22‑5

Once the slider has been dropped into place, the view implementation should read as follows:

struct ContentView: View {

    var body: some View {

        VStack {
            VStack {
                Text("Hello World")
                Slider(value: Value)
            }
        }
    }
}

1.5 Adding a State Property

The slider is going to be used to control the amount by which the Text view is to be rotated. As such, a binding needs to be established between the Slider view and a state property into which the current rotation value will be stored. Within the code editor, declare this property and configure the Slider to use a range between 0 and 360 in increments of 0.1:

struct ContentView: View {

    @State private var rotation: Double = 0

    var body: some View {
        VStack {
            VStack {
                Text("Hello World")
                Slider(value: $rotation, in: 0 ... 360, step: 0.1)
            }
        }
    }
}

Note that since we are declaring a binding between the Slider view and the rotation state property it is prefixed by a ‘$’ character.

1.6 Adding Modifiers to the Text View

The next step is to add some modifiers to the Text view to change the font and to adopt the rotation value stored by the Slider view. Begin by displaying the Library panel, switch to the modifier list and drag and drop a font modifier onto the Text view entry in the code editor:

Figure 22‑6

Select the modifier line in the editor, refer to the Attributes inspector panel and change the font property from Title to Large Title as shown in Figure 22‑7:

Figure 22‑7

Note that the modifier added above does not change the font weight. Since modifiers may also be added to a view from within the Attributes inspector, take this opportunity to change the setting of the Weight menu from Inherited to Heavy.

On completion of these steps, the View body should read as follows:

var body: some View {

    VStack {
        VStack {
            Text("Hello World")
                .font(.largeTitle)
                .fontWeight(.heavy)
            Slider(value: $rotation, in: 0 ... 360, step: 0.1)
        }
    }
}

1.7 Adding Rotation and Animation

The next step is to add the rotation and animation effects to the Text view using the value stored by the Slider (animation is covered in greater detail in the SwiftUI Animation and Transitions chapter). This can be implemented using a modifier as follows:

.
.

Text("Hello World")
    .font(.largeTitle)
    .fontWeight(.heavy)
    .rotationEffect(.degrees(self.rotation)
.
.

Note that since we are simply reading the value assigned to the rotation state property, as opposed to establishing a binding, the property name is not prefixed with the ‘$’ sign notation.

Click on the Live Preview button (indicated by the arrow in Figure 22‑8), wait for the code to compile, then use the slider to rotate the Text view:

Figure 22‑8

Next, add an animation modifier to the Text view to animate the rotation over 5 seconds using the Ease In Out effect:

Text("Hello World")
    .font(.largeTitle)
    .fontWeight(.heavy)
    .rotationEffect(.degrees(self.rotation))
    .animation(.easeInOut(duration: 5))

Use the slider once again to rotate the text and note that rotation is now smoothly animated.

1.8 Adding a TextField to the Stack

In addition to supporting text rotation, the app will also allow custom text to be entered and displayed on the Text view. This will require the addition of a TextField view to the project. To achieve this, either directly edit the View structure or use the Library panel to add a TextField so that the structure reads as follows (note also the addition of a state property in which to store the custom text string and the change to the Text view to use this property):

struct ContentView: View {

    @State private var rotation: Double = 0
    @State private var text: String = "Welcome to SwiftUI"

    var body: some View {

        VStack {
            VStack {
                Text(text)
                    .font(.largeTitle)
                    .fontWeight(.heavy)
                    .rotationEffect(.degrees(self.rotation))
                    .animation(.easeInOut(duration: 5))

                Slider(value: $rotation, in: 0 ... 360, step: 0.1)

                TextField("Enter text here", text: $text)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
            }
        }
    }
}

When the user enters text into the TextField view, that text will be stored in the text state property and will automatically appear on the Text view via the binding.

Return to the preview canvas and make sure that the changes work as expected.

1.9 Adding a Color Picker

The final view to be added to the stack before we start to tidy up the layout is a Picker view. The purpose of this view will be to allow the foreground color of the Text view to be chosen by the user from a range of color options. Begin by adding some arrays of color names and Color objects, together with a state property to hold the current array index value as follows:

import SwiftUI

struct ContentView: View {

    var colors: [Color] = [.black, .red, .green, .blue]
    var colornames = ["Black", "Red", "Green", "Blue"]

    @State private var colorIndex = 0
    @State private var rotation: Double = 0
    @State private var text: String = "Welcome to SwiftUI"

With these variables configured, display the Library panel, locate the Picker in the Views screen and drag and drop it beneath the TextField view in either the code editor or preview canvas so that it is embedded in the existing VStack layout. Once added, the view entry will read as follows:

Picker(selection: .constant(1, label: Text("Picker") {
    Text("1").tag(1)
    Text("2").tag(2)
}

The Picker view needs to be configured to store the current selection in the colorIndex state property and to display an option for each color name in the colorNames array. To make the Picker more visually appealing, the background color for each Text view will be changed to the corresponding color in the colors array.

For the purposes of iterating through the colorNames array, the code will make use of the SwiftUI ForEach construct, the purpose of which is to generate multiple views from a set of data such as an array. Within the editor, modify the Picker view declaration so that it reads as follows:

Picker(selection: $colorIndex, label: Text("Color")) {
    ForEach (0 ..< colornames.count) {
        Text(self.colornames[$0])
            .foregroundColor(self.colors[$0])
    }
}

Remaining in the code editor, locate the Text view and add a foreground color modifier to set the foreground color based on the current Picker selection value:

Text(text)
    .font(.largeTitle)
    .fontWeight(.heavy)
    .rotationEffect(.degrees(self.rotation))
    .animation(.easeInOut(duration: 5))
    .foregroundColor(self.colors[self.colorIndex])

Test the app in the preview canvas and confirm that the Picker view appears with all of the color names using the corresponding foreground color and that color selections are reflected in the Text view.

1.10 Tidying the Layout

Up until this point the focus of this tutorial has been on the appearance and functionality of the individual views. Aside from making sure the views are stacked vertically, however, no attention has been paid to the overall appearance of the layout. At this point the layout should resemble that shown in Figure 22‑9:

Figure 22‑9

The first improvement that is needed is to add some space around the Slider, TextField and Picker views so that they are not so close to the edge of the device display. To achieve this, we will add some padding modifiers to the views:

Slider(value: $rotation, in: 0 ... 360, step: 0.1)
    .padding()

TextField("Enter text here", text: $text)
    .textFieldStyle(RoundedBorderTextFieldStyle())
    .padding()

Picker(selection: $colorIndex, label: Text("Color")) {

    ForEach (0 ..< colornames.count) {
        Text(self.colornames[$0])
            .foregroundColor(self.colors[$0])
    }
}
.padding()

Next, the layout would probably look better if the Views were evenly spaced. One way to implement this is to add some Spacer views before and after the Text view:

VStack {

        Spacer()

        Text(text)

            .font(.largeTitle)

            .fontWeight(.heavy)

            .rotationEffect(.degrees(self.rotation))

            .animation(.easeInOut(duration: 5))

            .foregroundColor(self.colors[self.colorIndex])

        Spacer()

        Slider(value: $rotation, in: 0 ... 360, step: 0.1)
           .padding()

        TextField("Enter text here", text: $text)
            .textFieldStyle(RoundedBorderTextFieldStyle())
            .padding()

        Picker(selection: $colorIndex, label: Text("Color")) {
            ForEach (0 ..< colornames.count) {
                Text(self.colornames[$0])
                    .foregroundColor(self.colors[$0])
            }
        }
        .padding()
}

The Spacer view provides a flexible space between views that will expand and contract based on the requirements of the layout. If a Spacer is contained in a stack it will resize along the stack axis. When used outside of a stack container, a Spacer view can resize both horizontally and vertically.

To make the separation between the Text view and the Slider more obvious, also add a Divider view to the layout:

.

.

VStack {
    Spacer()
    Text(text)
        .font(.largeTitle)
        .fontWeight(.heavy)
        .rotationEffect(.degrees(self.rotation))
        .animation(.easeInOut(duration: 5))
        .foregroundColor(self.colors[self.colorIndex])
    Spacer()
    Divider()
.
.

The Divider view draws a line to indicate separation between two views in a stack container.

With these changes made, the layout should now appear in the preview canvas as shown in Figure 22‑10:

Figure 22‑10

1.11 Summary

The goal of this chapter has been to put into practice some of the theory covered in the previous chapters through the creation of an example app project. In particular, the tutorial made use of a variety of techniques for adding views to a layout in addition to the use of modifiers and state property bindings. The chapter also introduced the Spacer and Divider views and made use of the ForEach structure to dynamically generate views from a data array.