Chapters

Hide chapters

iOS Apprentice

Eighth Edition · iOS 13 · Swift 5.2 · Xcode 11

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

My Locations

Section 3: 11 chapters
Show chapters Hide chapters

Store Search

Section 4: 12 chapters
Show chapters Hide chapters

44. Hello, SwiftUI
Written by Joey deVilla

If you’ve made it this far into iOS Apprentice, congratulations! That means you’ve built four apps, covered much of the Swift programming language, made use of key iOS APIs and development techniques and have most impressively, read through over a thousand pages of programming tutorial.

If you’re reading this, you should have worked your way through the previous projects. Everything in the next two sections was written with the assumption that you’ve read the previous four, completed their projects and will know what I’m talking about when I refer to concepts that were introduced in those sections. Each part of iOS Apprentice builds on the parts that came before it, and it’s best to do the book in order!

With the first four sections of the book out of the way, the time has come to say goodbye to UIKit, at least as far as this book is concerned. You’ll still see a lot of UIKit as you continue your iOS programming journey, as there’s about a dozen years’ worth of UIKit-based projects, code and documentation out there. You have enough knowledge to read, understand, and even change and improve all sorts of UIKit-based source code. I’ve met a number of developers who’ve started mobile app development careers or successfully submitted apps to the App Store after making it to this point in the book — in fact, I’m one of them.

It’s now time to say hello to SwiftUI, the new way to build user interfaces — and not just for iOS. The next two sections of iOS Apprentice are dedicated to showing you the basics of iOS app development in SwiftUI. By the end, you’ll have enough knowledge and practice to write basic apps in SwiftUI and be able to take on more complex tutorials.

In order to keep the focus primarily on SwiftUI, the two apps that you’ll build in these sections will be ones that you’ve already seen, namely:

  1. Bullseye
  2. Checklist (a simplified version of Checklists that uses a single list)

Take a moment to reflect on how far you’ve come, and then get ready to dive into SwiftUI!

This chapter covers the following:

  • Introducing SwiftUI: After some congratulations on getting this far into the book, a quick explanation of why SwiftUI exists.
  • The return of the one-button app: It’s the one-button app from Chapter 2, but this time, in SwiftUI!
  • Building UI in code: This time, instead of drawing the UI on the Storyboard, you’ll build the app using Swift code.
  • State and SwiftUI: What is “state,” and what does it have to do with SwiftUI?
  • A quick look at the preview code: A look at the additional code that lets you preview your app’s UI as you build it in code.
  • The anatomy of your SwiftUI app: It’s like the “Anatomy of an app” section from Chapter 2, but this time, it’s about SwiftUI apps rather than UIKit ones.

UIKit’s downsides

UIKit has served iOS developers really well over the past dozen years, but it’s not without its downsides. I’m sure you’ve run into a few of them while working on this book’s projects. Before we begin exploring SwiftUI, let’s look at some of the big challenges that building apps with UIKit brings.

Building apps with UIKit requires two very different tools

In building the apps in this book so far, you’ve been using two very different tools. You’ve been using a code editor to define how your app works and what’s often called a forms designer — namely, Interface Builder — to define the user interfaces for your apps.

Both tools are part of Xcode, but that wasn’t always the case. Before Xcode 4, Interface Builder was a separate application. You would draw your screens in Interface Builder, save them as .nib files, then import them into Xcode.

Using a combination of a code editor and a forms designer to build programs has been around for almost as long as we’ve had graphical user interfaces. It was popularized by HyperCard in the 1980s and grew in leaps and bound in the 1990s thanks to tools like Visual Basic. This approach lives on today not just in Xcode, but in tools such as Android Studio and Visual Studio as well.

Using a forms designer means that you’re constantly switching between a coding tool and a drawing tool. It also means that you’re working in two different languages: A programming language for the code and drawings of the user interface.

The user interface drawings are represented by an underlying markup language. In more modern tools like Android Studio and Visual Studio, the user interface markup language is reasonably easy to read, and you’ll often find yourself working directly with it. In an older tool like Interface Builder (which dates back to 1988!), the markup language is a tangle that only a machine could love:

<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15705" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
    <device id="retina6_1" orientation="portrait" appearance="light"/>
    <dependencies>
        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15706"/>
        <capability name="Safe area layout guides" minToolsVersion="9.0"/>
        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
    </dependencies>
    <scenes>
        <!--View Controller-->
        <scene sceneID="tne-QT-ifu">
            <objects>
                <viewController id="BYZ-38-t0r" customClass="ViewController" customModule="Storyboard_app" customModuleProvider="target" sceneMemberID="viewController">
                    <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
                        <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                        <subviews>
                            <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ILe-t6-2Qd">
                                <rect key="frame" x="184" y="433" width="46" height="30"/>
                                <state key="normal" title="Tap me"/>
                            </button>
                        </subviews>
                        <color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
                        <constraints>
                            <constraint firstItem="ILe-t6-2Qd" firstAttribute="centerY" secondItem="8bC-Xf-vdC" secondAttribute="centerY" id="Kb9-eL-B2J"/>
                            <constraint firstItem="ILe-t6-2Qd" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="z5H-tW-owy"/>
                        </constraints>
                        <viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
                    </view>
                </viewController>
                <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
            </objects>
            <point key="canvasLocation" x="139" y="139"/>
        </scene>
    </scenes>
