Chapter 1: Using the Basic SwiftUI Views and Controls
In this chapter, we'll learn about SwiftUI's innovative way of building user interfaces across all platforms. SwiftUI does not use UIKit concepts such as Auto Layout. It has a completely new layout system designed to make it easy to write applications that work across Apple platforms.
We'll also learn about views and controls, SwiftUI's visual building blocks for app interfaces. We will look at how to use text, images, buttons, navigation, and much more.
By the end of the chapter, you'll be able to combine various views and components to create crisp and beautiful user interfaces.
In this chapter, we will cover the following recipes:
- How to lay out components
- Dealing with text
- Using images
- Adding buttons and navigating with them
- Beyond buttons – how to use advanced pickers
- How to apply groups of styles using ViewModifiers
- Separating presentation from content with
ViewBuilder
- Simple graphics using SF Symbols
- The best of both worlds – integrating UIKit into SwiftUI
- Adding SwiftUI to an existing app
- More views and controls (iOS 14+)
Technical requirements
The code in this chapter is based on Xcode 12 with iOS 13 as a minimum iOS target.
You can find the code in the book's GitHub repository at https://github.com/PacktPublishing/SwiftUI-Cookbook/tree/master/Chapter01%20-%20Using%20the%20basic%20SwiftUI%20Views%20and%20Controls
How to lay out components
SwiftUI uses three basic layout components – VStack
, HStack
, and ZStack
. VStack
is a view that arranges its children in a vertical line, HStack
arranges its children in a horizontal line, and ZStack
arranges its children by aligning them with the vertical and horizontal axes.
We will also take a look at how spacers and dividers can be used for layout.
Getting ready
Let's start by creating a new SwiftUI project with Xcode, calling it TheStacks
. Use the following steps:
- Start the Xcode program.
- Click on Create a new Xcode project from the left pane.
- Leave the default selected application, Single View App. Click Next.
- In the project options menu, make sure SwiftUI is selected instead of storyboard.
- Enter the product name,
TheStacks
. - Select the folder location to store the project and click Create.
How to do it…
- Replace
Text
inContentView
with a vertical stack containing three elements:struct ContentView: View { var body: some View { VStack{ Text("VStack Item 1") Text("VStack Item 2") Text("VStack Item 3") }.background(Color.blue) } }
This renders like the following in the canvas preview:
Figure 1.1 – VStack with three items
- Add a
Spacer
and aDivider
between items 2 and 3:Spacer() Divider()
This renders the following preview:
Figure 1.2 – VStack, HStack, and ZStack
- Add the following
HStack
andZStack
to the bottom of theVStack
:HStack{ Text("Item 1") Text("HStack Item 2") Divider().background(Color.black) Spacer() Text("HStack Item 3") }.background(Color.red) ZStack{ Text("ZStack Item 1") .padding() .background(Color.green) .opacity(0.8) Spacer() Text("ZStack Item 2") .padding() .background(Color.green) .offset(x: 80, y: -400) }

