SwiftUI was launched during Apple’s Worldwide Developer Conference (WWDC) in June 2019. Since then, its popularity has kept increasing as it has been adopted widely by the Apple developer community. Apple also releases updates every year, adding new and exciting capabilities to SwiftUI.
SwiftUI is a UI framework that ditches UIKit concepts such as Auto Layout for an easier-to-use declarative programming model. SwiftUI is Apple’s preferred way to build user interfaces. It is a platform-agnostic framework that allows the fast and easy creation of applications that work across Apple platforms (iOS, iPadOS, macOS, WatchOS, and tvOS).
There is no question today about the need to learn about SwiftUI. Apple released the fifth iteration of the framework in 2023 and it is adding more features every year. Here are some other compelling reasons to be proficient in SwiftUI:
This book is designed to be your SwiftUI reference material. Each project focuses on a single concept so that you can understand each concept thoroughly, and then combine multiple concepts to build amazing applications.
In this chapter, we will learn about views and controls, SwiftUI’s visual building blocks for app user interfaces. The following recipes will be covered:
ViewModifier
ViewBuilder
The code in this chapter is based on Xcode 15.0 and iOS 17.0. You can download and install the latest version of Xcode from the App Store. You’ll also need to be running macOS Ventura (13.4) or newer.
Simply search for Xcode in the App Store and select and download the latest version. Launch Xcode and follow any additional installation instructions that your system may prompt you with. Once Xcode has fully launched, you’re ready to go.
All the code examples for this chapter can be found on GitHub at https://github.com/PacktPublishing/SwiftUI-Cookbook-3rd-Edition/tree/main/Chapter01-Using-the-basic-SwiftUI-Views-and-Controls/.
SwiftUI has been evolving since the day it was announced in 2019. Every year, Apple adds new APIs and SwiftUI becomes more and more powerful. At the time of this writing, it is possible to create an iOS app exclusively using SwiftUI, without having to integrate UIKit. Since the previous edition of this book, Apple added new functionality to SwiftUI.
In WWDC 2022, Apple added new features to SwiftUI, improved some existing features, and even deprecated some of the APIs just introduced a few years ago. These are the most relevant features:
NavigationStack
and NavigationSplitView
Form
, to support multi-platform apps with a single code base:LabeledContent
view to display pairs of data, like key-value pairs or title and description, inside forms TextField
instancesMultiDatePicker
control to select more than one dateToggle
and Picker
views and a new format for Stepper
views. Table
, introduced in macOS 12, now available on iPadOS 16 and iOS 16, and new toolbar customization for iPadOSPhotosPicker
view, a multi-platform and privacy-preserving API for picking photos and videosShareLink
view, which enables the presentation of the share sheet even in WatchOSTransferable
protocol, to share data between appsgradient
and shadow
, which can also be applied to SFSymbols Grid
, to arrange content in a two-dimensional wayLayout
protocol to create full-custom layoutsIn WWDC 2023, Apple added more functionality to SwiftUI. These are the most relevant features:
Preview(_:traits:body:)
macro that supports UIKit and AppKit out of the boxSwiftData
, a successor of CoreData
, used to persist data between app launchesObservable
macro, and new @State
, @Environment
, and @Bindable
property wrappers, offering a new way of sharing data throughout the app PhaseAnimator
struct, and the Keyframe Animator and CustomAnimation
protocolinspector(isPresented:content:)
view modifierIn this very first recipe of the book, we will start laying out components. In SwiftUI, our user interface is composed of different elements. It is very important to understand how to group components together in different layouts. SwiftUI uses three basic layout components, VStack
, HStack
, and ZStack
. Use the VStack
view to arrange components on a vertical axis, HStack
to arrange components on a horizontal axis, and—you guessed it right—use the ZStack
to arrange components along the vertical and horizontal axis.
In this recipe, we will also look at spacing and adjust the position used to position elements. We will also look at how Spacer and Divider can be used for layout.
Let’s start by creating a new SwiftUI project called TheStacks. Use the following steps:
TheStacks
.Let’s implement the VStack
, HStack
, and ZStack
within a single screen to better understand how each works and the differences between them. The steps are given here:
Contentview/Contentview.swift
file on the navigation pane (left side of Xcode).VStack
and some Text
views:
var body: some View {
VStack {
Text("VStack Item 1")
Text("VStack Item 2")
Text("VStack Item 3")
}
.background(.blue)
}
Figure 1.1: VStack with three items
Spacer
and a Divider
between VStack Item 2
and VStack Item 3
:
Spacer()
Divider()
.background(.black)
Figure 1.2: VStack + Spacer + Divider
HStack
and a ZStack
below VStack Item 3
:
HStack{
Text("HStack Item 1")
Divider()
.background(.black)
Text("HStack Item 2")
Divider()
.background(.black)
Spacer()
Text("HStack Item 3")
}
.background(Color.red)
ZStack{
Text("ZStack Item 1")
.padding()
.background(.green)
.opacity(0.8)
Text("ZStack Item 2")
.padding()
.background(.green)
.offset(x: 80, y: -400)
}
Figure 1.3: VStack, HStack, and ZStack
This concludes our recipe on using stacks. Going forward, we’ll make extensive use of VStack
and HStack
to position various components in our views.
In Xcode 15, a new iOS app project with SwiftUI selected as the interface option starts with a VStack that includes an Image view and a Text view located at the center of the screen. We replaced the content provided by the template with our own VStack
, with three embedded Text views. SwiftUI container views like VStack
determine how to display content by using the following steps:
Adding the Spacer()
forces the view to use the maximum amount of vertical space. This is because the Spacer()
is the most flexible view—it fills the remaining space after all other views have been displayed.
The Divider()
component is used to draw a horizontal line across the width of its parent view. That is why adding a Divider()
view stretched the VStack
background from just around the Text views to the entire width of the VStack
. By default, the divider line does not have a color. To set the divider color, we add the .background(.black)
modifier. Modifiers are methods that can be applied to a view to return a new view. In other words, it applies changes to a view. Examples include .background(.black)
, .padding()
, and .offset(…)
.
The HStack
container is like the VStack
but its contents are displayed horizontally from left to right. Adding a Spacer()
in an HStack
thus causes it to fill all available horizontal space, and a divider draws a vertical line between components in the HStack
.
The ZStack
is like HStack
and VStack
but overlays its content on top of existing items.
You can also use the .frame
modifier to adjust the width and height of a component. Try deleting the Spacer()
and Divider()
from the HStack
and then apply the following modifier to the HStack
:
.frame(
maxWidth: .infinity,
maxHeight: .infinity,
alignment: .topLeading
)
The most basic building block of any application is text, which we use to provide or request information from a user. Some text requires special treatment, such as password fields, which must be masked for privacy reasons.
In this recipe, we will implement different types of SwiftUI Text views. A Text view is used to display one or more lines of read-only text on the screen. A TextField
view is used to display multiline editable text, and a SecureField
view is used to request private information that should be masked, such as passwords.
Create a new SwiftUI project named FormattedText.
We’ll implement multiple types of text-related views and modifiers. Each step in this section applies minor changes to the view, so note the UI changes that occur after each step. Let’s get started:
ContentView
body variable with our own VStack
. The ContentView should look like the following code:
struct ContentView: View {
var body: some View {
VStack{
Text("Hello World")
}
}
}
.fontWeight(.medium)
modifier to the text and observe the text weight change in the canvas preview:
Text("Hello World")
.fontWeight(.medium)
ContentView.swift
file: password
and someText
. Place the values below the ContentView
struct declaration. These variables will hold the content of the user’s password and Textfield
inputs:
struct ContentView: View {
@State private var password = "1234"
@State private var someText = "initial text"
var body: some View {
...
}
VStack
. Each view should be added immediately after the previous one. Add SecureField
and a Text view to the VStack
. The Text view displays the value entered in SecureField
:
SecureField("Enter a password", text: $password)
.padding()
Text("password entered: \(password)")
.italic()
TextField
and a Text view to display the value entered in TextField
:
TextField("Enter some text", text: $someText)
.padding()
Text(someText)
.font(.largeTitle)
.underline()
Text("Changing text color and make it bold")
.foregroundStyle(.blue)
.bold()
Text("Use kerning to change space between characters in the 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(.yellow)
.multilineTextAlignment(.trailing)
.lineSpacing(10)
Now is the moment to test the app. We can choose to run the app in a simulator or click the Play button in the canvas preview, which allows for interactivity. Play with the app and enter some text in the SecureField
and TextField
. Text entered in the SecureField
will be masked, while text in the TextField
will be shown.
The resulting preview should look like this:
Figure 1.4: FormattedText preview
Text views have several modifiers for font, spacing, and other formatting requirements. When in doubt, position the cursor on the line of code that includes the Text view, and press the Esc key to reveal a list of all available modifiers. This is shown in the following example:
Figure 1.5: Using Xcode autocomplete to view formatting options
Unlike regular Text views, TextField
and SecureField
require state variables to store the value entered by the user. State variables are declared using the @State
keyword. SwiftUI manages the storage of properties declared by using @State
and refreshes the body each time the value of the state variable changes.
Values entered by the user are stored using the process of binding. In this recipe, we have state variables bound to the SecureField
and TextField
input parameters. 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 wrapped value of bound state variables, which is the underlying value referenced by the state variable, is accessed without having to use the $ symbol. This is a convenience shortcut provided by Swift, as shown in the following code snippet:
Text(someText)
Apple documentation regarding SwiftUI Text view: https://developer.apple.com/documentation/swiftui/text.
Apps need to be appealing to users and need to engage them to interact with the app. To that purpose, a well-crafted and beautiful user interface with simple and intuitive interactions is very desirable.
Images play an important part in an app’s user interface as they add color and simplicity and they convey messages in a graphical way. It is fundamental to master how to use images in your apps.
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/, so special thanks to jf-brou, Kieran White, and Camilo Fierro.
Let’s start by creating a new SwiftUI project called UsingImages.
Let’s add some images to our SwiftUI project and introduce the modifiers used to style them. The steps are given here:
VStack
.
var body: some View {
VStack {
}
}
Assets.xcassets
(or Assets
) folder, as shown in the following screenshot:Figure 1.6: Assets.xcassets folder in Xcode
Image
view to VStack
:
Image("dogs1")
ImageResource
and ColorResource
structs were introduced, backward compatible to iOS 11 for UIKit and iOS 13 for SwiftUI. Xcode 15 automatically generates instances of ImageResource
and ColorResource
for images and colors in asset catalogs. For example, the three images in our asset catalog, shown in Figure 1.6, generate the following code:
// MARK: - Image Symbols -
@available(iOS 11.0, macOS 10.7, tvOS 11.0, *)
extension ImageResource {
/// The "dog-and-nature" asset catalog image resource.
static let dogAndNature = ImageResource(name: "dog-and-nature", bundle: resourceBundle)
/// The "dog2" asset catalog image resource.
static let dog2 = ImageResource(name: "dog2", bundle: resourceBundle)
/// The "dogs1" asset catalog image resource.
static let dogs1 = ImageResource(name: "dogs1", bundle: resourceBundle)
}
#if canImport(SwiftUI)
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension SwiftUI.Image {
/// Initialize an 'Image' with an image resource.
init(_ resource: ImageResource) {
self.init(resource.name, bundle: resource.bundle)
}
}
#endif
Image
, which takes an ImageResource
instead of Image("dogs1")
, we can write Image(.dogs1)
. The advantage of this approach is the compile-time checking of the correct image name, which eliminates runtime errors from typos..resizable()
modifier to the image and allow SwiftUI to adjust the image such that it fits the screen space available:
Image(.dogs1)
.resizable()
.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)
VStack
:
Image(.dogAndNature)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width:300, height:200)
.clipShape(Circle())
.overlay(Circle().stroke(.blue, lineWidth: 6))
.shadow(radius: 10)
UIImage
instance to initialize an Image
view. This is useful if the UIImage
was generated with legacy code or programmatically. In our example, we use the UIImage
convenience initializer, which takes an ImageResource
instance. For example, to create a UIImage
from the dogs2
image in our asset catalog, we would use: UIImage(resource: .dogs2)
.UIImage
and display it within the VStack
. The resulting code should look like this:
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: UIImage(resource: .dog2))
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 200, height: 200)
}
}
}
Figure 1.7: UsingImages preview
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 14 Pro Max screen:
Figure 1.8: The 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()
or .aspectRatio()
modifiers may lead to different results.
Note that if you set the project deployment target to iOS 14, the ImageResource
struct works without issues since Apple made the struct available for older versions of iOS. This allows us to use the new APIs in older versions of iOS in case your app needs to support them.
Apple documentation regarding SwiftUI Image: https://developer.apple.com/documentation/swiftui/image.
In this recipe, we will learn how to use the various buttons available in SwiftUI. We will use a Button view to trigger the change of a count when clicked and implement a NavigationStack
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.
Let’s start by creating a new SwiftUI project called Buttons.
Let’s create a home screen with buttons for each of the items we want to go over. Once clicked, we’ll use SwiftUI’s NavigationLink to show the view that implements the concept. The steps are given here:
ButtonView
to the project: File | New | File (or press the shortcut keys + N).ButtonView
.EditButtonView
.PasteButtonView
.MenuButtonView
.Important Note
Avoid using the MenuButton view because this is deprecated and only available in macOS 10.14–12.0. For similar functionality, use the Menu view instead, which is available for macOS, iOS and iPadOS.
ContentView.swift
file and create a NavigationStack
to navigate between the SwiftUI views we added to the project. The ContentView
struct should look like this:
struct ContentView: View {
var body: some View {
NavigationStack {
VStack(spacing: 44) {
NavigationLink("Buttons"){
ButtonView()
}
NavigationLink("EditButtons") {
EditButtonView()
}
NavigationLink("MenuButtons") {
MenuButtonView()
}
NavigationLink("PasteButtons") {
PasteButtonView()
}
NavigationLink("Details about text") {
Text("Very long text that should not be displayed in a single line because it is not good design")
.padding()
.navigationTitle(Text("Detail"))
}
}
.navigationTitle(Text("Main View"))
}
}
}
ContentView
preview should look like this:Figure 1.13: ButtonsApp ContentView
ButtonView.swift
file in the project navigator and replace the existing struct with the following code:
struct ButtonView: View {
@State var count = 0
var body: some View {
VStack {
Text("Welcome to your second view")
Text("Current count value: \(count)")
.padding()
Button {
count += 1
} label: {
Text("Tap to Increment count")
.fontWeight(.bold)
.foregroundStyle(.white)
.padding()
.background(.blue)
.clipShape(Capsule())
}
}.navigationBarTitle("Button View")
}
}
#Preview {
NavigationStack {
ButtonView()
}
}
EditButtonView.swift
file in the project navigator and replace the existing struct with the following code that implements an EditButton
:
struct EditButtonView: View {
@State private var animals = ["Cats", "Dogs", "Goats"]
var body: some View {
List{
ForEach(animals, id: \.self){ animal in
Text(animal)
}
.onDelete(perform: removeAnimal)
}
.toolbar {
EditButton()
}
.navigationTitle("EditButtonView")
}
func removeAnimal(at offsets: IndexSet){
animals.remove(atOffsets: offsets)
}
}
#Preview {
NavigationStack {
EditButtonView()
}
}
MenuButtonView.swift
file and replace the existing struct with the following code for MenuButtonView
:
struct MenuButtonView: View {
var body: some View {
Menu("Choose a country") {
Button("Canada") { print("Selected Canada") }
Button("Mexico") { print("Selected Mexico") }
Button("USA") { print("Selected USA") }
}
.navigationTitle("MenuButtons")
}
}
#Preview {
NavigationStack {
MenuButtonView()
}
}
PasteButtonView.swift
file and implement the text regarding PasteButtons
:
struct PasteButtonView: View {
@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()
}
.navigationTitle("PasteButton")
}
}
#Preview {
NavigationStack {
PasteButtonView()
}
}
Go back to ContentView
, run the code in the canvas preview or simulator, and play around with it to see what the results look like.
A NavigationLink
must be placed in a NavigationStack
or NavigationSplitView
prior to being used.
In this recipe, we use a NavigationLink
with 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
. Since our label is a simple Text view, we use the convenience initializer init(_:destination:)
of NavigatioLink
to keep our code more concise.
NavigationLink
buttons can be used to move from one SwiftUI view to another—for example, moving from ContentView
to EditButtonView
. They can also be used to display text details without creating a SwiftUI view in a separate 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 .navigationTitle("Main View"))
modifier adds a title to the ContentView
screen.
The .navigationTitle()
modifier is also added to EditButtonView
and other views. Since these views do not contain NavigationStack
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
. To solve this, we use a NavigationStack
in the PreviewProvider
structs. To make the previews more useful, note how we have enclosed the view in a NavigationStack
so we can see the title in the canvas preview window.
The EditButton
view is used in conjunction with List views to make lists editable. We will go over List and Scroll views in Chapter 2, Displaying Scrollable Content with Lists and Scroll Views, but EditButtonView
provides a peek into how to create an editable list.
The MenuButtonView
uses the Menu struct, introduced in iOS 14, to display a floating menu of actions. Check out the Exploring more views and controls recipe at the end of this chapter for more information on Menu.
PasteButtons
are only available on macOS. Refer to the See also section of this recipe for code on how the PasteButton
is implemented.
PasteButtons
can be found here: https://gist.github.com/sturdysturge/79c73600cfb683663c1d70f5c0778020#file-swiftuidocumentationpaste. More information regarding NavigationLink
buttons can be found here: https://developer.apple.com/documentation/swiftui/navigationlink.In this recipe, we will learn how to implement pickers—namely, Picker, Toggle, Slider, Stepper, and DatePicker. Pickers are typically used to prompt the user to select from a set of mutually exclusive values. Toggle views are used to switch between on/off states. Slider views are used for selecting a value from a bounded linear range of values. As with Slider views, Stepper views also provide a UI for selecting from a range of values. However, steppers use the + and : signs to allow users to increment the desired value by a certain amount. Finally, DatePicker views are used for selecting dates.
Create a new SwiftUI project named UsingPickers.
Let’s create a SwiftUI project that implements various pickers. Each picker will have a @State
variable to hold the current value of the picker. The steps are given here:
ContentView.swift
file, create @State
variables that will hold the values selected by the pickers and other controls. Place the variables between the ContentView
struct and the body:
@State private var choice = 0
@State private var showText = false
@State private var transitModes = ["Bike", "Car", "Bus"]
@State private var sliderVal: Float = 0
@State private var stepVal = 0
@State private var gameTime = Date()
ContentView
struct with a Form
view. Then, add a Section
view and a Picker
view to the form:
Form {
Section {
Picker("Transit Modes", selection: $choice) {
ForEach( 0 ..< transitModes.count, id: \.self) { index in
Text("\(transitModes[index])")
}
}
.pickerStyle(.segmented)
Text("Current choice: \(transitModes[choice])")
}
}
Section
view, add another Section
view and a Toggle
view:
Section{
Toggle(isOn: $showText){
Text("Show Text")
}
if showText {
Text("The Text toggle is on")
}
}
Section
view and a Slider
view:
Section{
Slider(value: $sliderVal, in: 0...10, step: 0.001)
Text("Slider current value \(sliderVal, specifier: "%.1f")")
}
Section
view and a Stepper
view:
Section {
Stepper("Stepper", value: $stepVal, in: 0...5)
Text("Stepper current value \(stepVal)")
}
Section
view and a DatePicker
view:
Section {
DatePicker("Please select a date", selection: $gameTime)
}
Section
view and a slightly modified DatePicker
view that only accepts future dates:
Section {
DatePicker("Please select a date", selection: $gameTime, in: Date()...)
}
Figure 1.14: SwiftUI Form with Pickers
Form
views group controls that are used for data entry, and Section
views create hierarchical view content. Section
views can be embedded inside a Form
view to display information grouped together. The default presentation style for a Form with embedded Section views is to include a gray padding area between each section for visual grouping, as shown in Figure 1.14.
Picker
views are used for selecting from a set of mutually exclusive values. In the following example, a segmented picker is used to select a transit mode from our transitModes
state variable:
Picker("Transit Modes", selection: $choice) {
ForEach( 0 ..< transitModes.count, id:\.self) { index in
Text("\(transitModes[index])")
}
}
.pickerStyle(.segmented)
As shown in the preceding example, a Picker
view takes two parameters, a string describing its function, and a state variable that holds the value selected. 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 transitModes
indices. The transit mode located in the selected index can then be displayed using Text("\(transitModes[index])")
. It is also worth noting that we need to apply a .segmented
style to the picker using the .pickerStyle()
modifier, to use the visual segmented style everyone is used to in iOS.
Toggle
views 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 @State
property of the Toggle
reflects the current state of the toggle.
Creating a slider requires three arguments:
value
: The @State
variable to bind the user input toin
: The range of the sliderstep
: By how much the slider should change when the user moves itIn the sample code, our slider moves can hold values between 0 and 10, with a step of 0.001.
Steppers take three arguments too—a string for the label, value
, and in
. The value
argument holds the @State
variable that binds the user input, and the in
argument holds the range of values for the stepper.
In this recipe, we also demonstrate two applications of a date picker. The first from the top shows a date picker whose first argument is the label of DatePicker
, and the second argument holds the state variable that binds the user input. Use it in situations where the user is allowed to pick any date without restriction. The other date picker contains a third parameter, in
. This parameter represents the date range the user can select.
Important Note
The @State
variables need to 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 container view. Styles can be overridden using the .pickerStyle()
modifier.
SwiftUI comes with built-in modifiers such as background()
and fontWeight()
, among others. It also gives you the ability to create your own custom modifiers. You can use custom modifiers to combine multiple existing modifiers into one.
In this section, we will create a custom modifier that adds rounded corners and a background to a Text view.
Create a new SwiftUI project named UsingViewModifiers
.
Let’s create a view modifier and use a single line of code to apply it to a Text
view. The steps are given here:
ContentView
view with:
Text("Perfect")
ContentView.swift
file, create a struct that conforms to the ViewModifier
protocol, accepts a parameter of type Color
, and applies styles to the view’s body:
struct BackgroundStyle: ViewModifier {
var bgColor: Color
func body(content: Content) -> some View{
content
.frame(width:UIScreen.main.bounds.width * 0.3)
.foregroundStyle(.black)
.padding()
.background(bgColor)
.cornerRadius(20)
}
}
modifier()
modifier:
Text("Perfect").modifier(BackgroundStyle(bgColor:
.blue))
extension
to the View
protocol. The extension should be created outside the struct or Xcode will issue an error:
extension View {
func backgroundStyle(color: Color) -> some View{
self.modifier(BackgroundStyle(bgColor: color))
}
}
Text
view with the backgroundStyle()
modifier that you just created, which will add your custom styles:
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.
A view modifier
creates a new view by altering the original view to which it is applied. We create a new view modifier by creating a struct that conforms to the ViewModifier
protocol and apply our styles in the implementation of the required body
function. You can make the ViewModifier
customizable by requiring input parameters/properties that would be used when applying styles.
In the example here, the bgColor
property is used in our BackGroundStyle
struct, which alters the background color of the content passed to the body function.
At the end of Step 2, we have a functioning ViewModifier
but decide to make it easier to use by creating a View
extension and adding in a function that calls our struct:
extension View {
func backgroundStyle(color: Color) -> some View {
modifier(BackgroundStyle(bgColor: color))
}
}
We are thus able to use .backgroundStyle(color: Color)
directly on our views instead of .modifier(BackgroundStyle(bgColor:Color))
.
Apple documentation on view modifiers: https://developer.apple.com/documentation/swiftui/viewmodifier.
Apple defines ViewBuilder
as “a custom parameter attribute that constructs views from closures.” ViewBuilder
can be used to create custom views that can be used across an application with minimal or no code duplication. In this recipe, we will create a custom SwiftUI view, BlueCircle
, that applies a blue circle to the right of its content.
Let’s start by creating a new SwiftUI project called UsingViewBuilder.
We’ll create our ViewBuilder
in a separate swift file and then apply it to items that we’ll create in the ContentView.swift
file. The steps are given here:
BlueCircle
and click Create.#Preview
macro from the file. BlueCircle ViewModifier
:
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()
}
}
ContentView.swift
file and try out the BlueCircle ViewBuilder
:
var body: some View {
VStack {
BlueCircle {
Text("some text here")
Rectangle()
.fill(Color.red)
.frame(width: 40, height: 40)
}
BlueCircle {
Text("Another example")
}
}
}
Figure 1.16: ViewBuilder result preview
We use the ViewBuilder
struct to create a view template that can be used anywhere in the project without duplicating code. The ViewBuilder
struct must contain a body
property since it extends the View
protocol.
Within the body
property/view, we update the content
property with the components we want to use in our custom view. In this case, we use a BlueCircle
. Notice the location of the content
property. This determines the location where the view passed to our ViewBuilder
will be placed.
Apple documentation on ViewBuilder: https://developer.apple.com/documentation/swiftui/viewbuilder.
The SF Symbols 5 library provides a set of over 5,000 free consistent and highly configurable symbols. Each year, Apple adds more symbols and symbol variants to the collection.
You can download and browse through a list of SF symbols using the macOS app available for download here: https://developer.apple.com/sf-symbols/.
In this recipe, we will use SF symbols in labels and images. We’ll also apply various modifiers that will add a punch to your design.
Let’s start by creating a new SwiftUI project called UsingSF Symbols.
Let’s create an app where we use different combinations of SF Symbols and modifiers. The steps are given here:
ContentView.swift
file and replace the entire body content with a VStack
, HStack
, and some SF Symbols:
VStack {
HStack{
Image(systemName: "c")
Image(systemName: "o")
Image(systemName: "o")
Image(systemName: "k")
}
.symbolVariant(.fill.circle)
.foregroundStyle(.yellow, .blue)
.font(.title)
}
VStack
content and embed another HStack
with SF Symbols for the word book
:
HStack{
Image(systemName: "b.circle.fill")
Image(systemName: "o.circle.fill")
.foregroundStyle(.red)
Image(systemName: "o.circle.fill")
.imageScale(.large)
Image(systemName: "k.circle.fill")
.accessibility(identifier: "Letter K")
}
.foregroundStyle(.blue)
.font(.title)
.padding()
HStack
with more SF Symbols:
HStack{
Image(systemName: "allergens")
Image(systemName: "ladybug")
}
.symbolVariant(.fill)
.symbolRenderingMode(.multicolor)
.font(.largeTitle)
Picker
view with a segmented style that changes the appearance of the Wi-Fi SF Symbol based on the picker selection:
HStack {
Picker("Pick One", selection: $wifiSelection) {
Text("No Wifi").tag(0)
Text("Searching").tag(1)
Text("Wifi On").tag(2)
}
.pickerStyle(.segmented)
.frame(width: 240)
.padding(.horizontal)
Group {
switch wifiSelection {
case 0:
Image(systemName: "wifi")
.symbolVariant(.slash)
case 1:
Image(systemName: "wifi")
.symbolEffect(.variableColor.iterative.reversing)
default:
Image(systemName: "wifi")
.foregroundStyle(.blue)
}
}
.foregroundStyle(.secondary)
.font(.title)
}
.padding()
@State
property to fix the Xcode error. Immediately below the declaration of the ContentView
struct, add the wifiSelection
property:
@State private var wifiSelection = 0
Figure 1.17: SF Symbols in action
SF Symbols defines several design variants such as enclosed, fill, and slash. These different variants can be used to convey different information—for example, a slash variant on a Wi-Fi symbol lets the user know if the Wi-Fi is unavailable.
In our first HStack
, we use the .symbolVariant(.fill.circle)
modifier to apply the .fill
and .circle
variants to all the items in the HStack
. This could also be accomplished using the following code:
HStack{
Image(systemName: "c.circle.fill")
Image(systemName: "o.circle.fill ")
Image(systemName: "o.circle.fill ")
Image(systemName: "k.circle.fill ")
}
However, the preceding code is too verbose and would require too many changes if we decided that we didn’t need either the .circle
or .fill
variant, or both.
We also notice something new in our first HStack
—the .foregroundStyle(...)
modifier. The .foregroundStyle
modifier can accept one, two, or three parameters corresponding to the primary, secondary, and tertiary colors. Some symbols may have all three levels of colors, or only primary and secondary, or primary and tertiary. For symbols without all three levels, only the ones that pertain to them are applied to the symbol. For example, a tertiary color applied to an SF Symbol with only primary and secondary levels will have no effect on the symbol.
The second HStack
also uses the .symbolVariant
modifier with one variant. It also introduces a new modifier, .symbolRenderingMode()
. Rendering modes can be used to control how color is applied to symbols. The multicolor rendering mode renders symbols as multiple layers with their inherited styles. Adding the .multicolor
rendering mode is enough to present a symbol with its default layer colors. Other rendering modes include hierarchical
, monochrome
, and palette
.
Finally, we create another HStack
with a segmented picker for a Wi-Fi system image where we change the appearance based on the status of the wifiSelection
state variable. The picker reads the state variable and changes the wifi
symbol appearance from a slashed symbol when “No Wifi” is selected to a variable color animated symbol when “Searching” is selected to a solid blue symbol when “Wifi On” is selected. Here, we used the new Symbols framework introduced in iOS 17, and the .symbolEffect
view modifier to add an animation to a symbol. When we want to add animations to a symbol, the SF Symbols Mac app allows us to configure all the animations and preview the result. We can even export the animation configuration to add it in Xcode.
SwiftUI was announced at WWDC 2019 and is only available on devices running iOS 13 and above. Many improvements and new APIs have been added to SwiftUI since its introduction, to the point that, at the time of this writing, we can create an app in SwiftUI without using any UIKit components.
However, if you’re dealing with legacy code written in UIKit, and have the need to integrate the code in your SwiftUI app, Apple provides a way to do this. UIViews and UIViewControllers can be seamlessly placed inside SwiftUI views and vice versa.
In this recipe, we’ll look at how to integrate UIKit APIs in SwiftUI. We will create a project that wraps instances of UIActivityIndicatorView
to display an indicator in SwiftUI.
Open Xcode and create a SwiftUI project named UIKitToSwiftUI.
We can display UIKit views in SwiftUI by using the UIViewRepresentable
protocol. Follow these steps to implement the UIActivityIndicatorView
in SwiftUI:
ActivityIndicator
.import Foundation
statement with import SwiftUI
:
import SwiftUI
ActivityIndicator
to use the UIViewRepresentable
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()
}
}
}
ContentView.swift
file and replace the struct with the following code to make use of the ActivityIndicator
instance that we just created. Let’s also add a Toggle
control to turn the indicator on or off:
struct ContentView: View {
@State private var animate = true
var body: some View {
VStack{
ActivityIndicator(animating: animate)
HStack{
Toggle(isOn: $animate){
Text("Toggle Activity")
}
}.padding()
}
}
}
ContentView
preview should look like this:Figure 1.18: UIKit UIActivityIndicatorView inside our SwiftUI view
UIKit views can be implemented in SwiftUI by using the UIViewRepresentable
protocol to wrap the UIKit views. In this recipe, we make use of a UIActivityIndicatorView
by first wrapping it with a UIViewRepresentable
.
In our ActivityIndicator.swift
file, we implement a struct that conforms to the UIViewRepresentable
protocol. This requires us to implement both the makeUIView
and updateUIView
functions. The makeUIView
function creates and prepares the view, while the updateUIView
function updates the UIView
when the animation changes.
Important Note
You can implement the preceding features in iOS 14+ apps by using SwiftUI’s ProgressView
. The purpose of the recipe was to show how to integrate a UIKit view with SwiftUI.
Check out the Exploring more views and controls recipe at the end of this chapter for more information on ProgressView
.
Apple’s tutorial on how to integrate UIKit and SwiftUI:
https://developer.apple.com/tutorials/swiftui/interfacing-with-uikit
In this recipe, we will learn how to navigate from a UIKit view to a SwiftUI view while passing a secret text to our SwiftUI view. This recipe assumes prior knowledge of UIKit and it is most useful to developers who want to integrate SwiftUI into a legacy UIKit app. If this is not your case, feel free to skip to the next recipe.
We’ll be making use of a UIKit storyboard, a visual representation of the UI in UIKit. The Main.storyboard
file is to UIKit what the ContentView.swift
file is to SwiftUI. They are both the default home views that are created when you start a new project.
We start off this project with a simple UIKit project that contains a button.
Get the following ready before starting out with this recipe:
StartingPoint
folder and double-click on AddSwiftUIToUIKit
.xcodeproj to open the project in Xcode.We will add a NavigationController
to the UIKit ViewController
that allows the app to switch from the UIKit to the SwiftUI view when the button is clicked:
Main.storyboard
file in Xcode by clicking on it. The Main.storyboard
looks like this:Figure 1.19: UIKit View Controller
ViewController
to select it.ViewController
to the project:Objects
library, type hosting
in the search bar, select Hosting View Controller, and drag it out to the storyboard:Figure 1.20: Creating a UIKit Hosting View Controller
ViewController
button to the new Hosting View Controller that we added.Figure 1.21: Adjust Editor Options button
Figure 1.22: Xcode with the Assistant editor open
viewDidLoad
function in the ViewController.swift
file.goToSwiftUI
and click Connect. The following code will be added to the ViewController.swift
file:
@IBSegueAction func goToSwiftUI(_ coder: NSCoder) -> UIViewController? {
return <#UIHostingController(coder: coder, rootView: ...)#>
}
ViewController
page, below import UIKit
:
import SwiftUI
goToSwiftUI
function, create a text that will be passed to our SwiftUI view. Also, create a rootView
variable that specifies the SwiftUI view that you would like to reach. Finally, return the UIHostingController
, which is a special ViewController
used to display the SwiftUI view. The resulting code should look like this:
@IBSegueAction func goToSwiftUI(_ coder: NSCoder) -> UIViewController? {
let greetings = "Hello From UIKit"
let rootView = Greetings(textFromUIKit: greetings)
return UIHostingController(coder: coder, rootView: rootView)
}
Greetings.swift
.View
component that displays some text passed to it:
struct Greetings: View {
var textFromUIKit: String
var body: some View {
Text(textFromUIKit)
}
}
#Preview {
Greetings(textFromUIKit: "Hello, World!")
}
Run the project in the simulator, click on the UIKit button, and watch the SwiftUI page get displayed.
To host SwiftUI views in an existing app, you need to wrap the SwiftUI hierarchy in a ViewController
or InterfaceController
.
We start by performing core UIKit concepts, such as adding a Navigation View Controller to the storyboard and adding a Hosting View Controller as a placeholder for our SwiftUI view.
Lastly, we create an IBSegueAction to present our SwiftUI view upon clicking the UIKit button.
In this section, we introduce some views and controls that did not clearly fit in any of the earlier created recipes. We’ll look at the ProgressView
, ColorPicker
, Link
, and Menu
views.
ProgressView
is used to show the degree of completion of a task. There are two types of ProgressView
: indeterminate progress views show a spinning circle till a task is completed, while determinate progress views show a bar that gets filled up to show the degree of completion of a task.
ColorPicker
views allow users to select from a wide range of colors, while Menu
views present a list of items that users can choose from to perform a specific action.
Let’s start by creating a new SwiftUI project called MoreViewsAndControls.
Let’s implement some views and controls in the ContentView.swift
file. We will group the controls in Section
instances in a List
view. Section allows us to include an optional header. The steps are given here:
ContentView
struct declaration, add the state variables that we’ll be using for various components:
@State private var progress = 0.5
@State private var color = Color.red
@State private var secondColor = Color.yellow
@State private var someText = "Initial value"
List
view with a Section
view, two ProgressView
views, and a Button
view:
List {
Section(header: Text("ProgressViews")) {
ProgressView("Indeterminate progress view")
ProgressView("Downloading",value: progress, total:2)
Button("More") {
if (progress < 2) {
progress += 0.5
}
}
}
}
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"))
}
}
ColorPicker
:
Section(header: Text("ColorPicker")) {
ColorPicker(selection: $color ) {
Text("Pick my background")
.background(color)
.padding()
}
ColorPicker("Picker", selection: $secondColor )
}
Link
:
Section(header: Text("Link")) {
Link("Packt Publishing", destination: URL(string: "https://www.packtpub.com/")!)
}
TextEditor
:
Section(header: Text("TextEditor")) {
TextEditor(text: $someText)
Text("current editor text:\n\(someText)")
}
Menu
:
Section(header: Text("Menu")) {
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 'real magic'"){
someText = "real magic"
}
Button("Turn first picker gray") {
color = Color.gray
}
}
}
}
listStyle
modifier on the List
:
List {
...
}
.listStyle(.grouped)
Figure 1.23: More Views and Controls app preview
We’ve implemented multiple views in this recipe. Let’s look at each one and discuss how they work.
Indeterminate ProgressView
requires no parameters:
ProgressView("Indeterminate progress view")
ProgressView()
Determinate ProgressView
components, 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 in the ProgressView
component is optional and defaults to 1.0
if not specified.
Label
views were mentioned earlier in the Simple graphics using SF Symbols recipe. Here, we introduce a second option for implementing labels where we customize the design of the label text and icon:
Label {
Text ("Fast")
.font(.title)
} icon: {
Circle()
.fill(Color.orange)
.frame(width: 40, height: 20, alignment: .center)
.overlay(Text("F"))
}
Let’s move on to the ColorPicker
view. Color pickers let you display a palette for users to pick colors from. We create a two-way binding using the color
state variable so that we can store the color selected by the user:
ColorPicker(selection: $color ) {
Text("Pick my background")
.background(color)
.padding()
}
Link
views are used to display clickable links:
Link("Packt Publishing", destination: URL(string: "https://www.packtpub.com/")!)
Finally, the Menu
view provides a convenient way of presenting a user with a list of actions to choose from and can also be nested, as seen here:
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 'real magic'"){
someText = "real magic"
}
Button("Turn first picker gray") {
color = Color.gray
}
}
}
You can add one or more buttons to a menu, each performing a specific action. Although menus can be nested, this should be done sparingly as too much nesting may decrease usability.
After learning the basics of SwiftUI, we concluded the chapter with this recipe, where we used several SwiftUI view components that we could incorporate into our apps.
To join the Discord community for this book – where you can share feedback, ask questions to the author, and learn about new releases – follow the QR code below:
Where there is an eBook version of a title available, you can buy it from the book details for that title. Add either the standalone eBook or the eBook and print book bundle to your shopping cart. Your eBook will show in your cart as a product on its own. After completing checkout and payment in the normal way, you will receive your receipt on the screen containing a link to a personalised PDF download file. This link will remain active for 30 days. You can download backup copies of the file by logging in to your account at any time.
If you already have Adobe reader installed, then clicking on the link will download and open the PDF file directly. If you don't, then save the PDF file on your machine and download the Reader to view it.
Please Note: Packt eBooks are non-returnable and non-refundable.
Packt eBook and Licensing When you buy an eBook from Packt Publishing, completing your purchase means you accept the terms of our licence agreement. Please read the full text of the agreement. In it we have tried to balance the need for the ebook to be usable for you the reader with our needs to protect the rights of us as Publishers and of our authors. In summary, the agreement says:
If you want to purchase a video course, eBook or Bundle (Print+eBook) please follow below steps:
Our eBooks are currently available in a variety of formats such as PDF and ePubs. In the future, this may well change with trends and development in technology, but please note that our PDFs are not Adobe eBook Reader format, which has greater restrictions on security.
You will need to use Adobe Reader v9 or later in order to read Packt's PDF eBooks.
Packt eBooks are a complete electronic version of the print edition, available in PDF and ePub formats. Every piece of content down to the page numbering is the same. Because we save the costs of printing and shipping the book to you, we are able to offer eBooks at a lower cost than print editions.
When you have purchased an eBook, simply login to your account and click on the link in Your Download Area. We recommend you saving the file to your hard drive before opening it.
For optimal viewing of our eBooks, we recommend you download and install the free Adobe Reader version 9.