</document>

In case you’re wondering what the previous monstrosity is, it’s the XML for a Storyboard containing a single view with a single button labeled “Tap me”, centered both horizontally and vertically. This almost-unreadable XML is why most people build their Storyboards and views graphically.

To see the XML behind any Storyboard file, right-click or control-click on the file in Project Navigator, select Open As in the pop-up menu that appears, and then select Source Code from the sub-menu.

Multiple screen resolutions, multiple problems

Working with Storyboards was simple when all iPhones had the same screen resolution. Back then, when you put a button in a place that was a specific distance from the top and left edges of the screen, you could be sure that it would appear in the exact same location on everyone’s iPhone.

The introduction of iPhones with different screen sizes and the iPad changed required the introduction of work-arounds such as Auto Layout and the View as: controls at the bottom of Interface Builder. View as: is reasonable easy to use, but Auto Layout is a constant source of frustration, even to experienced developers.

So many connections: Custom classes, outlets, and actions

Building the code one way and the user interface in a completely different way means that there needs to be some ways to connect the two. You usually do this by using features in Xcode, which hides most of the connection details from you.

One of these features is the Custom Class in the Identity Inspector, which connects the view controller for a view in the Storyboard to a specific view controller in the code.

The Custom Class menu connects a view controller on the Storyboard to a view controller in code
The Custom Class menu connects a view controller on the Storyboard to a view controller in code

In your UIKit projects, you’ve also been dragging and dropping actions/outlets to your classes from Interface Builder to Editor.

Dragging and dropping to create an outlet or action
Dragging and dropping to create an outlet or action

Dragging from a Storyboard and dropping onto view controller code is a clunky process, and the resulting code doesn’t tell you the whole story: The code for actions doesn’t tell you what event triggers the action. Unless you give the action method a meaningful name (which is a good idea; readable code is maintainable code, after all), you need to look at the Connections Inspector to find out which event the action is for.

Action code and the Connections Inspector
Action code and the Connections Inspector

Multiple platforms, multiple problems

Finally, there’s the issue of Apple’s many platforms, each one with its own user interface framework. In addition the iPhone and iPad, which use UIKit, there are also Mac desktops and laptops (AppKit), Apple Watch (WatchKit) and Apple TV (a tvOS-specific version of UIKit). Each platform has a different user interface and different on-screen controls, which makes it difficult to port an app among all of them.

Hello, SwiftUI!

Building apps with SwiftUI requires just one tool

With SwiftUI, you’re no longer working with Interface Builder or Storyboards, and you’re not switching between two different kinds of tools. You create your user interfaces completely in code.

This doesn’t mean that you can’t see your user interfaces as you build them. SwiftUI provides a Canvas that appears alongside the code editor. It gives you a dynamic preview of what the interface will look like, right beside the code you write.

Better still, you have options:

  1. You can create the user interface just by writing code. The Canvas will show you what the interface will look like.
  2. You can use the Canvas to graphically lay out the user interface. SwiftUI will automatically generate user interface code based on what you create in the Canvas.
  3. A mix of both writing code to build the UI and graphically laying out the UI in the Canvas. You can freely move between the two as you please.

No matter which way you choose to build user interfaces in SwiftUI, you get the benefits of being able to construct the user interface in code, combined with being able to see your work while you’re coding.

Since the UI and underlying code are in the same place, the relationships between the two are much clearer. SwiftUI also introduces the concept of state, which makes it possible for a change in a variable to immediately and automatically affect the UI, and vice versa.

Multiple platforms and screen resolutions, no problem!

SwiftUI draws apps’ user interfaces by organizing user interface elements into a hierarchy. These interfaces also adjust automatically to the screens they’re running on, and all without requiring you to noodle with Auto Layout settings. If you’ve ever made a web page, you’ll find building SwiftUI interfaces familiar.

Better still, it’s not just the new way to build user interfaces for iOS apps. It’s also the new way to build UIs for apps for Apple platforms: the Mac, Apple Watch, and Apple TV. SwiftUI draws the appropriate control for the platform it’s running on, which means that you can more easily port your app to another Apple platform. It means that you’re no longer just an iPhone/iPad developer, but a Mac, Apple Watch and Apple TV developer too!

A few downsides

SwiftUI isn’t perfect, and it does come with a few disadvantages that you need to be aware of:

  • SwiftUI works only on iOS 13 or later. Although iOS users are known for quickly updating to the latest version of the OS, most companies want to be able to support the previous version or two, in order to serve as many customers as possible. Because of this, many companies who currently have apps in the store may choose to stick with UIKit for a while.
  • SwiftUI is new. It still has some rough edges, and isn’t as fully-featured as UIKit, which has had a dozen years to evolve and grow. Once again, a company with apps in the store may decide to wait until SwiftUI has matured a little more.
  • There are few SwiftUI experts to call on. As I write this, SwiftUI has been released for less than a year, and few people outside of Apple can claim to have much SwiftUI experience — we’re all new at this. This state of affairs can also be an advantage: It means that you have a chance to become one of the early SwiftUI experts!