Figure 1.3 – VStack, HStack, and ZStack
That concludes our recipe on using stacks. Going forward, we'll make extensive use of VStacks and HStacks to position elements in our views.
How it works…
The SwiftUI stack layout process consists of three steps:
- The parent proposes the size for the child.
- The child chooses its size.
- The parent places the child in the parent's coordinate space.
Following the preceding steps, the initial text view on a new project gets placed at the center of the screen. Adding a divider causes the stack view to extend and use the full width of the screen.
Stacks also define a layout process for items added to the stack:
- The stack figures out its internal spacing and subtracts it from the size proposed by its parent view.
- The stack then divides the remaining space into equal parts, processes the size of its least flexible view, and divides the remaining unclaimed space by the unallocated space, and then repeats step 2.
- The stack then aligns its content and chooses its own size to exactly enclose its children.
A VStack
adds items in a vertical way, with new views added from top to bottom. Adding the Spacer()
forces the view to use the maximum amount of vertical space. Adding a Divider()
draws a line that uses the maximum horizontal space allowed. By default, the divider line does not have a color. To set the divider color, we add the .background(Color.black)
modifier. Items such as background(Color.black)
, .padding()
, and .offests(…)
are called ViewModifiers
, or just modifiers. Modifiers can be applied to views or other modifiers, producing a different version of the original value.
The HStack
also works in a similar manner. However, since items are added to an HStack
in a horizontal manner, the Spacer()
causes the HStack
to fill the available horizontal space, while the Divider()
draws a vertical line to separate items in the HStack
. The Divider()
thus causes the HStack
to fill the maximum vertical space available.
The ZStack
overlays its content on top of existing items. It uses the .offsets()
modifier to position items along the X and Y axes.
Dealing with text
The SwiftUI text struct is a view that displays one or more lines of read-only text. Text structs come with a number of standard modifiers to format text. In this section, we will create a project that applies modifiers to 10 different texts.
We will also implement TextField
, which is used to display an editable text interface. We will also look at SecureField
. SecureField
is almost identical to TextField
, but masks its content for privacy.
Getting ready
Create a new SwiftUI project named FormattedText
.
How to do it…
- Enclose the text in the
body
view with aVStack
:struct ContentView: View { var body: some View { VStack{ Text("Hello World") } } }
- Add the
.fontWeight(.medium)
modifier to the text and observe the weight change in the canvas preview:Text("Hello World") .fontWeight(.medium)
- Add two state variables to
ContentView
,password
andsomeText
. These variables will store the user input placed inSecureField
andTextField
, respectively:struct ContentView: View { @State var password = "" @State var someText = "" var body: someView { … }
- Add
SecureField
and aText
view to display the value entered inTextField
:SecureField("Enter a password", text: $password) .padding() Text("password entered: \(password)") .italic()
- Add
TextField
and aText
view to display the value entered inTextField
:TextField("Enter some text", text: $someText) .padding() Text("\(someText)") .font(.largeTitle) .underline()
- Now, let's add some other texts and modifiers to the list:
Text("\(someText)") .font(.largeTitle) .underline() Text("Changing text color and make it bold") .foregroundColor(Color.blue) .bold() Text("Use kerning to change space between lines of text") .kerning(7) Text("Changing baseline offset") .baselineOffset(100) Text("Strikethrough") .strikethrough() Text("This is a multiline text implemented in swiftUI. The trailing modifier was added to the text. This text also implements multiple modifiers") .background(Color.yellow) .multilineTextAlignment(.trailing) .lineSpacing(10)
The resulting preview should look as follows:
Figure 1.4 – FormattedText preview
- Run the code in a simulator or click the play button next to the preview. Enter some text in the
SecureField
andTextField
.
Text entered in the SecureField
will be masked, while text in the TextField
will be in clear text.
How it works…
Text views have a number of modifiers for font, spacing, and other formatting requirements. When in doubt, type .
after the text view and choose from the list of possible options that will appear. This is shown in the following example:

Figure 1.5 – Using Xcode autocomplete to view formatting options
Unlike regular text views, TextFields
and SecureFields
require state variables to store the value entered by the user. State variables are declared using the keyword @State
. Values entered by the user are stored using the process of binding, where the state variable is bound to the SecureField
or TextField
input parameter. The $ symbol is used to bind a state variable to the field. Using the $ symbol ensures that the state variable's value is changed to correspond to the value entered by the user, as shown in the following example:
TextField("Enter some text", text: $someText)
Binding also notifies other views of state changes and causes the views to be redrawn on state change.
The content of bound state variables is displayed without using the $ symbol because no binding is required when displaying content, as shown in the following code:
Text("\(someText)")
There's more…
Try adding an eleventh element to the VStack
. You will get an error, because SwiftUI views can hold a maximum of 10 elements. To add an eleventh element, you would have to enclose the first 10 elements in a Group
view. Groups are required because the SwiftUI view building system has various code designed to allow the addition of views 1 through 10 but not beyond. A Group
view can be used as shown in the following code:
Group { Text("Item 1") … Text("Item 10") } Text("Item 11")
Items in a Group
are considered as one view. The preceding code thus allows us to display 11 items as two views, the Group
view, and the Text
view at the bottom.
See also
Apple documentation regarding SwiftUI Text view: https://developer.apple.com/documentation/swiftui/text
Using images
In this recipe, we will learn how to add an image to a view, use an already existing UIImage, put an image in a frame, and use modifiers to present beautiful images. The images in this section were obtained from https://unsplash.com/. Special thanks to jf-brou, Kieran White, and Camilo Fierro.
Getting ready
Create a new SwiftUI project named ImageApp
.
How to do it…
- Replace the initial
Text
view with aVstack
. - Download the project images from the GitHub link at https://github.com/PacktPublishing/SwiftUI-Cookbook/tree/master/Chapter01%20-%20Using%20the%20basic%20SwiftUI%20Views%20and%20Controls/03%20-%20Using%20Images
- Drag and drop the downloaded images for this recipe into the project's
Assets.xcassets
folder, as shown in the following screenshot:Figure 1.6 – Assets.xcassets folder in Xcode
- Add an
Image
view toVStack
:Image("dogs1")
Observe the result in the canvas preview.
- Add a
.resizable()
modifier to the image and allow SwiftUI to adjust the image such that it fits the screen space available:Image("dogs1") .resizable()
- The
.resizable()
modifier causes the full image to fit on the screen, but the proportions are distorted. That can be fixed by adding the.aspectRatio(contentMode: .fit)
modifier:Image("dogs1") .resizable() .aspectRatio(contentMode: .fit)
- Add the
dog-and-nature
image toVStack
:Image("dog-and-nature") .resizable() .aspectRatio(contentMode: .fit) .frame(width:300, height:200) .clipShape(Circle()) .overlay(Circle().stroke(Color.blue, lineWidth: 6)) .shadow(radius: 10)
- To use a
UIImage
class as input for images, create a function,getImageFromUIImage(image: String)
, that accepts an image name and returns aUIImage
:func getImageFromUIImage(image:String) -> UIImage { guard let img = UIImage(named: image) else { fatalError("Unable to load image") } return img }
- Use
getImageFromUIImage(image: String)
to display aUIImage
within theVStack
. The resulting code should look as follows:struct ContentView: View { var body: some View { VStack{ Image("dogs1") .resizable() .aspectRatio(contentMode: .fit) Image("dog-and-nature") .resizable() .aspectRatio(contentMode: .fit) .frame(width:300, height:200) .clipShape(Circle()) .overlay(Circle().stroke(Color.blue, lineWidth: 6)) .shadow(radius: 10) Image(uiImage: getImageFromUIImage(image: "dog2")) .resizable() .frame(width: 200, height: 200) .aspectRatio(contentMode: .fit) } } } func getImageFromUIImage(image:String) -> UIImage { guard let img = UIImage(named: image) else { fatalError("Unable to load image") } return img }

Figure 1.7 – ImageApp preview
How it works…
Adding the Image
view to SwiftUI displays the image in its original proportions. The image might be too small or too big for the device's display. For example, without any modifiers, the dog-and-nature
image fills up the full iPhone 11 Pro Max screen:

Figure 1.8 – Dog-and-nature image without the resizable modifier
To allow an image to shrink or enlarge to fit the device screen size, add the .resizable()
modifier to the image.
Adding the .resizable()
modifier causes the image to fit within its view, but it may be distorted due to changes in proportion:

Figure 1.9 – Image with resizable modifier
To address the issue, add the .aspectRatio(contentMode: .fit)
modifier to the image:

Figure 1.10 – Image with AspectRatio set
To specify the width and height of an image, add the .frame(width, height)
modifier to the view and set the width
and height
:
.frame(width: 200, height: 200)
Images can be clipped to specific shapes. The .clipShape(Circle())
modifier changes the image shape to a circle:

Figure 1.11 – Image with the clipShape(Circle()) modifier
The .overlay(Circle().stroke(Color.blue, lineWidth: 6))
and .shadow(radius: 10)
modifiers were used to draw a blue line around the image circle and add a shadow to the circle:

Figure 1.12 – Stroke and shadow applied to image
Important note
The order in which the modifiers are added matters. Adding the .frame()
modifier before the .resizable ()
modifier may lead to different results, or cause an error.
See also
Apple documentation regarding SwiftUI images: https://developer.apple.com/documentation/swiftui/image
Adding buttons and navigating with them
In this recipe, we will learn how to use various buttons available in SwiftUI. We will use a Button
view to trigger the change of a count when clicked, implement a navigationView
to move between various SwiftUI views, and an EditButton
to remove items from a list. We will also briefly discuss the MenuButton
and PasteButton
, only available in macOS.
Getting ready
Create a new SwiftUI project named ButtonsApp
.
How to do it…
- Add a new SwiftUI
View
file calledButtonView
to the project: File | New | File, or press the shortcut keys: ⌘ + N. - Select SwiftUI View from the User Interface templates.
- In the Save As field of the pop-up menu, enter the filename
ButtonView
. - Repeat step 1 and enter the filename
EditButtonView
. - Repeat step 1 and enter the filename
PasteButtonView
. - Repeat step 1 and enter the filename
MenuButtonView
. - Open the
ContentView.swift
file and create aNavigationView
to navigate between the SwiftUI views we added to the project:NavigationView { VStack{ NavigationLink(destination: ButtonView()){ Text("Buttons") } NavigationLink(destination: EditButtonView()) { Text("EditButtons") .padding() } NavigationLink(destination: MenuButtonView()) { Text("MenuButtons") .padding() } NavigationLink(destination: PasteButtonView()) { Text("PasteButtons") .padding() } NavigationLink(destination: Text("Very long text that should not be displayed in a single line because it is not good design") .padding() .navigationBarTitle(Text("Detail")) ) { Text("details about text") .padding() } }.navigationBarTitle(Text("Main View"), displayMode: .inline)
Upon completion, the
ContentView
preview should look like the following screenshot:Figure 1.13 – ButtonsApp ContentView
- Open the
EditButtonView.swift
file in the project navigator and add the following code that implements anEditButton
:@State private var animals = ["Cats", "Dogs", "Goats"] var body: some View { NavigationView{ List{ ForEach(animals, id: \.self){ animal in Text(animal) }.onDelete(perform: removeAnimal) } .navigationBarItems(trailing:EditButton()) .navigationBarTitle(Text("EditButtonView"), displayMode: .inline) } } func removeAnimal(at offsets: IndexSet){ animals.remove(atOffsets: offsets) }
- Open the
MenuButtonView.swift
file and add the code forMenuButtons
:var body: some View { Text("MenuButtons are currently available on MacOS currently") .padding() .navigationBarTitle("MenuButtons", displayMode: .inline) /* MenuButton("country +") { Button("USA") { print("Selected USA") } .background(Color.accentColor) Button("India") { print("Selected India") } } */ }
- Open the
PasteButtonView.swift
file and implement the text regardingPasteButtons
:@State var text = String() var body: some View { VStack{ Text("PasteButton controls how you paste in macOS but is not available in iOS. For more information, check the \"See also\" section of this recipe") .padding() }.navigationBarTitle("PasteButton", displayMode: .inline) }
Go back to ContentView
and run the code in the canvas preview or simulator and play around with it to see what the results look like.
How it works…
A NavigationLink
has to be placed in a NavigationView
prior to being used. It takes two parameters – destination
and label
. The destination
parameter represents the view that would be displayed when the label is clicked, while the label
parameter represents the text to be displayed within NavigationLink
.
NavigationLin
k buttons can be used to move from one SwiftUI view file to another, for example, moving from ContentView
to EditButtonView
. It can also be used to display text details without creating a SwiftUI view file, such as in the last NavigationLink
, where a click just presents a long piece of text with more information. This is made possible because the text struct conforms to the view protocol
The .navigationBarTitle(Text("Main View"), displayMode: .inline)
modifier adds a title to ContentViewPage
. The first argument passed to the modifier represents the title to be displayed, and the displayMode
argument controls the style for displaying the navigation bar.
The .navigationBarTitle()
modifier was also added to EditButtonView
and other views. Since these views do not contain NavigationView
structs, the titles would not be displayed when viewing the page directly from the preview, but would show up when running the code and navigating from ContentView.swift
to the view provided in NavigationLink
.
The EditButton
view is used in conjunction with List
views to make lists editable. We will go over Lists and Scroll Views in Chapter 2, Going Beyond the Single Component with List and Scroll Views, but EditButtonView
provides a peek into how to create an editable list.
As of April 2020, MenuButtons
and PasteButtons
are only available on macOS. This recipe provides some sample code for menu buttons. Refer to the See also section of this recipe for code on how the PasteButton
is implemented.
See also
The code for implementing PasteButtons
can be found here: https://gist.github.com/sturdysturge/79c73600cfb683663c1d70f5c0778020#file-swiftuidocumentationpastebutton-swift
More information regarding NavigationLink
buttons can be found here: https://developer.apple.com/documentation/swiftui/navigationlink
Beyond buttons – how to use advanced pickers
In this recipe, we will learn how to implement the pickers, namely, Picker
, Toggle
, Slider
, Stepper
, and DatePickers
. Pickers are typically used to prompt the user to select from a set of mutually exclusive values; Toggles are used to switch between on/off states; and Sliders are used to select a value from a bounded linear range of values. Like Sliders, Steppers also provide the user interface for selecting from a range of values. However, Steppers use a + and – sign to allow the users to increment the desired value by a certain amount. Finally, DatePickers are used to select dates.
Getting ready
Create a new SwiftUI project named PickersApp
.
How to do it…
- In
ContentView.swift
, create@State
variables to hold the values selected fromPickers
and other controls. Place the variables between theContentView
struct and the body:@State var choice = 0 @State var showText = false @State var transitModes = ["Bike", "Car", "Bus"] @State var sliderVal: Float = 0 @State var stepVal = 0 @State var gameTime = Date()
- Create a
Form
view within the body view of theContentView
struct. Add aSection
view and aPicker
to the form:Form{ Section{ Picker(selection: $choice, label:Text("Transit Mode")){ ForEach( 0 ..< transitModes.count) { index in Text("\(self.transitModes[index])") } }.pickerStyle(SegmentedPickerStyle()) Text("Current choice: \ (transitModes[choice])") } }
- Now, add a
Section
view and aToggle
view:Section{ Toggle(isOn: $showText){ Text("Show Text") } if showText { Text("The Text toggle is on") } }
- Add a
Section
view and aSlider
view:Section{ Slider(value: $sliderVal, in: 0...10, step: 0.001) Text("Slider current value \(sliderVal, specifier: "%.1f")") }
- Add a
Section
view and aStepper
view:Section { Stepper("Stepper", value: $stepVal, in: 0...5) Text("Stepper current value \(stepVal)") }
- Add a
Section
view and aDatePicker
view:Section { DatePicker("Please select a date", selection: $gameTime) }
- Add a
Section
view and a slightly modifiedDatePicker
view that only accepts future dates:Section { DatePicker("Please select a date", selection: $gameTime, in: Date()...) }
The result should be a beautiful form, as seen in the following diagram:

Figure 1.14 – SwiftUI form with pickers
How it works…
Form
views group controls used for data entry and Section
creates hierarchical view content. Enclosing Section
views, our Form
creates the gray area between each picker.
To use SwiftUI pickers, we first need to declare @State
variables to store the values from the pickers.
Pickers are used for selecting from a set of mutually exclusive values. In the following example, Picker
is used to select a transit mode from an array of strings called TransitModes
:
Picker(selection: $choice, label:Text("Transit Mode")){ ForEach( 0 ..< transitModes.count) { index in Text("\(self.transitModes[index])") } }.pickerStyle(SegmentedPickerStyle())
As shown in the example, a Picker
view takes two parameters, a state variable that holds the value selected, and a label. The state variable should be of the same type as the range of values to select from. In this case, the ForEach
loop iterates through the transitModes
array indices. The value selected would be an Int
within the range of transit mode indices. The transit mode located in the selected index can then be displayed using Text("\(self.transitModes[index])")
.
Toggles
are controls that switch between on and off states. The state variable for holding the toggle selection should be of the Bool
type. The section with the Toggle
view also contains some text. The current value of the toggle @State
property stores the current value of the Toggle and can be used to show or hide the view as necessary.
Creating a Slider
requires three arguments:
- Value: The
@State
variable to bind the user input to - In: The range of the
Slider
- Step: By how much the
Slider
should change when the user moves it
In the sample code, our slider moves can hold values between 0 and 10, with a step of 0.001.
Steppers
take three arguments too, the first being a string representing the label of the Stepper
. The two other arguments, value
and in
, hold the @State
variable that binds the user input and the range of the Stepper
, respectively.
Two applications of DatePickers
are demonstrated in this recipe. The first from the top shows one whose first argument is the label of DatePicker
, and the second argument, selection, holds the state variable that binds the user input. Such a DatePicker
would allow the user to pick any date. In the second version of DatePicker
, the last contains a third parameter, in
, representing the date range. The date range here only allows future dates to be selected.
Important note
The @State
variables should be of the same type as the data to be stored. For example, the gameTime
state variable is of the Date
type.
Picker styles change based on its ancestor. The default appearance of a picker may be different when placed within a form or list instead of a VStack
or some other view. Styles can be overridden using the .pickerStyle(..)
modifier.
How to apply groups of styles using ViewModifiers
SwiftUI comes with built-in modifiers such as background()
and fontWeight()
, among others. SwiftUI also gives programmers the ability to create their own custom modifiers to do something specific. Custom modifiers allow the programmer to combine multiple existing modifiers into a single modifier.
In this section, we will create a custom modifier that adds rounded corners and a background to a Text
view.
Getting ready
Create a new SwiftUI project named ViewModifiersApp
.
How to do it…
- Change the
Text
inContentView
to"Perfect"
:Text("Perfect")
- In the
ContentView.swift
file, create a struct that conforms to theViewModifier
protocol, accepts parameter of theColor
type, and applies styles to the body:struct BackgroundStyle: ViewModifier { var bgColor: Color func body(content: Content) -> some View{ content .frame(width:UIScreen.main.bounds.width * 0.3) .foregroundColor(Color.black) .padding() .background(bgColor) .cornerRadius(CGFloat(20)) } }
- Add the custom style to the text using the
modifier()
modifier:Text("Perfect").modifier(BackgroundStyle(bgColor: .blue))
- To apply styles without using the
modifier()
modifier, create an extension to theView
protocol:extension View { func backgroundStyle(color: Color) -> some View{ self.modifier(BackgroundStyle(bgColor: color)) } }
- Remove the modifier on the
Text
view and add your custom style using thebackgroundStyle()
modifier you just created:Text("Perfect") .backgroundStyle(color: Color.red)

Figure 1.15 – Custom View modifier
This concludes the section on View modifiers. View modifiers promote clean coding and reduce repetition.
How it works…
A ViewModifier
modifies a view, thereby producing a different version of the original value. To apply ViewModifier
, we first create a view that conforms to the ViewModifier
protocol. In this example, we created the BackgroundStyle
modifier.
The bgColor
variable in BackgroundStyle
means it would accept a color as a parameter and use it to change the style of the view.
The body function within the view gets the current body of the caller. Styles applied to the content parameter will be applied to the caller's view.
To make custom modifiers easier to use, we create an extension of View
and add a function that calls our custom modifier. The backgroundStyle(color: Color)
function in our View
extension allows us to apply the custom modifier to the view using .backgroundStyle(color:Color)
instead of .modifier(BackgroundStyle(bgColor: Color))
.
See also
Apple documentation on view modifiers: https://developer.apple.com/documentation/swiftui/viewmodifier
Separating presentation from content with ViewBuilder
ViewBuilder
is a custom parameter attribute that constructs views from closures. ViewBuilder
can be used to create custom views that can be used across the application with minimal or no code duplication. We will create a SwiftUI view, BlueCircle.swift
, for declaring the ViewBuilder
and implement the custom ViewBuilder
. The ContentView.swift
file will be used to implement the custom view.
Getting ready
Create a new SwiftUI project named ViewBuildersApp
.
How to do it…
- Open the ViewBuildersApp and add a new SwiftUI view to the project from the menu: File | New | File.
- Select the SwiftUI view from the menu.
- Click Next.
- Name the file
BlueCircle
and click Create. - Delete the
BlueCircle_Previews
: struct from the file (delete all five lines of code). - Add
BlueCircle ViewModifier
to the file:struct BlueCircle<Content: View>: View { let content: Content init(@ViewBuilder content: () -> Content) { self.content = content() } var body: some View { HStack { content Spacer() Circle() .fill(Color.blue) .frame(width:20, height:30) }.padding() } }
- Open the
ContentView.swift
file and implementBlueCircle ViewModifier
:VStack { BlueCircle { Text("some text here") Rectangle() .fill(Color.red) .frame(width: 40, height: 40) } BlueCircle { Text("Another example") } }
The result should look like the following screenshot:

Figure 1.16 – ViewModifiers preview
How it works…
ViewBuilder
structs create view templates that can be used without the need to duplicate code.
ViewBuilder
is a struct that implements the View
protocol and therefore should have a body variable within it.
Once created and initialized, the UI logic added within ViewBuilder
would be displayed each time the custom view is used. The location of the Content
constant determines where items added to the custom view would be displayed.
See also
Apple documentation on ViewBuilder
: https://developer.apple.com/documentation/swiftui/viewbuilder
Simple graphics using SF Symbols
The San Francisco Symbols (also known as SF Symbols) provide a set of over 1,500 consistent and highly configurable symbols.
To browse through the list or look at symbol names, you can download the SF Symbols app from https://developer.apple.com/design/downloads/SF-Symbols.dmg.
In this recipe, we will create a project that uses various SF Symbols and applies different styles to them.
Getting ready
Create a new SwiftUI project named SFSybmolsApp
.
How to do it…
Using SF Symbols is very similar to using images.
- Open the
ContentView.swift
file and delete the defaultText
view. - Add a
VStack
and anHStack
to theContentView
body and implement various SF symbols:VStack { HStack{ Image(systemName: "c.circle.fill") Image(systemName: "o.circle.fill") Image(systemName: "o.circle.fill") Image(systemName: "k.circle.fill") Image(systemName: "b.circle.fill") Image(systemName: "o.circle.fill") Image(systemName: "o.circle.fill") Image(systemName: "k.circle.fill") }.foregroundColor(.blue) .font(.title) .padding() HStack{ Image(systemName: "clock") .foregroundColor(Color.purple) .font(.largeTitle) Image(systemName: "wifi") .foregroundColor(Color.red) .font(.largeTitle) } }
The resulting preview should appear as shown in the following diagram:

Figure 1.17 – SF symbols in action
How it works…
SF Symbols are displayed using the Image
view and a systemName
parameter. Most modifiers used on Image
and Text
views can then be applied to SF Symbols. In this example, fonts, frame, and background modifiers were applied to the images.
See also
Apple documentation on SF Symbols:
https://developer.apple.com/design/human-interface-guidelines/sf-symbols/overview/
The best of both worlds – integrating UIKit into SwiftUI
SwiftUI was announced at WWDC 2019 and is only available on iOS 13 devices or later. Due to its relative newness, SwiftUI lacks broad API coverage compared to UIKit. For example, as of April 2020, UICollectionViews
, UITextView
, UIKit APIs for displaying grids, and multiline texts are not available in SwiftUI. There is therefore a need to implement certain UIKit APIs in SwiftUI.
This recipe goes over the required process for integrating UIKit into SwiftUI. We will create a project that wraps instances of UIActivityIndicatorView
to display an indicator in SwiftUI.
Getting ready
Open Xcode and create a SwiftUI project named UIKitToSwiftUI
.
How to do it…
UIKit views are displayed in SwiftUI by using the representable protocol. Follow these steps to implement UIActivityIndicatorView
in SwiftUI.
- Within the Xcode menu, click File | New | File and select SwiftUI view. Name the view
ActivityIndicator
. - Import
UIKit
intoActivityIndicator
. - Modify the code to make
ActivityIndicator
use the representable protocol:struct ActivityIndicator: UIViewRepresentable { var animating: Bool func makeUIView(context: Context) -> UIActivityIndicatorView { return UIActivityIndicatorView() } func updateUIView(_ activityIndicator: UIActivityIndicatorView, context: Context) { if animating { activityIndicator.startAnimating() } else { activityIndicator.stopAnimating() } } }
- Open the
ContentView.swift
file and add the following code to applyActivityIndicator
. Also add a toggle control to turn the indicator on or off.struct ContentView: View { @State var animate = true var body: some View { VStack{ ActivityIndicator(animating: animate) HStack{ Toggle(isOn: $animate){ Text("Toggle Activity") } } } } }
The result should look as follows:

Figure 1.18 – UIKit ActivityIndicator in SwiftUI
How it works…
UIKit views can be implemented in SwiftUI by using the representable protocol to wrap the UIKit views. In this recipe, we implement a UIActivityIndicatorView
by wrapping it with UIViewRepresentable
.
To conform to the UIViewRepresentable
protocol, we added a makeUIView
and updateUIView
function to our ActivityIndicator
struct.
The makeUIView()
function creates and prepares the view. In this case, it just returns the UIActivityIndicator
view.
The updateUIView
method updates the UIView when the animation state changes. The value of the animating variable in the struct is used to start or stop animating UIActivityIndicator
.
Important note
If working on an iOS 14+ app, use ProgressView
provided out of the box.
See also
Check out the recipe, More views and controls, at the end of this chapter for more information on ProgressView
.
Adding SwiftUI to an existing app
In this recipe, we will learn how to navigate from a UIKit view to a SwiftUI view while passing a text as argument from our UIKit storyboard to our SwiftUI view.
A storyboard is a visual representation of the user interface in UIKit. The Main.storyboard
file is to UIKit as the ContentView.swift
file is to SwiftUI. They both are the default home views created when you start a new project.
The recipe begins with a UIKit project that contains a button.
Getting ready
To prepare for this recipe, perform the following steps:
- Clone or download the GitHub project at https://github.com/PacktPublishing/SwiftUI-Cookbook.
- Open the
StartingPoint
folder located atSwiftUI-Cookbook/Chapter01 - Using the basic SwiftUI Views and Controls/10 - Adding SwiftUI to UIKit
and double-click on theAddSwiftUIToUIKitApp.xcodeproj
file. It opens up the UIKit project in Xcode.
How to do it…
We will add a NavigationController
to the UIKit ViewController
that allows the app to navigate to the SwiftUI view when the button is clicked:
- Open up the
Main.storyboard
file in Xcode by clicking on it. The display should look as follows:Figure 1.19 – UIKit ViewController
- Click anywhere in
ViewController
to select it. - In the Xcode menu, click Editor | Embed in | Navigation Controller.
- Add a new
ViewController
to the project. - Click on the + button, five buttons to the left of the top-right corner of Xcode.
- In the pop-up menu that appears, type
hosting
and select the Hosting View Controller:Figure 1.20 – Creating a UIKit ViewController
- Hold down the Ctrl key, and then click and drag from the button in
ViewController
to the new hostingViewController
recently created. - In the pop-up menu, select the Show action segue.
- Click the Adjust Editor Options button:
Figure 1.21 – Adjust Editor Options button
- Click Assistant. This splits the view into two panes, as shown here:
Figure 1.22 View when Assistant is opened
- To create a segue action, hold the Ctrl key, and then click and drag from the segue button to the space after the
viewDidLoad()
function inViewController.swift
. - In the pop-up menu, name the segue
goToSwiftUI
and click connect. The following code will be added to theViewController.swift
file:@IBSegueAction func goToSwiftUI(_ coder: NSCoder) -> UIViewController? { return <#UIHostingController(coder: coder, rootView: ...)#> }
- Add a statement to import SwiftUI at the top of the
ViewController
page, next toImport UIKit
:Import SwiftUI
- Within the
goToSwiftUI
segue action, create the string that will be passed to the SwiftUI view. Also create arootView
variable that indicates the SwiftUI view you would like to navigate to. Finally, return theviewController
used to display the SwiftUI view. The resulting code should be as follows:@IBSegueAction func goToSwiftUI(_ coder: NSCoder) -> UIViewController? { let greetings = "Hello From UIKit" let rootView = Greetings(randomText: greetings) return UIHostingController(coder: coder, rootView: rootView) }
- Create the Greetings SwiftUI view.
- Within the Xcode menu, click File | New | File and select SwiftUI View.
- Name the View
Greetings.swift
. - Add the code to display some text:
struct Greetings: View { var randomText = "" var body: some View { Text(randomText) } }
Run the resulting program in a simulator or device.
How it works…
To host SwiftUI views in an existing app, you need to wrap the SwiftUI hierarchy in a ViewController
or InterfaceController
.
We started by performing core UIKit concepts, such as adding a NavigationView
controller to the storyboard. We then proceeded to add HostingController
as a placeholder for our SwiftUI view.
Lastly, an IBSegueAction
function was created to present the SwiftUI view upon clicking a button.
More views and controls (iOS 14+)
Let's now take a look at some new SwiftUI views and controls introduced in iOS 14. We will look at the ProgressView
, Label
, ColorPicker
, Link
, TextEditor
, and Menu
views. We use ProgressViews to show the degree of completion of a task. There are two types of ProgressViews; indeterminate progress views show a spinning circle till a task is completed, and determinate progress views show a bar that gets filled up to show the progress of a task.
Labels provide an easy way to display a label and icon. ColorPickers expand when clicked and present the user with a wide range of colors to select from. TextEditor
provides a multiline interface for the user to input text.
Finally, Menus present a list of items the user can chose from. Each item should perform a specific action.
Getting ready
Create a new SwiftUI app called MoreViewsAndControls
.
How to do it…
Let's now implement all the new views and controls in the ContentView.swift
file. To make the resulting app elegant, we will implement each item in a separate Section
within a List
. Let's proceed as follows:
- Within the
ContentView
struct, just above thebody
variable, declare some state variables that we'll use later:@State private var progress = 0.5 @State private var color = Color.red @State private var secondColor = Color.yellow @State private var someText = "Initial value"
- Within the
body
view, let's add aList
view and aSection
view containing aProgressView
:List{ Section(header: Text("ProgressViews")) { ProgressView("Indeterminate progress view") ProgressView("Downloading",value: progress, total:2) Button("More"){ if(progress < 2){ progress += 0.5 } } } }
- Now, let's add another section that implements two Labels in two ways:
Section(header: Text("Labels")) { Label("Slow ", systemImage: "tortoise. fill") Label{ Text ("Fast") .font(.title) } icon: { Circle() .fill(Color.orange) .frame(width: 40, height: 20, alignment: .center) .overlay(Text("F")) } }
- Add the
Link
section below theColorPicker
section:Section(header: Text("Link")) { Link("Packt Publishing", destination: URL(string: "https://www.packtpub.com/")!) }
- Next, add a
TextEditor
:Section(header: Text("TextEditor")) { TextEditor(text: $someText) Text("current editor text:\n\(someText)") }
- Next, add a
Menu
:Section(header: Text("Menu")) { Menu("Actions") { Button("Set TextEditor text to 'magic'"){ someText = "magic" } Button("Turn first picker green") { color = Color.green } } }
- Finally, let's improve the style by adding a
.listStyle()
modifier to ourList
:List{ … }.listStyle(GroupedListStyle())
Here is what the resulting app should look like:

Figure 1.23 – MoreViewsAndControls app
Nice work! Run the app in Xcode preview and take some time to interact with the components to get a better appreciation for how they work.
How it works…
We've implemented multiple views in this recipe. Let's look at each one and discuss how they work.
Indeterminate ProgressViews require no parameter, just a label:
ProgressView("Indeterminate progress view") ProgressView()
Determinate ProgressViews, on the other hand, require a value
parameter that takes a state variable and displays the level of completion:
ProgressView("Downloading", value: progress, total:2)
The total parameter is optional, but defaults to 1.0 if not specified.
Labels are used to display some text and an icon. Labels can be implemented on a single line as follows:
Label("Slow ", systemImage: "tortoise.fill")
Alternatively, they can be implemented on multiple lines. This option is used when you need to customize how the Label
text or icon is displayed:
Label{ Text ("Fast") .font(.title) } icon: { Circle() .fill(Color.orange) .frame(width: 40, height: 20, alignment: .center) .overlay(Text("F")) }
ColorPickers let you display a palate for users to pick colors from. We pass in an @State
variable to the ColorPicker
that stores the value of the selected color:
ColorPicker(selection: $color ){ Text("Pick my background") .background(color) .padding() } ColorPicker("Picker", selection: $secondColor )
The Link
view is used to display clickable links:
Link("Packt Publishing", destination: URL(string: "https://www.packtpub.com/")!)
TextEditor
provides an easy way to allow the user to provide multiline input. The value entered is stored in the @State
variable, text
:
TextEditor(text: $someText)
Menu
provides a convenient way of presenting a user with a list of actions to choose from:
Menu("Actions") { Button("Set TextEditor text to 'magic'"){ someText = "magic" } Button("Turn first picker green") { color = Color.green } }
Menus can also be nested:
Menu("Actions") { Button("Set TextEditor text to 'magic'"){ someText = "magic" } Button("Turn first picker green") { color = Color.green } Menu("Actions") { Button("Set TextEditor text to 'magic'"){ someText = "magic" } Button("Turn first picker green") { color = Color.green } } }
You can add one or more buttons to a menu, each performing a specific action. Although menus can be nested, nesting them should be used sparingly as too much nesting may decrease usability.