UIKit or SwiftUI?

You’re probably asking this question: “Should I learn UIKit or SwiftUI?”

The fact that this book covers both UIKit and SwiftUI should be a hint: You should learn both!

Here are the reasons why:

  • SwiftUI represents the future of not just iOS development, but development for all Apple platforms. By learning it now, you’ll be better prepared for the future, and the sooner you start, the more of a head start you’ll have over other iOS developers.
  • If you’re working on a “greenfield” project (that’s an engineering term for “brand new project without any limitations from older projects”) and are comfortable with targeting only iOS 13 or later, it might be a good candidate for SwiftUI.
  • UIKit isn’t going to go away for a while. The vast majority of iOS projects out there were written with UIKit, and in the near term, it’s not worth the time, effort, and money to rewrite them with SwiftUI. If you need to work on one of these “brownfield” projects (in software, these are projects based on prior work) an app that’s been out there for even a couple of years, you’re going to need to know UIKit.
  • Since it’s been around since the dawn of the modern smartphone industry, the vast majority of iOS literature and open source code is based on UIKit. In order to learn from these sources, you need to understand UIKit programming.

With all that preamble out of the way, let’s get started working with SwiftUI!

The return of the one-button app

Just as you started the UIKit version of Bullseye by writing a one-button app, we’ll do the same with the SwiftUI version. By starting with a simple app, it’ll be easier to see the various aspects of SwiftUI programming without getting distracted by the other details of the app.

You might remember that this app was incredibly simple. It presents the user with a single button in the center of the screen. When the user taps the button, it displays an alert.

Here’s what the SwiftUI version will look like in the end:

The one-button app in SwiftUI
The one-button app in SwiftUI

Creating the app

As with the UIKit version, start with a Single View App project:

Choosing the template for the new project
Choosing the template for the new project

You’ll be taken to the familiar Choose options for your new project pop-up, which you’ll fill out differently this time:

Configuring the new project
Configuring the new project

➤ Fill out these options as follows:

  • Product Name: Enter Bullseye SwiftUI here.

  • Team: Set this to None if you plan to stick to using the Simulator or your team name if you want to run the app on a device.

  • Organization Name and Organization Identifier: Use whatever you’ve been using for the previous projects.

  • Language: This should still be set to Swift.

  • User Interface: Here’s where the differences start. Set this to SwiftUI. Don’t pick Storyboard — you’ve already built that project!

  • Use Core Data, Include Unit Tests, and Include UI Tests: This project’s too simple to need these features, so leave these checkboxes unselected.

➤ Press Next. Xcode will ask where to save your project:

Choosing where to save the project
Choosing where to save the project

➤ Choose a location for the project files.

➤ Check Create Git repository on My Mac. Now that you know about Git, you should make a habit of using it. It doesn’t just save older versions of your code — it can save you a lot of time and aggravation, and I’ve even seen it save projects and jobs.

➤ Click Create to finish. Xcode will create the “Bullseye SwiftUI” project.

Exploring a slightly different user interface

You should now see a mostly familiar sight:

Xcode displays a new SwiftUI project
Xcode displays a new SwiftUI project

Let’s take a closer look at Xcode from left to right, starting with the Project Navigator:

The Project Navigator
The Project Navigator

You’ve seen most of the files in the project, but you might notice that a couple are missing:

  • ViewController.swift
  • Main.storyboard

In their place is a new file: ContentView.swift.

➤ Select ContentView.swift. Take a quick look at its code:

import SwiftUI

struct ContentView: View {
  var body: some View {
    Text("Hello, World!")
  }
}

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
  }
}

Before you get too involved with the code, take a look at the area to the right of the Editor. It’s called the Canvas and should look like this:

The empty Canvas
The empty Canvas

If you don’t see the Canvas, it might be hidden. To show it, open the Editor menu and select Canvas.

At the top left part of the Canvas, you’ll see the text Automatic preview updating paused with an “info” icon beside it. Tap the icon to enjoy another cryptic message from Xcode:

A cryptic message from Xcode
A cryptic message from Xcode

This is just Xcode’s way of saying that it needs to compile the user interface code in the editor before it can display anything in the Canvas. You can make Xcode do that by tapping the Resume button — either the one in the info pop-up or the one in the upper right corner of the Canvas.

The Canvas will display a progress indicator, and after a moment or two, you’ll see this:

A preview of the current iOS app
A preview of the current iOS app

You may need to zoom out to see the entire user interface. Use the zoom controls — the + and - at the lower right corner of the Canvas — to adjust the zoom level.

From the “Hello, World!” in both the code pane and the Canvas, you’re probably beginning to realize that the Canvas provides a visual preview of the user interface defined in the code. Think of the Canvas as a sort of replacement for the Storyboard, except that it shows you a single screen instead of a set of screens updating in real time.

Let’s try making changes to the app’s UI, starting with using the Canvas.

Editing the UI with the Canvas and the Attributes Inspector

➤ In the Canvas, click on “Hello, World!”:

'Hello, World!' highlighted in both the code editor and the Canvas
'Hello, World!' highlighted in both the code editor and the Canvas

Notice that “Hello, World!” is highlighted in both the Canvas and the Editor. In the Canvas, the highlighting looks like a fine blue rectangle drawn around “Hello, World!” and in the Editor, the line Text("Hello, World!") is highlighted. This dual highlighting is a hint that changes that you make in one pane will be reflected in the other.

The right side of Xcode, which contains the Canvas and the Attributes Inspector, should remind you of working with the Storyboard:

The Canvas and the Attributes Inspector
The Canvas and the Attributes Inspector

Let’s try changing the “Hello, World!” text as if you were still working with a Storyboard.

➤ In the Attributes Inspector, change the contents of the Text text field to “Hello there!” and press the Enter key:

Changing the text to 'Hello there!
Changing the text to 'Hello there!

You can also change the text by using an Inspector.

➤ In the Canvas, Command-click on “Hello there!”. A pop-up menu appears, listing a number of actions you can take:

Changing the text to 'Hello there!
Changing the text to 'Hello there!

➤ Select the Show SwiftUI Inspector… item from the menu. This brings up the Inspector, which displays the properties of “Hello there!” and lets you change them:

The Inspector for 'Hello there!
The Inspector for 'Hello there!

It may not be immediately apparent, but the Inspector is bigger than it appears. If you scroll while the cursor is over the Inspector, you can see all the properties:

Scrolling through the inspector
Scrolling through the inspector

➤ Scroll to the top of the Inspector and enter “Hey there!” into the Text text field:

Editing the text to say ‘Hey there!’
Editing the text to say ‘Hey there!’

➤ Click anywhere on the Inspector to dismiss it. Notice that the text is updated to “Hey there!” in the Editor, the Canvas and the Attributes Inspector:

The text is updated in the Editor, Canvas and Attributes Inspector
The text is updated in the Editor, Canvas and Attributes Inspector

Note: If the Canvas hasn’t updated itself to show the the new text, click the Resume button near the upper-right corner of the Canvas.

Editing the UI in code

SwiftUI’s real ability to edit the user interface in code. Let’s try another change to the text.

➤ In the Editor, change Text("Hey there!") to the following:

Text("Welcome to my first app!")

The change you just made in the code is reflected in the Canvas and the Attributes Inspector:

The text is updated in the Editor, Canvas and Attributes Inspector
The text is updated in the Editor, Canvas and Attributes Inspector

You’ve been viewing the outcomes of your changes in the Canvas, but haven’t yet run the app. Let’s make sure that they actually took effect by running the app.

➤ Click the Run button. You should see your changes when the app starts in the Simulator:

The app running in the Simulator
The app running in the Simulator

Now that you’ve played with SwiftUI a little, let’s look at how it’s really meant to be used.

Building a UI in code

The view and its preview

Now that we’ve explored the Canvas (SwiftUI’s version of Interface Builder). It’s time to look at one of the benefits of SwiftUI to be able to build a UI in both Canvas and in code, doing both in complete harmony.

Let’s start by looking at the code for ContentView.swift:

import SwiftUI

struct ContentView: View {
  var body: some View {
    Text("Welcome to my first app!")
  }
}

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
  }
}

Since this is a SwiftUI project, this code starts with import SwiftUI instead of import UIKit. This means that instead of the UIKit objects that you’ve become accustomed to working with, you’ll now be working with a whole new set of objects specific to SwiftUI.

Instead of the single ViewController class that Xcode automatically generates for a UIKit single view project, this file contains two structs instead:

  1. ContentView, which defines the user interface, and
  2. ContentView_Previews, which draws a preview of the user interface in the Canvas.

You should think of ContentView as being for the user, since it’s what defines what’s in the user interface. On the other hand, the result of ContentView_Previews is visible only within Xcode during the development process, and is solely for the benefit of you, the developer.

Stepping through the view code in detail

Since ContentView actually affects how the app works, we’ll look at it first:

struct ContentView: View {
  var body: some View {
    Text("Welcome to my first app!")
  }
}

Let’s examine it line by line. Here’s the first one:

struct ContentView: View {

The first part of this line says that ContentView is a struct. Like classes, structs are objects that can have properties and methods. For now, it’s easiest to just think of structs as being like classes, except that they’re value types instead of reference types.

Another key difference between structs and classes is that structs don’t support inheritance, but they adopt or conform to one or more protocols. You might remember from way back that the Delegates & Protocols chapter described a protocol as “simply a name for a group of methods.” It’s more accurate to say that a protocol is a name for a group of properties and methods. An object that conforms to a protocol gets that protocol’s properties and methods.

With that in mind, we can now interpret the second half of ContentView’s first line: ContentView conforms to the View protocol. This means that in addition to whatever properties and methods it may define, ContentView also has the properties and methods defined in the View protocol.

In class definitions, the : character in Swift means “inherits from”, “conforms to” or “is of this type”. In general, whenever you see the : character in Swift code, you should think of it as being shorthand for “is a/an”. When you see code like ContentView: View, you should think of it as saying “ContentView is a View.”

You can even apply this thinking to variable and constant declarations. When you see code like var count: Int, you should think of it as saying “The variable count is an Int.”

A protocol isn’t just a group of properties and methods. It’s a group of related properties and methods that taken together, fit some specific task or functionality. The View protocol is a group of properties and methods that define the behavior of a view, which is anything that is drawn onscreen. You’ll make use some of these properties and methods soon.

One of the most important properties in SwiftUI’s View protocol is the body property. It defines the content and structure of the view. In our ContentView struct, it’s declared in the second line:

var body: some View {

Most of this line should be familiar to you. It says that body is a property of ContentView, and that body is a variable. It also says that body’s type isn’t just View, but some View. The addition of the keyword some in front of View means “Something that conforms to the View protocol,” or if you prefer, “Some kind of View.”

From all the programming you’ve already done, you know that compilers hate ambiguity. “Something that conforms to the View protocol” is too ambiguous a statement for a compiler to accept. It needs to be told or be able to infer what type body is, and that’s what the code in the next line is for:

Text("Welcome to my first app!")

This line creates an instance of a Text object, which is a kind of View (or more technically, conforms to the View protocol). Text is initialized with a string parameter — “Welcome to my first app” — which is used to set the value that it initially displays.

Notice that the line Text("Welcome to my first app!") is surrounded by { and } brackets, and in Swift, these characters mark the start and end of a block of code. The block of code that follows some View is a closure, and its return value specifies what kind of View that some View is. In this case, the return value is a Text object, which is some kind of View.

SwiftUI takes advantage of the fact that you can skip the return statement in functions and closures that are single expressions. The return value is the value of that single expression. This works only for single-line functions and closures where that single line results in a value. It’s called implicit return.

You can confirm that the closure is implicitly returning a value by making the return explicit. Change the line in the closure to return Text("Welcome to my first app!"). Xcode won’t complain and the code will compile just fine.

Making the text bolder

The text needs some sprucing up. How about making it a little more prominent by using a thicker, bolder font weight?

➤ Edit the line in ContentView that instantiates the Text object to become the following:

Text("Welcome to my first app!").fontWeight(.black)

You’ll see a matching change in the Canvas (if you don’t, click the Canvas’ Resume button) and the Attributes Inspector:

Giving the text a heavier weight
Giving the text a heavier weight

fontWeight() is just one of many methods provided by the Text protocol, and you’ve probably guessed that it sets the font’s weight.

➤ Option-click on fontWeight in the code to find out more about this method.

Getting more information about the fontWeight() method
Getting more information about the fontWeight() method

As you can see, the fontWeight() method returns a Text object. This makes sense, because Text is a kind of View, and body’s type is some Text.

Changing the text’s color with method chaining

Let’s make the text stand out even more by changing its color.

➤ Change the line in ContentView that instantiates the Text object to the following:

Text("Welcome to my first app!").fontWeight(.black).foregroundColor(.green)

Once again, you should see this change reflected in the Canvas:

Making the text green
Making the text green

➤ Option-click on foregroundColor in the code to find out more about this method.

Getting more information about the foregroundColor() method
Getting more information about the foregroundColor() method

The foregroundColor() method, like the fontWeight() method, also returns a Text object.

You’ve just taken advantage of a feature called method chaining. That’s a computer science-y term for arranging methods in a chain so that any given method in the chain uses the result of the previous method in the chain.

Right now, the chain’s a little hard to read:

The code works, but it’s hard to read
The code works, but it’s hard to read

➤ Let’s make the chain a little easier to read by reformatting the line so that it looks like this:

Text("Welcome to my first app!")
  .fontWeight(.black)
  .foregroundColor(.green)

Now that the chain is easier to read, it’s easier to see how method chaining works:

  1. Text("Welcome to my first app!") creates a Text object.
  2. .fontWeight(.black) takes the Text object created by the previous method and creates a new Text object with a heavier font weight.
  3. .foregroundColor(.green) takes the Text object created by the previous method and creates a new Text object with green coloring.

Here’s the chain, illustrated:

Method chaining, illustrated
Method chaining, illustrated

Method chaining makes your code readable and concise, especially with the right formatting. In this case, it lets us create a complex Text object with a single line of code (it may be formatted so that it takes up several lines, but as far as the compiler’s concerned, it’s just one line). Without method chaining, the code to declare body would have to be written with additional variables and multiple lines, and would also need a return statement:

var body: some View {
  let initialText = Text("Welcome to my first app!")
  let blackText = initialText.fontWeight(.black)
  return blackText.foregroundColor(.green)
}

Even though the Canvas is giving you a preview of what the UI for the app looks like so far, you might want to run the app anyway, just to see it in action. Go ahead!

Adding a button

Right now, the app simply displays text and then just sits there. That simply won’t do: It’s time to add some interactivity and add the “Hit me!” button.

In SwiftUI, the object that represents a button has an obvious name: Button. Like Text, it’s a struct that conforms to the View protocol. Remember, a View object is something that’s drawn onscreen.

Rather than explain the parameters for Button’s initializer, let me show you the code for a button labeled “Hit me!” and which prints “Button pressed!” to the Console:

Button(action: { print("Button pressed!") }) {
  Text("Hit me!")
}

Let’s try putting this code into body, just below the line that instantiates the Text object:

➤ Add the Button initializer code above to the declaration for body so that the full declaration looks like this:

var body: some View {
  Text("Welcome to my first app!")
    .fontWeight(.black)
    .foregroundColor(.green)
  Button(action: { print("Button pressed!") } ) {
    Text("Hit me!")
  }
}

Note that Xcode starts yelling at you. See if you can figure out why this happened before reading on.

An error message and two warnings after adding a button
An error message and two warnings after adding a button

The reason is that body is declared as some View, which means that it’s some kind of View object, and the specific type of View is defined by the return value of the closure that follows some View. Here’s the code for that closure:

Text("Welcome to my first app!")
  .fontWeight(.black)
  .foregroundColor(.green)
Button(action: { print("Button pressed!") } ) {
  Text("Hit me!")
}

The closure is no longer a one-line function, which means that it no longer implicitly returns a value. You now need to include a return statement. There’s also the fact that there are now two views, and a function or closure can return only a single value.

The solution is to make use of a view that acts as a container for other views. One such type of view is the VStack, whose name is short for “vertical stack”. Let’s put the Text and Button views into a VStack view.

➤ Change the declaration for body to the following:

var body: some View {
  VStack {
    Text("Welcome to my first app!")
      .fontWeight(.black)
      .foregroundColor(.green)
    Button(action: { print("Button pressed!") } ) {
      Text("Hit me!")
    }
  }
}

By putting the Text and Button views into a VStack, the closure parameter of some View effectively becomes a single expression that returns a View object. As a result, body is assigned a View object, which it expects. The error message and warnings should disappear, and the Canvas will now display the user interface that we were expecting:

The Text and Button views inside a VStack
The Text and Button views inside a VStack

Let’s confirm that the button reacts to being pressed.

➤ Run the app. You should see “Button pressed!” appear in the Console:

The button is pressed
The button is pressed

Now that you’ve put Button’s initializer to use, let’s talk about its parameters. To create a button, you need to provide Button with the following:

  1. action: A closure whose code gets executed when the button is pressed.
  2. A closure that returns a view that defines the button’s contents.

In this case, we want the button to contain the text “Hit me!”. The Text object is how you display text in SwiftUI, so we make the content-defining closure for the button return the result of the initializer Text("Hit me!").

Now that we can put a button onscreen, let’s now try to make it display an alert when pressed. In order to do that, we need to take a little detour and look at a key SwiftUI concept: State.

State and SwiftUI

Looking at state in the real world

Rather than start with the computer science definition of state, let’s go with something that might be a little more familiar, the dashboard of a car.

The dashboard of a car
The dashboard of a car

The gauges and odometers are usually the most noticeable parts of a dashboard. They show the car’s current speed, fuel level, engine temperature and distance traveled, each of which is some kind of numerical property.

Dashboards also have warning lights, such as the “check engine” light, the low oil pressure warning light, the “it’s time to take the car to the shop for overpriced regular maintenance” light and the “someone’s not wearing their seat belt and will be very sorry if there’s an accident” light. Each of these lights is either on, indicating that there’s a problem that needs the driver’s attention, or off, and each is a boolean property.

The information on a car’s dashboard — speed, fuel level, whether on not someone in the car likes to live dangerously without a seat belt and so on — taken all together, is that car’s state.

The driver’s actions can change the car’s state, and the new state is immediately shown in the dashboard. For example, if the driver presses on the accelerator pedal, the car’s speed increases, which in turn causes the speedometer to display the car’s new speed. If the driver then presses on the brake pedal, the car slows down, and the speedometer automatically and immediately shows the new, slower speed.

Internal circumstances can also change the car’s state, and once again, this new state is shown in the dashboard instantly. As the car uses up fuel, the fuel gauge moves away from F and towards E. When the driver fills the tank, the fuel gauge immediately goes back to F.

Another example is the “maintenance” light. When a pre-determined amount of time passes or the car has traveled a pre-determined distance since it was last brought in for maintenance, the car’s state changes from not needing maintenance to needing it, and the “maintenance” light turns on. Once the car has been brought to the dealership or a mechanic for maintenance, the car’s state changes back to not needing maintenance and the light turns off.

There’s a term for every possible combination of every possible value of those things that make up the car’s state: the state space. With all the possible combinations of things that make up the car’s state — speed, fuel level, engine temperature, distance traveled, and so on — a car has a really large state space.

The one-button app’s state space

Unlike our car example, the one-button app you’re building has a much smaller state space. It only has two states:

  1. Welcome: Simply displaying the “Welcome to my first app!” screen that you’ve already seen. This is what the user should see when they haven’t pressed the Hit me! button.
  2. Alert: Displaying the alert. This is what the user should see when they press the button.

Its state space is small enough to draw in a state diagram:

State diagram for the one-button app
State diagram for the one-button app

The rectangles represent the app’s two states. The arrows are transitions, which are the ways to get from one state to another. Written beside each transition arrow is the reason for the change in state, formally known as a transition condition. The app has two transitions:

  1. From Welcome to Alert. This transition happens when the user presses Hit me!.
  2. From Alert to Welcome. This transition happens when the user dismisses the alert.

SwiftUI and state space

Coding a user interface in SwiftUI is similar to drawing a state diagram. Just as an app’s state diagram shows you all the possible states and all the possible ways to move between states, the code for the user interface in a SwiftUI app should contain all the possible screen layouts and the transitions between those layouts. Think of it as the state diagram for the user interface, in code form.

Let’s review the code for the user interface as it is right now:

struct ContentView : View {
  var body: some View {
    VStack {
      Text("Welcome to my first app!")
        .fontWeight(.black)
        .foregroundColor(.green)
      Button(action: {
        print("Button pressed!")
      }) {
        Text("Hit me!")
      }
    }
  }
}

Remember, the body property of ContentView defines the layout of the screen. At the moment, body contains a VStack, which arranges a Text object and a Button object in a vertical stack, in that order. This covers the Welcome state. We’re missing the layout for the Alert state.

We’ll define that layout soon, but there’s something we need to take care of first: the app’s state.

Representing state in the app with a variable

Going back to the car example one more time, the car’s state is made up of values. Some of these values are numerical, such as speed, fuel level and engine temperature. Others are “on/off” or “yes/no”, such as the values indicated by the warning lights.

A program’s state is also made up of values, which are stored in variables. Any program variable that stores a value that the program needs to do its job is part of its state.

Our one-button app has just two states, which can be represented by two values:

  1. A value representing the state where the app is not displaying the alert.
  2. A value representing the state where the app is displaying the alert.

By now, this value’s “on/off” or “yes/no” nature should suggest to you that we should use a Boolean value. Let’s create a property that holds a Boolean value to keep track of whether or not the alert should be visible. We’ll give it the name alertIsVisible. When the app starts, the alert should not be displayed, which means that alertIsVisible’s initial value should be false.

➤ Add the alertIsVisible property to ContentView so that its code looks like the following:

struct ContentView : View {
  @State var alertIsVisible: Bool = false

  var body: some View {
    VStack {
      Text("Welcome to my first app!")
        .fontWeight(.black)
        .foregroundColor(.green)
      Button(action: {
        print("Button pressed!")
      }) {
        Text("Hit me!")
      }
    }
  }
}

There’s something new here: An unfamiliar attribute, @State, at the start of the declaration for the alertIsVisible property. This declares that alertIsVisible is part of the ContentView view’s state, which does the following:

  1. It allows code within ContentView to change alertIsVisible’s value. Normally, code within a struct can’t change the values of that struct’s var properties and methods within a struct can’t change the values of that struct’s var properties without being marked as mutating. A property in a SwiftUI view object that’s been marked as a @State variable can have its value changed by any method within that view object.
  2. It tells SwiftUI to watch for changes to alertIsVisible’s value. This is because changes to that value mean changes to the state. When that happens, SwiftUI needs to take action.

When the app starts, ContentView is instantiated and alertIsVisible’s value is false. It will stay that way forever unless we add code to change its value to true. We want that to happen when the user presses Hit me!.

➤ Change the code for ContentView so that it reads as follows:

struct ContentView : View {
  @State var alertIsVisible: Bool = false

  var body: some View {
    VStack {
      Text("Welcome to my first app!")
        .fontWeight(.black)
        .foregroundColor(.green)
      Button(action: {
        print("Button pressed!")
        self.alertIsVisible = true
      }) {
        Text("Hit me!")
      }
    }
  }
}

With this addition, alertIsVisible is set to true when the user presses the button. We now need to define the layout for that state.

Defining the layout for the “Alert” state

It’s worth repeating: In SwiftUI, you define the layout for all possible states. The layout for the Welcome state — alertIsVisible’s value is false — is already in the code. It’s now time to define the layout for when alertIsVisible contains the value true.

Rather than tell you how it works, it’s simpler (and more fun!) to show you:

➤ Change the code for ContentView so that it reads as follows:

struct ContentView : View {
  @State var alertIsVisible: Bool = false

  var body: some View {
    VStack {
      Text("Welcome to my first app!")
        .fontWeight(.black)
        .foregroundColor(.green)
      Button(action: {
        print("Button pressed!")
        self.alertIsVisible = true
      }) {
        Text("Hit me!")
      }
      .alert(isPresented: $alertIsVisible) {
        Alert(title: Text("Hello there!"),
              message: Text("This is my first SwiftUI alert."),
              dismissButton: .default(Text("Awesome!")))
      }
    }
  }
}

Let’s take a closer look at the code you just added:

.alert(isPresented: $alertIsVisible) {
  Alert(title: Text("Hello there!"),
        message: Text("This is my first SwiftUI alert."),
        dismissButton: .default(Text("Awesome!")))
}

alert(isPresented:content:) is a method on Button that presents an alert to the user. It has two parameters:

  1. A binding, or two-way connection to a Boolean state property. In this case, that variable is alertIsVisible, and the two-way connection means that alertIsVisible’s value and the alert are tied together. Changing alertIsVisible to true causes the alert to appear, and the user dimissing the pop-up causes alertIsVisible’s value to be set to false. The $ in $alertIsVisible tells Swift that this is a two-way binding: changes to alertIsVisible affect the alert, and changes to the alert — affect alertIsVisble.
  2. A closure that returns the Alert object to be displayed.

Let’s see this new code in action.

➤ Press the Run button, and when the app starts up in the Simulator, press the Hit me! button.

You should see the following:

![bordered width=50%](images/44-Hello-SwiftUI/29_The alert.png “Your first SwiftUI alert”)

A quick look at the preview code

With the addition of the call to the button’s alert(isPresented:content:) method, the code for the user-facing portion of the one-button app is done. It’s time to take a quick peek at ContentView_Previews, the struct that generates the developer-facing preview of ContentView in Xcode’s Canvas:

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
  }
}

The first thing you should know is that it’s not necessary to have a preview for your app to work. You can delete ContentView_Previews and your app will still run. The preview is a convenience for you, the developer, to be able to see the resulting UI while you’re coding. Let’s test this, just to be sure.

➤ Delete ContentView_Previews. You should see the following:

The Canvas after you delete the preview code
The Canvas after you delete the preview code

➤ Run the app. It will still work, because the preview doesn’t affect the user experience.

➤ Press the Create Preview button in the Canvas. You’ll see this:

The newly-generated preview doesn’t quite match
The newly-generated preview doesn’t quite match

Xcode generates some new preview code for you, but because it really doesn’t know what kind of UI you’re building, it gives you a preview code for a dummy screen that says “Hello, World!”.

➤ Run the app. Notice that a preview that doesn’t match has no effect on the app’s UI. What you see is completely defined by ContentView and not ContentView_Previews

Now that you’ve made your first SwiftUI view, the preview code should seem familiar:

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    Text("Hello, World!")
  }
}

You can see that the previews property of ContentView_Previews is analogous to the body property of ContentView. The view returned by its closure parameter — the code in the { and } that follow some View — determines the structure and content of the preview.

Let’s make the preview properly reflect what the UI view presents by replacing the “Hello, World!” text view with an instance of the UI view:

➤ Update ContentView_Previews to the following:

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
  }
}

The Canvas should now show a preview of the UI that ContentView generates.

If you want to be 100% sure that you’re getting a proper preview, try changing the content of any of the Text views in ContentView. You should see those changes in the Canvas.

The anatomy of your SwiftUI app

Let’s finish this chapter by looking at what goes on behind the scenes of your first SwiftUI app.

The key objects in the app so far are:

  • ContentView: An object representing the app’s main screen.
  • body: A property of ContentView, which contains all the possible user interface elements on the main screen and defines all the possible ways how they can be laid out onscreen, for all circumstances. body contains the following objects:
    • A VStack view, which organizes other views into a vertical stack.
    • A Button view, which the user presses to initiate an action. It contains the following:
      • A Text view, which defines the text inside the button.
      • An action: method, which is called when the user presses the button.
      • An alert() method, which is bound to the alertIsVisible state property. It gets called whenever the value contained in alertIsVisible changes.

You’ve also been introduced to the idea of state, which you can think of as a snapshot of the app’s current circumstances. Right now, the app has two possible states, which are represented by the contents of ContentView’s alertIsVisible property, a Boolean:

  1. The Welcome state, where the user sees the main screen, with the “Welcome to my first app!” message. This is the app’s state when alertIsVisible is set to false.
  2. The Alert state, where the main screen is obscured by the alert. This is the app’s state when alertIsVisible is set to true.

In SwiftUI, an app works by having its objects and state affect and be affected by each other. An object can react to some external stimulus — which could come from the user, the operating system, another object, or a change in state — which then causes the object to change the app’s state. Changing the state can affect objects, which perform some action or change some data in response, which in turn can affect the state.

Way back in Chapter 2, you were presented with a diagram of the UIKit version of the one-button app. Here’s the diagram for the SwiftUI version you just built:

The anatomy of your SwiftUI app
The anatomy of your SwiftUI app

When the user presses the Hit me! button on the screen, its corresponding Button object executes the code in its action: method, which sets the contents of the alertIsVisible variable to true. alertIsVisible is a state property, which means that it can affect objects in the app.

The Button object also calls its alert() method, which has a two-way binding to alertIsVisible. This means that this method is called every time alertIsVisible changes. The alert() method also defines an Alert view that should appear if alertIsVisible is set to true, which is the case when the button is pressed.

With the alert now displayed onscreen, the app returns back to what it does most of the time: waiting for the next user interaction. Since the alert is modal — that’s user interface jargon for “completely blocking everything else on the screen until the user deals with it” — the only possible user interaction within the app is to dismiss the alert by pressing its button.

When the user presses the Awesome! button on the alert to dismiss it, the two-way connection between the visibility of the alert and alertIsVisible causes alertIsVisible to be set to false. The user is now back on the main screen, and the app goes back to waiting for the next user interaction.

You can find the project files for the app up to this point under 44 - Hello SwiftUI in the Source Code folder.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.