0% found this document useful (0 votes)
28 views

Ng Simon Mastering Swiftui Ios 17 Updated 2024

The document is a comprehensive guide to mastering SwiftUI for iOS 17 and Xcode 15, covering various topics from basic UI elements to advanced animations and data handling. It emphasizes the shift from imperative to declarative programming in UI development, showcasing practical examples and exercises throughout. The content is structured in chapters that progressively build on concepts, making it suitable for both beginners and experienced developers transitioning to SwiftUI.

Uploaded by

hhkktsj
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
28 views

Ng Simon Mastering Swiftui Ios 17 Updated 2024

The document is a comprehensive guide to mastering SwiftUI for iOS 17 and Xcode 15, covering various topics from basic UI elements to advanced animations and data handling. It emphasizes the shift from imperative to declarative programming in UI development, showcasing practical examples and exercises throughout. The content is structured in chapters that progressively build on concepts, making it suitable for both beginners and experienced developers transitioning to SwiftUI.

Uploaded by

hhkktsj
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 989

Table of Contents

Preface
Chapter 1 - Introduction to SwiftUI
Declarative vs Imperative Programming
No more Interface Builder and Auto Layout
The Combine Approach
Learn Once, Apply Anywhere
Interfacing with UIKit/AppKit/WatchKit
Use SwiftUI for Your Next Project
Chapter 2 - Getting Started with SwiftUI and Working with Text
Creating a New Project for Playing with SwiftUI
Displaying a Simple Text
Changing the Font Type and Color
Using Custom Fonts
Working with Multiline Text
Setting the Padding and Line Spacing
Rotating the Text
Summary
Chapter 3 - Working with Images
Understanding SF Symbols
Displaying a System Image
Using Your Own Images
Resizing an Image
Aspect Fit and Aspect Fill
Creating a Circular Image
Adjusting the Opacity
Applying an Overlay to an Image
Darken an Image Using Overlay
Wrap Up
Chapter 4 - Layout User Interfaces with Stacks
Understanding VStack, HStack, and ZStack
Creating a New Project with SwiftUI enabled
Using VStack
Using HStack
Using ZStack
Exercise #1
Handling Optionals in SwiftUI
Using Spacer
Exercise #2
Chapter 5 - Understanding ScrollView and Building a Carousel UI
Creating a Card-like UI
Introducing ScrollView
Exercise #1
Creating a Carousel UI with Horizontal ScrollView
Hiding the Scroll Indicator
Grouping View Content
Resize the Text Automatically
Exercise #2
Chapter 6 - Working with SwiftUI Buttons and Gradient
Customizing the Button's Font and Background
Adding Borders to the Button
Creating a Button with Images and Text
Published: 31/10/2019 | Last updated: 20/9/2023 | AppCoda © 2023

Using Label
Creating a Button with Gradient Background and Shadow
Creating a Full-width Button
Styling Buttons with ButtonStyle
Exercise
Summary
Chapter 7 - Understanding State and Binding
Controlling the Button's State
Exercise #1
Working with Binding
Exercise #2
Summary
Chapter 8 - Implementing Path and Shape for Line Drawing and Pie Charts
Understanding Path
Using Stroke to Draw Borders
Drawing Curves
Fill and Stroke
Drawing Arcs and Pie Charts
Understanding the Shape Protocol
Using the Built-in Shapes
Creating a Progress Indicator Using Shapes
Drawing a Donut Chart
Summary
Chapter 9 - Basic Animations and Transitions
Implicit and Explicit Animations
Creating a Loading Indicator Using RotationEffect
Creating a Progress Indicator

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 4


Delaying an Animation
Transforming a Rectangle into Circle
Understanding Transitions
Exercise #1: Using Animation and Transition to Build a Fancy Button
Exercise #2: Animated View Transitions
Summary
Chapter 10 - Understanding Dynamic List, ForEach and Identifiable
Creating a Simple List
Creating a List View with Text and Images
Refactoring the Code
Exercise
Chapter 11 - Working with Navigation UI and Navigation Bar Customization
Implementing a Navigation View
Passing Data to a Detail View Using NavigationLink
Customizing the Navigation Bar
Exercise
Building the Detail View
Removing the Disclosure Indicator
An even more Elegant UI with a Custom Back Button
Summary
Chapter 12 - Playing with Modal Views, Floating Buttons and Alerts
Understanding Sheet in SwiftUI
Implementing the Modal View Using isPresented
Changing the Navigation View Style
Implementing the Modal View with Optional Binding
Creating a Floating Button for Dismissing the Modal View
Using Alerts

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 5


Displaying a Full Screen Modal View
Summary
Chapter 13 - Building a Form with Picker, Toggle and Stepper
Building the Form UI
Creating a Picker View
Working with Toggle Switches
Using Steppers
Presenting the Form
Exercise
What's Coming Next
Chapter 14 - Data Sharing with Combine and Environment Objects
Refactoring the Code with Enum
Saving the User Preferences in UserDefaults
Sharing Data Between Views Using @EnvironmentObject
Implementing the Filtering Options
Implementing the Sort Option
What's Coming Next
Chapter 15 - Building a Registration Form with Combine and View Model
Layout the Form using SwiftUI
Understanding Combine
Combine and MVVM
Summary
Chapter 16 - Working with Swipe-to-Delete, Context Menu and Action Sheets
Implementing Swipe-to-delete
Creating a Context Menu
Working with Action Sheets
Exercise

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 6


Chapter 17 - Using Gestures
Using the Gesture Modifier
Using Long Press Gesture
The @GestureState Property Wrapper
Using Drag Gesture
Combining Gestures
Refactoring the Code Using Enum
Building a Generic Draggable View
Exercise
Summary
Chapter 18 - Displaying an Expandable Bottom Sheet Using Presentation Detents
Introducing Presentation Detents
Creating the Restaurant Detail View
Make It Scrollable
Bring Up the Detail View
Hide the Drag Indicator
Controlling its Size Using Fraction and Height
Storing the Selected Detent
Summary
Chapter 19 - Creating a Tinder-like UI with Gestures and Animations
Building the Card Views and Menu Bars
Implementing the Card Deck
Implementing the Swiping Motion
Displaying the Heart and xMark icons
Removing/Inserting the Cards
Fine Tuning the Animations
Summary

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 7


Chapter 20 - Creating an Apple Wallet like Animation and View Transition
Building a Card View
Building the Wallet View and Card Deck
Adding a Slide-in Animation
Handling the Tap Gesture and Displaying the Transaction History
Rearranging the Cards Using the Drag Gesture
Summary
Chapter 21 - Working with JSON, Slider and Data Filtering
Understanding JSON and Codable
Using JSONDecoder and Codable
Working with Custom Property Names
Working with Nested JSON Objects
Working with Arrays
Building the Kiva Loan App
Calling the Web API
Summary
Chapter 22 - Building a ToDo app with SwiftData
Understanding SwiftData
Understanding the ToDo App Demo
Working with SwiftData
Working with SwiftUI Preview
Summary
Chapter 23 - Integrating UIKit with SwiftUI Using UIViewRepresentable
Understanding UIViewRepresentable
Adding a Search Bar
Capturing the Search Text
Handling the Cancel Button

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 8


Performing the Search
Summary
Chapter 24 - Creating a Search Bar View and Working with Custom Binding
Implementing the Search Bar UI
Dismissing the Keyboard
Working with Custom Binding
Summary
Chapter 25 - Putting Everything Together to Build a Real World App
Understanding the Model
Working with Core Data
Implementing the New Payment View
Implementing the Payment Activity Detail View
Walking Through the Dashboard View
Managing Payment Activities with Core Data
Exploring the Extensions
Handling the Software Keyboard
Summary
Chapter 26 - Creating an App Store like Animated View Transition
Introducing the Demo App
Understanding the Card View
Implementing the Card View
Building the List View
Expanding the Card View to Full Screen
Animating the View Changes
Summary
Chapter 27 - Building an Image Carousel
Introducing the Travel Demo App

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 9


The ScrollView Problem
Building a Carousel with HStack and DragGesture
Moving the HStack Card by Card
Adding the Drag Gesture
Animating the Card Transition
Adding the Title
Exercise: Working on the Detail View
Implementing the Trip Detail View
Bringing up the Detail View
Summary
Chapter 28 - Building an Expandable List View Using OutlineGroup
The Demo App
Creating the Expandable List
Using Inset Grouped List Style
Using OutlineGroup to Customize the Expandable List
Understanding DisclosureGroup
Exercise
Summary
Chapter 29 - Building Grid Layout Using LazyVGrid and LazyHGrid
The Essential of Grid Layout in SwiftUI
Using LazyVGrid to Create Vertical Grids
Using GridItem to Vary the Grid Layout (Flexible/Fixed/Adaptive)
Switching Between Different Grid Layouts
Building Grid Layout with Multiple Grids
Exercise
Summary
Chapter 30 - Creating an Animated Activity Ring with Shape and Animatable

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 10


Preparing the Color Extension
Implementing the Circular Progress Bar
Adding a Gradient
Varying the Progress
Animating the Ring Shape with Animatable
The 100% Problem
Exercise
Summary
Chapter 31 - Working with AnimatableModifier and LibraryContentProvider
Understanding AnimatableModifier
Animating Text using AnimatableModifer
Using LibraryContentProvider
Exercise
Summary
Chapter 32 - Working with TextEditor to Create Multiline Text Fields
Using TextEditor
Using the onChange() Modifier to Detect Text Input Change
Summary
Chapter 33 - Using matchedGeometryEffect to Create View Animations
Revisiting SwiftUI Animation
Understanding the matchedGeometryEffect Modifier
Morphing From a Circle to a Rounded Rectangle
Exercise #1
Swapping Two Views with Animated Transition
Exercise #2
Creating a Basic Hero Animation
Passing @Namespace between Views

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 11


Summary
Chapter 34 - ScrollViewReader and Grid Animation
The Demo App
Building the Photo Grid
Adding the Dock
Handling Photo Selection
Using MatchedGeometryEffect to Animate the Transition
Using ScrollViewReader to Move a Scroll View
Summary
Chapter 35 - Working with Tab View and Tab Bar Customization
Using TabView to Create the Tab Bar Interface
Customizing the Tab Bar Color
Switching Between Tabs Programmatically
Hiding the Tab Bar in a Navigation View
Chapter 36 - Using AsyncImage in SwiftUI for Loading Images Asynchronously
The Basic Usage of AsyncImage
Customizing the Image Size and PlaceHolder
Handling Different Phases of the Asynchronous Operation
Chapter 37 - Implementing Search Bar Using Searchable
The Basic Usage of Searchable
Search Bar Placement
Performing Search and Displaying Search Results
Chapter 38 - Creating Bar Charts and Line Charts with the Charts Framework
Building a Simple Bar Chart
Creating a Line Chart
Customizing Chart Axes
Creating a Multi-line Charts

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 12


Chapter 39 - Capturing Text within Image Using Live Text APIs
Using DataScannerViewController
Working with DataScannerViewController in SwiftUI
Capturing Text Using DataScanner
Chapter 40 - How to Use ShareLink for Sharing Data Like Text and Photos
Basic Usage of ShareLink
Customizing the Appearance of Share Link
Sharing Images
Conforming to Transferable
Chapter 41 - Using ImageRenderer to Convert SwiftUI Views into Images
Converting the View into an Image using ImageRenderer
Adding a Share Button
Adjusting the Image Scale
Chapter 42 - Using ImageRenderer to Convert SwiftUI Views into Images
Saving the Chart View as a PDF Document Using ImageRenderer
Make the PDF file available to the Files app
Chapter 43 - Using Gauge to Display Progress and Create a Speedometer
Using Custom Range
Using Image Labels
Customizing the Gauge Style
Chapter 44 - Creating Grid Layout Using Grid APIs
The Basics
Comparing Grid Views and Stack Views
Using gridCellColumns to Merge Cells
Adding a blank cell
Adjusting cell spacing
Chapter 45 - Switching Layout with AnyLayout

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 13


Using AnyLayout
Switching Layouts based on the device's orientation
Chapter 46 - Working with Maps and Annotations
The MapKit Basics
Animating the Change of Map Position
Adding Markers and Annotations
Search for Points of Interest
Chapter 47 - Working with Preview Macro
The New #Preview Macro
Previewing Multiple Views
Preview Views in Landscape Orientation
Writing UIKit Previews
Chapter 48 - Building Pie Charts and Donut Charts
Creating Pie Charts with SectorMark
Customizing the Pie Chart
Converting the Pie Chart to Donut Chart
Interacting with Charts
Chapter 49 - Detecting scroll positions in ScrollView with SwiftUI
Using the ScrollPosition Modifier
Scroll to the Top
Adjusting Content Margins of Scroll Views
Chapter 50 - Animating Scroll View Using SwiftUI
Using ScrollTransition Modifier
Working with Scroll Transition Configuration
Using the Phase Value
Chapter 51 - Using UnevenRoundedRectangle to Round Specific Corners
Working with UnevenRoundedRectangle

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 14


Animating the Rounded Corners
Creating Unique Shapes
Chapter 52 - Getting Started with SwiftData
Using Code to Create the Data Model
Building a Simple To Do App
Storing to-do items into the database
Populating Dummy Data for Preview
Chapter 53 - How to Embed Photo Pickers in iOS Apps
Revisiting PhotosPicker
Implementing an Inline PhotosPicker
Handling Multiple Photo Selections
Chapter 54 - Using PhaseAnimator to Create Dynamic Multi-Step Animations
Building a Simple Animation with PhaseAnimator
Using Enum to Define Multi Step Animations
Using Triggers
Chapter 55 - Creating Advanced Animations with KeyframeAnimator
Working with KeyframeAnimator
Keyframe Types
Multiple Keyframe Tracks
Chapter 56 - Using TipKit to Display Tooltips
Understanding the Tip Protocol
Displaying Tips Using Popover and TipView
Previewing Tips

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 15


Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 16
Copyright ©2023 by AppCoda Limited

All right reserved. No part of this book may be used or reproduced, stored or transmitted
in any manner whatsoever without written permission from the publisher.

Published by AppCoda Limited

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 17


Preface
Frankly, I didn't expect Apple would announce anything big in WWDC 2019 that would
completely change the way we build UI for Apple platforms. A couple years ago, Apple
released a brand new framework called SwiftUI, along with the release of Xcode 11. The
debut of SwiftUI was huge, really huge for existing iOS developers or someone who is
going to learn iOS app building. It was unarguably the biggest change in iOS app
development in recent years.

I have been doing iOS programming for over 10 years and already get used to developing
UIs with UIKit. I love to use a mix of storyboards and Swift code for building UIs.
However, whether you prefer to use Interface Builder or create UI entirely using code,
the approach of UI development on iOS doesn't change much. Everything is still relying
on the UIKit framework.

To me, SwiftUI is not merely a new framework. It's a paradigm shift that fundamentally
changes the way you think about UI development on iOS and other Apple platforms.
Instead of using the imperative programming style, Apple now advocates the
declarative/functional programming style. Instead of specifying exactly how a UI
component should be laid out and function, you focus on describing what elements you
need in building the UI and what the actions should perform when programming in
declarative style.

If you have worked with React Native or Flutter before, you will find some similarities
between the programming styles and probably find it easier to build UIs in SwiftUI. That
said, even if you haven't developed in any functional programming languages before, it
would just take you some time to get used to the syntax. Once you manage the basics, you
will love the simplicity of coding complex layouts and animations in SwiftUI.

SwiftUI has evolved so much in these four years. Apple has packed even more features
and brought more UI components to the SwiftUI framework, which comes alongside with
Xcode 15. It just takes UI development on iOS, iPadOS, and macOS to the next level. You
can develop some fancy animations with way less code, as compared to UIKit. Most

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 18


importantly, the latest version of the SwiftUI framework makes it easier for developers to
develop apps for Apple platforms. You will understand what I mean after you go through
the book.

The release of SwiftUI doesn't mean that Interface Builder and UIKit are deprecated right
away. They will still stay for many years to come. However, SwiftUI is the future of app
development on Apple's platforms. To stay at the forefront of technological innovations,
it's time to prepare yourself for this new way of UI development. And I hope this book
will help you get started with SwiftUI development and build some amazing UIs.

Simon Ng
Founder of AppCoda

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 19


What You Will Learn in This Book
We will dive deep into the SwiftUI framework, teaching you how to work with various UI
elements, and build different types of UIs. After going through the basics and
understanding the usage of common components, we will put together with all the
materials you've learned and build a complete app.

As always, we will explore SwiftUI with you by using the "Learn by doing" approach. This
new book features a lot of hands-on exercises and projects. Don't expect you can just read
the book and understand everything. You need to get prepared to write code and debug.

Audience
This book is written for both beginners and developers with some iOS programming
experience. Even if you have developed an iOS app before, this book will help you
understand this brand-new framework and the new way to develop UI. You will also
learn how to integrate UIKit with SwiftUI.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 20


What You Need to Develop Apps with
SwiftUI
Having a Mac is the basic requirement for iOS development. To use SwiftUI, you need to
have a Mac installed with macOS Catalina and Xcode 11 (or up). That said, to properly
follow the content of this book, you are required to have Xcode 15 installed.

If you are new to iOS app development, Xcode is an integrated development environment
(IDE) provided by Apple. Xcode provides everything you need to kick start your app
development. It already bundles the latest version of the iOS SDK (short for Software
Development Kit), a built-in source code editor, graphic user interface (UI) editor,
debugging tools and much more. Most importantly, Xcode comes with an iPhone (and
iPad) simulator so you can test your app without the real devices. With Xcode 15, you can
instantly preview the result of your SwiftUI code and test it on the fly.

Installing Xcode
To install Xcode, go up to the Mac App Store and download it. Simply search "Xcode" and
click the "Get" button to download it. At the time of this writing, the latest official version
of Xcode is 15.0. Once you complete the installation process, you will find Xcode in the
Launchpad.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 21


Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 22
Frequestly Asked Questions about SwiftUI
I got quite a lot of questions from new comers when the SwiftUI framework was first
announced. These questions are some of the common ones that I want to share with you.
And I hope the answers will give you a better idea about SwiftUI.

1. Do I need to learn Swift before learning SwiftUI?

Yes, you still need to know the Swift programming language before using SwiftUI.
SwiftUI is just a UI framework written in Swift. Here, the keyword is UI, meaning
that the framework is designed for building user interfaces. However, for a complete
application, other than UI, there are many other components such as network
components for connecting to remote server, data components for loading data from
internal database, business logic component for handling the flow of data, etc. All
these components are not built using SwiftUI. So, you should be knowledgeable
about Swift and SwiftUI, as well as, other built-in frameworks (e.g. Map) in order to
build an app.

2. Should I learn SwiftUI or UIKit?

The short answer is Both. That said, it all depends on your goals. If you target to
become a professional iOS developer and apply for a job in iOS development, you
better equip yourself with knowledge of SwiftUI and UIKit. Over 90% of the apps
published on the App Store were built using UIKit. To be considered for hire, you
should be very knowledgeable with UIKit because most companies are still using the
framework to build the app UI. However, like any technological advancement,
companies will gradually adopt SwiftUI in new projects. This is why you need to
learn both to increase your employment opportunities.

On the other hand, if you just want to develop an app for your personal or side
project, you can develop it entirely using SwiftUI. However, since SwiftUI is very
new, it doesn't cover all the UI components that you can find in UIKit. In some
cases, you may also need to integrate UIKit with SwiftUI.

3. Do I need to learn auto layout?

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 23


This may be a good news to some of you. Many beginners find it hard to work with
auto layout. With SwiftUI, you no longer need to define layout constraints. Instead,
you use stacks, spacers, and padding to arrange the layout.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 24


Chapter 1
Introduction to SwiftUI
At WWDC 2019, Apple surprised developers with the announcement of a completely new
framework called SwiftUI. This framework not only changes the way you develop iOS
apps but also represents the most significant shift in the Apple developer ecosystem since
the debut of Swift. SwiftUI is applicable to all Apple platforms, including iPadOS, macOS,
tvOS, and watchOS, making it a game-changer for developers across the ecosystem.

SwiftUI is an innovative, exceptionally simple way to build user interfaces across all
Apple platforms with the power of Swift. Build user interfaces for any Apple device
using just one set of tools and APIs.

- Apple (https://developer.apple.com/xcode/swiftui/)

For a long time, developers have been debating whether to use Storyboards or build the
app UI programmatically. With the introduction of SwiftUI, Apple has provided an
answer to this debate. This brand new framework offers developers a new way to create
user interfaces. Take a look at the figure below and examine the code.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 25


Figure 1. Programming in SwiftUI

With the release of SwiftUI, it's now possible to develop an app's UI with a declarative
Swift syntax in Xcode. This means that the UI code is easier and more natural to write,
and compared to existing UI frameworks like UIKit, you can create the same UI with
much less code.

In the past, the preview function in Xcode has been a weak point, as it was only possible
to preview simple layouts in Interface Builder, and it wasn't possible to preview the
complete UI until the app was loaded onto the simulators. However, with SwiftUI, you
get immediate feedback on the UI you're coding. For example, if you add a new record to
a table, Xcode renders the UI change on the fly in a preview canvas. If you want to
preview how your UI looks in dark mode, you just need to change an option. This instant
preview feature makes UI development a breeze and iteration much faster.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 26


In addition to allowing you to preview the UI, the new canvas also lets you design the
user interface visually using drag-and-drop. Xcode automatically generates the SwiftUI
code as you add UI components visually, ensuring that the code and the UI are always in
sync. This is a feature that Apple developers have been anticipating for a long time.

In this book, you will delve deep into SwiftUI, learning how to lay out built-in
components and create complex UIs with the framework. If you already have experience
in iOS development, let me first walk you through the major differences between the
existing framework you're using (e.g., UIKit) and SwiftUI. However, if you're completely
new to iOS development or have no programming experience, you can use this
information as a reference or even skip the following sections. I don't want to discourage
you from learning SwiftUI; it's an awesome framework for beginners.

Declarative vs Imperative Programming


Swift is an imperative programming language, much like Java, C++, PHP, and C#.
However, SwiftUI is proudly claimed as a declarative UI framework, which allows
developers to create UI in a declarative way. But what does the term "declarative" mean?
How does it differ from imperative programming, and most importantly, how does this
change affect the way you code?

If you're new to programming, you may not need to worry too much about the difference
as everything is new to you. However, if you have some experience in object-oriented
programming or have developed with UIKit before, this paradigm shift can affect how
you think about building user interfaces. You may need to unlearn some old concepts and
relearn new ones.

So, what's the difference between imperative and declarative programming? If you search
for the terms on Wikipedia, you'll find these definitions:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 27


In computer science, imperative programming is a programming paradigm
that uses statements that change a program's state. In much the same way that the
imperative mood in natural languages expresses commands, an imperative
program consists of commands for the computer to perform.

In computer science, declarative programming is a programming paradigm—a


style of building the structure and elements of computer programs—that expresses
the logic of a computation without describing its control flow.

Understanding the difference between imperative and declarative programming can be


challenging if you haven't studied computer science. Let me explain the difference using a
cooking analogy.

Imagine you're instructing someone else (a helper) to prepare a pizza (or any dish you
like). You can either do it imperatively or declaratively. To cook the pizza imperatively,
you would give your helper specific instructions like a recipe:

1. Heat the over to 550°F or higher for at least 30 minutes


2. Prepare one-pound of dough
3. Roll out the dough to make a 10-inch circle
4. Spoon the tomato sauce onto the center of the pizza and spread it out to the edges
5. Place toppings (including onions, sliced mushrooms, pepperoni, cooked sausage,
cooked bacon, diced peppers and cheese) on top of the sauce
6. Bake the pizza for 5 minutes

On the other hand, if you cook pizza in a declarative way, you wouldn't need to specify
step-by-step instructions. Instead, you'd describe how you'd like the pizza cooked. Do you
want a thick or thin crust? Pepperoni and bacon, or just a classic Margherita with tomato
sauce? 10-inch or 16-inch? The helper would figure out the rest and cook the pizza
accordingly.

That's the core difference between imperative and declarative programming. Now, let's
relate this to UI programming. Imperative UI programming requires developers to write
detailed instructions to lay out the UI and control its states. Conversely, declarative UI
programming lets developers describe what the UI looks like and how it should respond
when a state changes.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 28


Using the declarative approach makes code much easier to read and understand. Most
importantly, the SwiftUI framework allows you to write much less code to create a user
interface. For example, let's say you're building a heart button in an app. This button
should be positioned at the center of the screen and able to detect touches. When a user
taps the heart button, its color changes from red to yellow, and when a user taps and
holds the heart, it scales up with an animation.

Figure 2. The implementation of an interactive heart button

Take a look at figure 2. That's the code you need to implement the heart button. In
around 20 lines of code, you create an interactive button with a scale animation. This is
the power of the SwiftUI declarative UI framework.

No more Interface Builder and Auto Layout


Starting from Xcode 11, you have the option to choose between SwiftUI and Storyboard
for building the user interface. If you've built an app before, you may have used Interface
Builder to lay out the UI on the storyboard. With SwiftUI, Interface Builder and

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 29


storyboards are replaced by a code editor and a preview canvas, like the one shown in
Figure 2. You write the code in the code editor, and Xcode renders the user interface in
real-time, displaying it in the canvas.

Figure 3. User interface option in Xcode

Auto layout has always been a challenging topic when learning iOS development.
However, with SwiftUI, you no longer need to learn how to define layout constraints and
resolve conflicts. Instead, you can compose the desired UI using stacks, spacers, and
padding. We will discuss this concept in detail in later chapters.

The Combine Approach

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 30


In addition to storyboards, the view controller is also gone. If you're new to iOS
development, you can ignore what a view controller is. However, if you're an experienced
developer, you may find it strange that SwiftUI doesn't use a view controller as a central
building block for communicating with the view and model.

Instead, communication and data sharing between views are now handled by a new
framework called Combine. This new approach completely replaces the role of the view
controller in UIKit. In this book, we will cover the basics of Combine and how to use it to
handle UI events.

Learn Once, Apply Anywhere


While this book primarily focuses on building UIs for iOS, it's important to note that
everything you learn here can be applied to other Apple platforms, such as watchOS and
macOS. Prior to the launch of SwiftUI, developers relied on platform-specific UI
frameworks to create user interfaces. For instance, UIs for macOS apps were written
using AppKit, while TVUIKit was used for developing tvOS apps, and WatchKit for
watchOS apps.

However, with the introduction of SwiftUI, Apple has provided developers with a unified
UI framework for building user interfaces on all Apple devices. The UI code written for
iOS can be easily adapted to your watchOS/macOS/watchOS app with minimal or no
modifications. This is possible due to the declarative UI framework, where code describes
how the user interface should look. Depending on the platform, the same piece of code in
SwiftUI can result in different UI controls.

For example, the following code snippet demonstrates how to declare a toggle switch:

Toggle(isOn: $isOn) {
Text("Wifi")
.font(.system(.title))
.bold()
}.padding()

For iOS and iPadOS, the toggle is rendered as a switch. On the other hand, SwiftUI
renders the control as a checkbox for macOS.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 31


Figure 4. Toggle on macOS and iOS

The beauty of this unified framework is that you can reuse most of the code on all Apple
platforms without having to make any changes. SwiftUI automatically renders the
corresponding controls and layouts, doing most of the heavy lifting for you.

However, it's important not to consider SwiftUI as a "Write once, run anywhere"
solution. As Apple emphasized in a WWDC talk, that's not the primary goal of SwiftUI.
Therefore, you shouldn't expect to turn a beautiful app designed for iOS into a tvOS app
without any modifications.

There are definitely going to be opportunities to share code along the way, just
where it makes sense. And so we think it's kind of important to think about SwiftUI
less as write once and run anywhere and more like learn once and apply anywhere.

- WWDC Talk (SwiftUI On All Devices)

While the UI code is portable across Apple platforms, you still need to provide
specialization that targets for a particular type of device. You should always review each
edition of your app to make sure the design is right for the platform. That said, SwiftUI
already saves you a lot of time from learning another platform-specific framework, plus
you should be able to reuse most of the code.

Interfacing with UIKit/AppKit/WatchKit


Can I use SwiftUI on my existing projects? I don't want to rewrite the entire app which
was built on UIKit.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 32


SwiftUI is designed to work with the existing frameworks like UIKit for iOS and AppKit
for macOS. Apple offers several representable protocols that you can adopt to wrap a
view or controller into SwiftUI, making it possible to use SwiftUI in your existing
projects.

Figure 5. The Representable protocols for existing UI frameworks

Say, you have a custom view developed using UIKit, you can adopt the
UIViewRepresentable protocol for that view and make it into SwiftUI. Figure 6 shows the
sample code of using WKWebView in SwiftUI.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 33


Figure 6. Porting WKWebView to SwiftUI

Use SwiftUI for Your Next Project


Whenever a new framework is released, developers often wonder whether it's ready for
their next project or if they should wait a little longer. Although SwiftUI is still new to
most developers, now is the right time to learn and incorporate it into your new projects.
With the release of Xcode 15, Apple has made the SwiftUI framework more stable and
feature-rich. If you have personal or side projects for personal use or work, there's no
reason why you shouldn't try out SwiftUI.

However, it's essential to carefully consider whether you should apply SwiftUI to your
commercial projects. One significant drawback of SwiftUI is that the device must run on
at least iOS 13, macOS 10.15, tvOS 13, or watchOS 6. If your app requires support for
lower platform versions (e.g. iOS 12), you may need to wait before adopting SwiftUI.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 34


As of this writing, SwiftUI has been officially released for over four years. The debut of
Xcode 15 has brought new UI controls, such as Grid, and APIs, such as Charts, to
SwiftUI. In terms of features, SwiftUI can't be directly compared to existing UI
frameworks like UIKit, which has been available for years. Some features that are present
in the old framework, such as Camera access, may not be available in SwiftUI, and you
may need to develop workarounds. This is something you have to take into account when
adopting SwiftUI in production projects.

Although SwiftUI is still relatively new, it has already matured into a reliable framework.
It's clear that SwiftUI is the future of UI development for Apple platforms, including
Apple's new visionOS. Even if it isn't yet applicable to your production projects, I
recommend starting a side project and exploring the framework. Once you try out
SwiftUI and understand its benefits, you'll enjoy developing UIs in a declarative way.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 35


Chapter 2
Getting Started with SwiftUI and
Working with Text
If you've worked with UIKit before, you'll find that the Text control in SwiftUI is very
similar to the UILabel in UIKit. It's a view that allows you to display one or multiple lines
of text. This Text control is non-editable but is useful for presenting read-only
information on the screen. For example, if you want to present an on-screen message,
you can use Text to implement it.

In this chapter, we'll show you how to work with Text to present information. You'll also
learn how to customize the text with different colors, fonts, backgrounds, and apply
rotation effects.

Creating a New Project for Playing with SwiftUI


To get started, open Xcode and create a new project using the App template under the
iOS category. If you've used an older version of Xcode before, you may recall the Single
Application template, which is now replaced with the App template.

Select Next to proceed to the next screen and type a name for your project. I've named it
SwiftUIText, but you're free to choose any name you like. For the organization name, you
can use your company or organization's name. The organization identifier is a unique
identifier for your app. Here, I've used com.appcoda, but you should set it to your own
value. If you have a website, use your domain in reverse domain name notation.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 36


Figure 1. Creating a new project

To use SwiftUI, you have to choose SwiftUI in the Interface option. The language should
be set to Swift. Click Next and choose a folder to create the project.

Once you save the project, Xcode should load the ContentView.swift file and display a
design/preview canvas. If you can't see the design canvas, you can go up to the Xcode
menu and choose Editor > Canvas to enable it. To give yourself more space for writing
code, you can hide both the project navigator and the inspector (see figure 2).

By default, Xcode generates some SwiftUI code for ContentView.swift . In Xcode 15, the
preview canvas should automatically render the app preview in a simulator that you
choose in the simulator selection (e.g. iPhone 14/15 Pro). For older version of Xcode, you
may have to click the Resume button in order to see the preview.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 37


Figure 2. The code editor and the canvas

Displaying Simple Text


The sample code generated in ContentView shows you how to display a single line of text
and images. It also uses a VStack to embed the text and image. We will discuss both
images and stack views in later chapters. Now let's focus on the usage of Text first.

To display text on screen, you initialize a Text object and pass to it the text (e.g. Hello
World) to display. Update the code of body like this:

Text("Stay Hungry. Stay Foolish.")

The preview canvas should display the text Stay Hungry. Stay Foolish. on the screen.
This is the basic syntax for creating a text view in SwiftUI. You are free to change the text
to any value you want, and the canvas will update instantaneously to show you the
change.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 38


Figure 3. Changing the text

Changing the Font Type and Color


In SwiftUI, you can change the properties (e.g. color, font, weight) of a control by calling
methods that are known as Modifiers. Let's say, you want to bold the text. You can use
the modifier fontWeight and specify your preferred font weight (e.g. .bold ) like this:

Text("Stay Hungry. Stay Foolish.").fontWeight(.bold)

To access a modifier in SwiftUI, use the dot syntax. As you type a dot, Xcode will display
a list of possible modifiers or values that you can use. For example, when you type a dot
after the fontWeight modifier, you will see various font weight options. You can select
bold to make the text bold. To make it even bolder, you can use heavy or black .

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 39


Figure 4. Choosing your preferred font weight

When you call fontWeight with the value .bold in SwiftUI, it returns a new view with
the text bolded. In SwiftUI, it's possible to chain this new view with other modifiers. For
example, if you want to make the bolded text slightly bigger, you can write the code like
this:

Text("Stay Hungry. Stay Foolish.").fontWeight(.bold).font(.title)

Since we may chain multiple modifiers together, we usually write the code above in the
following format:

Text("Stay Hungry. Stay Foolish.")


.fontWeight(.bold)
.font(.title)

The functionality is the same, but I believe that you will find the code above easier to
read. We will continue to use this coding convention throughout the rest of this book.

In SwiftUI, the font modifier allows you to change the font properties of a text view. In
the code above, we specify the title font style to enlarge the text. SwiftUI provides several
built-in text styles, such as title, largeTitle, body, and others. If you want to further

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 40


increase the font size, you can replace .title with .largeTitle .

Note: You can always to refer the documentation


(https://developer.apple.com/documentation/swiftui/font) to find out all the supported
values of the font modifier.

Figure 5. Changing the font type

You can also use the font modifier to specify the font design. Let's say, if you want the
font to be rounded, you can write the font modifier like this:

.font(.system(.title, design: .rounded))

Here, you specify to use the system font with title text style and rounded design. The
preview canvas should immediately respond to the change and show you the rounded
text.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 41


Figure 6. Using the rounded font design

Dynamic Type is an iOS feature that automatically adjusts the font size based on the
user's setting (Settings > Display & Brightness > Text Size). When you use text styles
(such as .title ), the font size will be adjusted automatically by your app to match the
user's preferred font size. This ensures that the text is always legible and accessible,
regardless of the user's visual acuity.

To use a fixed-size font, you write the code like this:

.font(.system(size: 20))

This tells the system to use a fixed font size of 20 points.

You can chain other modifiers to further customize the text. Let's change the font color.
To do that, you use the foregroundStyle modifier like this:

.foregroundStyle(.green)

The foregroundStyle modifier accepts a value of Color . Here we specify .green , which
is a built-in color. You may use other built-in values like .red , .purple , etc.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 42


Figure 7. Changing the font color

Working with Multiline Text


Text supports multiple lines by default, so it can display a paragraph of text without
using any additional modifiers. Replace your current code with the following:

Text("Your time is limited, so don’t waste it living someone else’s life. Don’t be
trapped by dogma—which is living with the results of other people’s thinking. Don
’t let the noise of others’ opinions drown out your own inner voice. And most impo
rtant, have the courage to follow your heart and intuition.")
.fontWeight(.bold)
.font(.title)
.foregroundStyle(.gray)

You're free to replace the paragraph of text with your own text. Just make sure it's long
enough. Once you have made the change, the design canvas will render a multiline text
label.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 43


Figure 9. Display multiline text

To center align the text, insert the multilineTextAlignment modifier after the .foreground

modifier and set its value to .center like this:

.multilineTextAlignment(.center)

In some cases, you may want to limit the number of lines to a certain number. You use
the lineLimit modifier to control it. Here is an example:

.lineLimit(3)

Another modifier, truncationMode specifies where to truncate the text within the text
view. You can truncate at the beginning, middle, or end of the text view. By default, the
system is set to use tail truncation. To modify the truncation mode of the text, you use the
truncationMode modifier and set its value to .head or .middle like this:

.truncationMode(.head)

After the change, your text should look like the figure below.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 44


Figure 10. Using the .head truncation mode

Earlier, I mentioned that the Text control displays multiple lines by default. The reason
is that the SwiftUI framework has set a default value of nil for the lineLimit modifier.
You can change the value of .lineLimit to nil and see the result:

.lineLimit(nil)

Setting the Padding and Line Spacing


Normally the default line spacing is good enough for most situations. To alter the default
setting, you adjust the line spacing by using the lineSpacing modifier.

.lineSpacing(10)

As you see, the text is too close to the left and right side of the edges. To give it some
more space, you can use the padding modifier, which adds some extra space to each side
of the text. Insert the following line of code after the lineSpacing modifier:

.padding()

Your design canvas should now look like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 45


Figure 11. Setting the padding and line spacing of the text

Rotating the Text


The SwiftUI framework provides a modifier to let you easily rotate the text. You use the
rotateEffect modifier and pass the degree of rotation like this:

.rotationEffect(.degrees(45))

If you insert the above line of code after padding() , you will see the text is rotated by 45
degrees.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 46


Figure 12. Rotate the text

By default, the rotation happens around the center of the text view. If you want to rotate
the text around a specific point (say, the top-left corner), you write the code like this:

.rotationEffect(.degrees(20), anchor: UnitPoint(x: 0, y: 0))

We pass an extra parameter anchor to specify the point of the rotation.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 47


Figure 13. Rotate the text around the top-left of the text view

Not only can you rotate the text in 2D, SwiftUI provides a modifier called
rotation3DEffect that allows you to create some amazing 3D effects. The modifier takes
two parameters: rotation angle and the axis of the rotation. Say, you want to create a
perspective text effect, you write the code like this:

.rotation3DEffect(.degrees(60), axis: (x: 1, y: 0, z: 0))

With just a line of code, you have created the Star Wars perspective text!

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 48


Figure 14. Create amazing text effect by using 3D rotation

You can further insert the following line of code to create a drop shadow effect for the
perspective text:

.shadow(color: .gray, radius: 2, x: 0, y: 15)

The shadow modifier will apply the shadow effect to the text. All you need to do is specify
the color and radius of the shadow. Optionally, you can tell the system the position of the
shadow by specifying the x and y values.

Figure 15. Applying the drop shadow effect

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 49


Using Custom Fonts
By default, all text is displayed using the system font. Say, if you want to use a custom
font that you have found on Google Fonts (e.g.
https://fonts.google.com/specimen/Nunito), how can you use the custom font in the
app?

Assuming you've downloaded the font files, you should first add them to your Xcode
project. You can simply drag the font files to the project navigator and insert them under
the SwiftUIText folder. For the purposes of this demo, I just add the regular font file (i.e.
Nunito-Regular.ttf). If you need to use the bold or italic font, you will need to add the
corresponding font files as well.

Figure 16. Adding the font file to the project

Once you added the font, Xcode will prompt you an option dialog. Please make sure you
enable Copy items if added and check the SwiftUIText target.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 50


Figure 17. Choosing the options for adding files

After adding the font file, it is not immediately usable in the app. Xcode requires
developers to register the font in the project configuration. To do this, select SwiftUIText
in the project navigator and then click on SwiftUIText under Targets. This will bring up
the Info tab, where you can configure the project.

Under the Info tab, scroll down to find the Custom iOS Target Properties section.
Expand the section by clicking the disclosure indicator next to it, and then click on the +

button to add a new row. Set the key name to Fonts provided by application. Expand the
entry by clicking on the disclosure indicator and then set the value for item 0 to Nunito-

Regular.ttf , which is the font file you have just added. If you have added multiple font
files, you can click the + button to add additional items.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 51


Figure 18. Set the font file in the project configuration

Now you can go back to ContentView.swift . To use the custom font, you can replace the
following line of code:

.font(.title)

With:

.font(.custom("Nunito", size: 25))

Rather than using the system font style, the code above uses .custom and specifies the
preferred font name. The font names can be found in the "Font Book" application. To
access Font Book, open Finder and navigate to the "Applications" folder. From there,
click on "Font Book" to launch the application.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 52


Figure 19. Using the custom font

Displaying Markdown Text


Markdown is a lightweight markup language that you can use to add formatting
elements to plaintext text documents. Created by John Gruber in 2004, Markdown
is now one of the world’s most popular markup languages.

SwiftUI has a built-in capability for rendering Markdown. For those unfamiliar with
Markdown, it is a way of styling plain text using a simple and easy-to-read format. If you
would like to learn more about Markdown, you can check out this guide
(https://www.markdownguide.org/getting-started/).

To use Markdown for rendering text in your SwiftUI app, all you need to do is to specify
the text in Markdown format. The Text view will automatically render the text for you.
Here is an example:

Text("**This is how you bold a text**. *This is how you make text italic.* You can
[click this link](https://www.appcoda.com) to go to appcoda.com")
.font(.title)

If you write the code in the ContentView file, you will be able to see how the given text is
rendered. To test the hyperlink, you will need to run the app in the iOS simulator. When
you tap on the link, the iOS device will redirect you to mobile Safari and open the URL.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 53


Figure 20. Using Markdown

Summary
Do you enjoy creating user interfaces with SwiftUI? Hopefully, you do. The declarative
syntax of SwiftUI makes the code more readable and easier to understand. As you have
experienced, it only takes a few lines of code in SwiftUI to create fancy 3D-style text.

For your reference, you can download the complete project for the text example by
following this link:

Demo project (https://www.appcoda.com/resources/swiftui5/SwiftUIText.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 54


Chapter 3
Working with Images and Labels
Now that you have a basic introduction to SwiftUI and understand how to display textual
content, it's time to learn how to display images in your app. In this chapter, we will
explore the usage of Label , one of the most common user interface components, as well
as the Image view for rendering images on screen. Similar to what we did in the previous
chapter, I'll show you how to work with Image by building a simple demo. This chapter
covers the following topics:

What is SF Symbols and how to display a system image


How to display custom images
How to resize an image
How to display a full-screen image using ignoresSafeArea

How to create a circular image


How to apply an overlay to an image

Creating a New Project for Playing with Images


To begin, fire up Xcode and create a new project using the App template (under iOS).
Name the project SwiftUIImage. For the organization name, you can enter your
company or organization name. In this example, com.appcoda is used, but you should set
it to your own value. To use SwiftUI, make sure you select SwiftUI for the User Interface
option. Click Next and choose a folder to create the project.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 55


Figure 1. Creating a new project

After saving the project, Xcode should load the ContentView.swift file and display a
design/preview canvas.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 56


Figure 2. Previewing the generated code

Understanding SF Symbols
With over 5,000 symbols, SF Symbols is a library of iconography designed to
integrate seamlessly with San Francisco, the system font for Apple
platforms.Symbols come in nine weights and three scales, and automatically align
with text. They can be exported and edited using vector graphics editing tools to
create custom symbols with shared design characteristics and accessibility features.
SF Symbols 5 introduces a collection of expressive animations, over 700 new
symbols, and enhanced tools for custom symbols.

Before I show you how to display an image on the screen, let's discuss where the images
come from. You can provide your own images for use in the app, but starting from iOS 13,
Apple introduced a large set of system images called SF Symbols that developers can use
in any apps. With the release of iOS 17, Apple further improved the image set by releasing
SF Symbols 5, which features over 700 new symbols and supports a collection of
expressive animations.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 57


These images are referred to as symbols because they are integrated with the built-in San
Francisco font. To use these symbols, no extra installation is required. As long as your
app is deployed to a device running iOS 13 (or later), you can access these symbols
directly. However, there are now seven different sets of symbols to consider:

SF Symbols v1.1 available in iOS/iPadOS/tvOS/Mac Catalyst 13.0, watchOS 6.0


and macOS 11.0
SF Symbols v2.0 available in iOS/iPadOS/tvOS/Mac Catalyst 14.0, watchOS 7.0
and macOS 11.0
SF Symbols v2.1 available in iOS/iPadOS/tvOS/Mac Catalyst 14.2, watchOS 7.1
and macOS 11.0
SF Symbols v2.2 available in iOS/iPadOS/tvOS/Mac Catalyst 14.5, watchOS 7.4
and macOS 11.3
SF Symbols v3.0 available in iOS/iPadOS/tvOS/Mac Catalyst 15.0, watchOS 8.0
and macOS 12.0
SF Symbols v4.0 available in iOS/iPadOS/tvOS/Mac Catalyst 16.0, watchOS 9.0
and macOS 13.0
SF Symbols v5.0 available in iOS/iPadOS/tvOS/Mac Catalyst 17.0, watchOS 10.0
and macOS 14.0

To use the symbols, all you need is the name of the symbol. With over 5,000 symbols
available for your use, Apple has released an app called SF Symbols
(https://developer.apple.com/sf-symbols/), which allows you to easily explore the
symbols and find the one that fits your needs. It is highly recommended that you install
the app before proceeding to the next section.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 58


Figure 3. SF Symbols App

Displaying a System Image


To display a system image (symbol) on screen, you initialize an Image view with the
systemName parameter like this:

Image(systemName: "cloud.heavyrain")

This will create an image view and load the specified system image. As mentioned before,
SF Symbols are seamlessly integrated with the San Francisco font. You can easily scale
the image by applying the font modifier:

Image(systemName: "cloud.heavyrain")
.font(.system(size: 100))

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 59


Given that the image is part of a font family, you can vary the font size using the size

parameter, as we did in the previous chapter.

Figure 4. Display a system image

Again, since SF Symbols are actually fonts, you can apply modifiers, such as
foregroundStyle , that you learned in the previous chapter, to change their appearance.

For example, if you want to change the symbol's color to blue, you can write the code like
this:

Image(systemName: "cloud.heavyrain")
.font(.system(size: 100))
.foregroundStyle(.blue)

To add a drop shadow effect, you use the shadow modifier:

Image(systemName: "cloud.heavyrain")
.font(.system(size: 100))
.foregroundStyle(.blue)
.shadow(color: .gray, radius: 10, x: 0, y: 10)

Using Your Own Images


Obviously, other than using system images, you will need to use your own images when
building apps. Let's see how you can load your images using the Image view.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 60


Note: You are free to use your own image. However, if you don't have a suitable image
to use, you can download this image (https://unsplash.com/photos/Q0-fOL2nqZc)
from unsplash.com to follow along with the rest of the material. After downloading the
photo, make sure you change the filename to "paris.jpg".

Before you can use an image in your project, you must import the image into the asset
catalog ( Assets ). Assuming you have already prepared the image ( paris.jpg ), press
Command+0 in Xcode to reveal the project navigator, then choose Assets . Open Finder
and drag the image to the outline view. Alternatively, you can right-click the blank area of
the outline view and select Import....

Figure 5. Add the image to the asset catalog

If you're new to iOS app development, the asset catalog is where you store application
resources such as images, colors, and data. Once you add an image to the asset catalog,
you can load the image by referring to its name. Additionally, you can configure on which
device the image can be loaded (e.g., iPhone only).

To display the image on the screen, write the code like this (see figure 6):

Image("paris")

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 61


All you need to do is specify the name of the image, and you should see the image in the
preview canvas. However, since the image is a high-resolution image (4437x6656 pixels),
you will only see a part of the image.

Figure 6. Loading a custom image

Resizing an Image
To resize the image, the resizable modifier is used:

Image("paris")
.resizable()

By default, the image resizes the image using the stretch mode. This means the original
image will be scaled to fill the whole screen (except the top and bottom area).

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 62


Figure 7. Resizing the image with the resizable modifier

Technically speaking, the image fills the whole safe area as defined by iOS. The concept of
the safe area has been around for quite a long time. The safe area is defined as the view
area that is safe to lay out UI components. For example, as you can see in the figure, the
safe area is the view area that excludes the top bar (i.e., status bar) and the bottom bar.
The safe area will prevent you from accidentally hiding system UI components, such as
the status bar, navigation bar, and tab bar.

If you want to display a full-screen image, you can ignore the safe area by setting the
ignoresSafeArea modifier.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 63


Figure 8. Ignoring the safe area

You can also choose to ignore the safe area for a specific edge. To ignore the safe area for
the top edge but keep it for the bottom edge, you can specify the parameter .bottom like
this:

.ignoresSafeArea(.container, edges: .bottom)

Aspect Fit and Aspect Fill


If you look into both images in the previous section and compare it with the original
image, you will find that the aspect ratio is a bit distorted. The stretch mode doesn't take
into account the aspect ratio of the original image. It stretches each side to fit the view
area. To keep the original aspect ratio, you can apply the modifier scaledToFit like this:

Image("paris")
.resizable()
.scaledToFit()

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 64


Figure 9. Scaling the image and keep the original aspect ratio

Alternatively, you can use the aspectRatio modifier and set the content mode to .fit .
This will achieve the same result.

Image("paris")
.resizable()
.aspectRatio(contentMode: .fit)

In some cases you may want to keep the aspect ratio of the image but stretch the image to
as large as possible, to do this, apply the .fill content mode:

Image("paris")
.resizable()
.aspectRatio(contentMode: .fill)

To get a better understanding of the difference between these two modes, Let's limit the
size of the image. The frame modifier allows you to control the size of a view. By setting
the frame's width to 300 points, the image's width will be limited to 300 points.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 65


Figure 10. Limit the width of the image using the frame modifier

Now replace the Image code with the following:

Image("paris")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 300)

The image will be scaled down in size but the original aspect ratio is kept. If you change
the content mode to .fill , the image looks pretty much the same as figure 7. However,
if you switch over to the Selectable mode and look at the image carefully, the aspect ratio
of the original image is maintained.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 66


Figure 11. Using .fill content mode

One thing you may notice is that the image's width still takes up the whole screen width.
To make it scale correctly, you use the clipped modifier to eliminate extra parts of the
view (the left and right edges).

Figure 12. Use .clipped to clip the view

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 67


Creating a Circular Image
Apart from clipping the image in a rectangle shape, SwiftUI offers various other
modifiers that allow you to clip the image into different shapes such as circle, ellipse, and
capsule. To create a circular image, you can use the clipShape modifier in the following
way:

Image("paris")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 300)
.clipShape(Circle())

Alternatively, you can use .circle instead of Circle() :

Image("paris")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 300)
.clipShape(.circle)

Here, we specify to clip the image into a circular shape. You can pass different
parameters to create an image with various shapes. Figure 13 shows you some examples.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 68


Figure 13. Use the .clipShape modifier to create image with different shape

Adjusting the Opacity


SwiftUI comes with a modifier named opacity that you can utilize to control the
transparency of an image (or any view). You can pass a value between 0 and 1 to indicate
the opacity of the image, where a value of 0 means the view is completely invisible, and a
value of 1 indicates the image is fully opaque.

For instance, if you apply the opacity modifier to an image view and set its value to 0.5,
the image will appear partially transparent.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 69


Figure 14. Adjusting the opacity to 50%

Applying an Overlay to an Image


When designing your app, you may require layering another image or text on top of an
image view. To achieve this, SwiftUI offers a modifier called overlay . Developers can use
this modifier to apply an overlay to an image. For example, if you want to overlay a
system image (i.e., heart.fill) on top of an existing image, you can write the following
code:

Image("paris")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 300)
.clipShape(.circle)
.overlay(
Image(systemName: "heart.fill")
.font(.system(size: 50))
.foregroundColor(.black)
.opacity(0.5)
)

The .overlay modifier takes in a View as parameter. In the code above, we create
another image (i.e. heart.fill) and lay it over the existing image (i.e. Paris).

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 70


Figure 15. Applying an overlay to the existing image

In fact, you can apply any view as an overlay. For example, you can overlay a Text view
on the image, like this:

Image("paris")
.resizable()
.aspectRatio(contentMode: .fit)
.overlay(

Text("If you are lucky enough to have lived in Paris as a young man, then
wherever you go for the rest of your life it stays with you, for Paris is a moveab
le feast.\n\n- Ernest Hemingway")
.fontWeight(.heavy)
.font(.system(.headline, design: .rounded))
.foregroundStyle(.white)
.padding()
.background(Color.black)
.cornerRadius(10)
.opacity(0.8)
.padding(),

alignment: .top

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 71


In the overlay modifier, you create a Text view that will be applied as an overlay to the
image. As we have discussed in the previous chapter, you should be familiar with the
modifiers available for the Text view. To change the text, we can adjust the font and its
color. Additionally, we can add some padding and apply a background color.

It's important to note the alignment parameter, which is optional for the overlay

modifier. You can use this parameter to adjust the alignment of the view, which is set to
center by default. In this case, we want to position the text overlay at the top part of the
image. To achieve this, change the value of the alignment parameter from .center to
.top . You can see how this works by making this change to the code.

Figure 16. Applying an overlay to the existing image

Darken an Image Using Overlay


Not only can you overlay an image or text on another image, you can also apply an
overlay to darken an image. To see this effect, replace the existing Image code with the
following:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 72


Image("paris")
.resizable()
.aspectRatio(contentMode: .fit)
.overlay(
Rectangle()
.foregroundStyle(.black)
.opacity(0.4)
)

To apply a darkening effect to an image, we can draw a Rectangle over it and set its
foreground color to black. Then we can set the opacity of the Rectangle to 0.4, giving it a
40% opacity. This will cause the image to appear darker.

Alternatively, we can achieve the same effect by rewriting the code in the following way:

Image("paris")
.resizable()
.aspectRatio(contentMode: .fit)
.overlay(
Color.black
.opacity(0.4)
)

In SwiftUI, Color is also a view, which means we can use it as the top layer to darken the
image underneath. This technique is especially useful if you want to overlay light-colored
text on a bright image to make the text more legible. To achieve this, replace the existing
Image code with the following:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 73


Image("paris")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 300)
.overlay(
Color.black
.opacity(0.4)
.overlay(
Text("Paris")
.font(.largeTitle)
.fontWeight(.black)
.foregroundStyle(.white)
.frame(width: 200)
)
)

As mentioned earlier, the overlay modifier is not limited to Image views, but can be
applied to any other view. In the code above, we use Color.black to darken the image
and then apply an overlay to place a Text view on top of it. If you have made the change
correctly, you should see the word "Paris" in bold white, positioned over the darkened
image.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 74


Figure 17. Darken an image and apply a text overlay

Applying Multicolors to SF Symbols


Starting from iOS 15, SF Symbols offers four rendering modes, providing a range of
options for applying color to symbol. Depending on the mode you choose, you can apply
a single color or multicolours to a symbol. For example, "square.and.arrow.down" is a

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 75


symbol which supports Palette Rendering. You can apply two or more contrasting colors
to the symbol. The figure below shows you how to test palette rendering using the SF
Symbols app.

Figure 18. Using palette rendering in SF Symbols

In SwiftUI, you can attach the symbolRenderingMode modifier to change the mode. To
create the same symbol with multiple colors, you can write the code like this:

Image(systemName: "square.and.arrow.down")
.symbolRenderingMode(.palette)
.foregroundStyle(.indigo, .yellow, .gray)
.font(.system(size: 200))

We specify in the code to use the palette mode and then apply the colors by using the
foregroundStyle modifier.

Variable Colors
SF Symbols also comes with a feature called Variable Color. You can adjust the color of
the symbol by changing a percentage value. This is especially useful when you use some
of the symbols to indicate a progress.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 76


After you open the SF Symbols application, choose the Variable Color category and pick
one of the symbols. In the inspector, you can click the Variable Color button to activate
the feature. As you change the percentage value, the symbol reacts and fills certain parts
accordingly. For instance, consider the slowmo symbol: when the percentage value is set
to 60%, only some of the bars are filled to indicate progress.

Figure 19. Using Variable Color in SF Symbols

Variable Color is compatible with every available rendering mode in SF Symbols, and you
can switch between modes to observe the various effects.

To set the percentage value programmatically, you can instantiate the Image view with an
additional variableValue parameter and pass the desired percentage value as follows:

Image(systemName: "slowmo", variableValue: 0.6)


.symbolRenderingMode(.palette)
.foregroundStyle(.indigo)
.font(.largeTitle)

Wrap Up
In this chapter, I showed you how to work with images in SwiftUI, highlighting how
developers can easily display images and apply various modifiers to achieve desired
image effects. The introduction of SF Symbols is particularly advantageous for indie

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 77


developers, as it saves you a lot of time from searching third-party icons!

To access the sample project, please download it from the following link:

Demo project (https://www.appcoda.com/resources/swiftui5/SwiftUIImage.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 78


Chapter 4
Layout User Interface with Stacks
Stacks in SwiftUI are similar to stack views in UIKit. By combining views in horizontal
and vertical stacks, you can construct complex user interfaces for your apps. In UIKit, it
is necessary to use auto layout to build interfaces that fit all screen sizes. However, auto
layout can be a complicated subject and difficult to learn for beginners. The good news is
that in SwiftUI, you no longer need to use auto layout. Everything, including VStack,
HStack, and ZStack, is a stack.

In this chapter, I will guide you through all types of stacks and show you how to build a
grid layout using stacks. Our project will involve laying out a simple grid interface step by
step. By the end of this chapter, you will be able to effectively combine views with stacks
and build the UI you desire.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 79


Figure 1. The demo app

Understanding VStack, HStack, and ZStack


SwiftUI offers three types of stacks for developers to combine views in different
orientations. Depending on how you want to arrange the views, you can use:

HStack - arranges views horizontally


VStack - arranges views vertically
ZStack - overlays one view on top of another

The figure below demonstrates how these stacks can be used to organize views.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 80


Figure 2. Different types of stack views

Creating a New Project with SwiftUI enabled


To begin, launch Xcode and create a new project using the App template under the iOS
tab. On the following screen, enter a name for your project. In this example, I have
named it SwiftUIStacks, but feel free to choose any other name you prefer. Ensure that
you select the SwiftUI option for the interface.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 81


Figure 3. Creating a new project

Once you save the project, Xcode will load the ContentView.swift file and display a
preview in the design canvas.

Using VStack
We're going to build the UI as displayed in figure 1. To accomplish this, we will first
deconstruct the UI into smaller components. Let's start with the heading, as illustrated
below.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 82


Figure 4. The heading

At this point, Xcode should have already generated the following code to display the
"Hello, World!" label:

struct ContentView: View {


var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
.padding()
}
}

To display the text as shown in figure 4, we will combine two Text views within a
VStack like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 83


struct ContentView: View {
var body: some View {
VStack {
Text("Choose")
.font(.system(.largeTitle, design: .rounded))
.fontWeight(.black)
Text("Your Plan")
.font(.system(.largeTitle, design: .rounded))
.fontWeight(.black)
}
}
}

When you embed views in a VStack , the views will be arranged vertically like this:

Figure 5. Combining two texts using VStack

By default, the views embedded in the stack are centered. To align both views to the left,
you can specify the alignment parameter and set its value to .leading , like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 84


VStack(alignment: .leading, spacing: 2) {
Text("Choose")
.font(.system(.largeTitle, design: .rounded))
.fontWeight(.black)
Text("Your Plan")
.font(.system(.largeTitle, design: .rounded))
.fontWeight(.black)
}

You can also adjust the spacing between embedded views by using the spacing

parameter. In the code above, we added the spacing parameter to the VStack and set its
value to 2 . The resulting view is shown below:

Figure 6. Changing the alignment of VStack

Using HStack

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 85


Next, let's layout the first two pricing plans. If you look at the Basic and Pro plans, you'll
notice that the look and feel of these two components are very similar. Let's take the
Basic plan as an example. To achieve the desired layout, you can use a VStack to
combine three text views.

Figure 7. Layout the pricing plans

Both the Basic and Pro components are arranged side by side. To lay out views
horizontally, we can use HStack . Stacks can be nested, which means you can have stack
views within other stack views. Since the pricing plan block sits right below the heading
view, which is a VStack , we can use another VStack to embed a vertical stack (i.e.
Choose Your Plan) and a horizontal stack (i.e. the pricing plan block).

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 86


Figure 8. Using a VStack to embed other stack views

Now that you have a general idea of how we'll be using VStack and HStack to implement
the UI, let's dive into the code.

To embed an existing VStack in another VStack , hold down the control key and click the
VStack keyword. This will bring up a context menu showing all the available options.
Choose Embed in VStack to embed the VStack .

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 87


Figure 9. Embed in VStack

Xcode will then generate the necessary code to embed the stack. Your code should now
resemble the following:

struct ContentView: View {


var body: some View {
VStack {
VStack(alignment: .leading, spacing: 2) {
Text("Choose")
.font(.system(.largeTitle, design: .rounded))
.fontWeight(.black)
Text("Your Plan")
.font(.system(.largeTitle, design: .rounded))
.fontWeight(.black)
}
}
}
}

Extracting a View

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 88


Before we proceed with laying out the UI, let me show you a tip for organizing your code.
As you build a more complex UI that involves several components, the code inside
ContentView will eventually become a large code block that is difficult to review and
troubleshoot. It's always a good practice to break large blocks of code into smaller, more
manageable blocks, making the code easier to read and maintain.

Xcode has a built-in feature for refactoring SwiftUI code. To extract code from the
VStack that holds the text views (i.e., line 13), hold down the control key and click on the
VStack . Select Extract Subview to extract the code.

Figure 10. Extract subview

Xcode will extract the code block and create a default struct named ExtractedView . To
give it a more meaningful name, rename ExtractedView to HeaderView . See the figure
below for details:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 89


Figure 11. Extract subview

The UI remains the same. However, if you look at the code block in ContentView , it is now
much cleaner and easier to read.

Let's continue implementing the UI for the pricing plans. We'll start by creating the UI
for the Basic plan. Update ContentView as follows:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 90


struct ContentView: View {
var body: some View {
VStack {
HeaderView()

VStack {
Text("Basic")
.font(.system(.title, design: .rounded))
.fontWeight(.black)
.foregroundColor(.white)
Text("$9")
.font(.system(size: 40, weight: .heavy, design: .rounded))
.foregroundColor(.white)
Text("per month")
.font(.headline)
.foregroundColor(.white)
}
.padding(40)
.background(Color.purple)
.cornerRadius(10)
}
}
}

Here we add another VStack under HeaderView . This VStack holds three text views to
display the Basic plan. We won't go into detail about padding , background , and
cornerRadius because we have already covered these modifiers in earlier chapters.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 91


Figure 12. The Basic Plan

Next, we'll implement the UI for the Pro plan. This Pro plan should be placed directly to
the right of the Basic plan. To do this, you need to embed the VStack of the Basic plan in
an HStack . Hold down the control key and click on the VStack keyword. Choose Embed
in HStack.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 92


Figure 13. Embed in HStack

Xcode should insert the code for the HStack and embed the selected VStack in the
horizontal stack, like this:

HStack {
VStack {
Text("Basic")
.font(.system(.title, design: .rounded))
.fontWeight(.black)
.foregroundColor(.white)
Text("$9")
.font(.system(size: 40, weight: .heavy, design: .rounded))
.foregroundColor(.white)
Text("per month")
.font(.headline)
.foregroundColor(.white)
}
.padding(40)
.background(Color.purple)
.cornerRadius(10)
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 93


Now we're ready to create the UI for the Pro plan. The code is very similar to that of the
Basic plan, except for the background and text colors. Insert the following code right
below cornerRadius(10) to implement the Pro plan UI:

VStack {
Text("Pro")
.font(.system(.title, design: .rounded))
.fontWeight(.black)
Text("$19")
.font(.system(size: 40, weight: .heavy, design: .rounded))
Text("per month")
.font(.headline)
.foregroundColor(.gray)
}
.padding(40)
.background(Color(red: 240/255, green: 240/255, blue: 240/255))
.cornerRadius(10)

As soon as you insert the code, you should see the layout below in the canvas.

Figure 14. Using HStack to layout two views horizontally

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 94


Currently, the size of the pricing blocks appears similar, but each block will adjust itself
when the length of the text changes. For example, if you update the word "Pro" to
"Professional", the gray area will expand to accommodate the change. In short, the view
defines its own size, and its size is just big enough to fit the content.

Figure 15. The size of the Pro block becomes wider

If you refer to figure 1 again, both pricing blocks have the same size. To adjust both
blocks to have the same size, you can use the .frame modifier to set the maxWidth to
.infinity like this:

Edited:

If you refer back to Figure 1, you'll notice that both pricing blocks have the same size. To
adjust both blocks to have the same size, you can use the .frame modifier to set the
maxWidth to .infinity , like this:

.frame(minWidth: 0, maxWidth: .infinity, minHeight: 100)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 95


The .frame modifier allows you to define the frame size. You can specify the size as a
fixed value. For example, in the code above, we set the minHeight to 100 points. When
you set the maxWidth to .infinity , the view will adjust itself to fill the maximum width.
For example, if there is only one pricing block, it will take up the whole screen width.

Figure 16. Setting the maxWidth to .infinity

When maxWidth is set to .infinity , iOS will fill the blocks equally for two pricing blocks.
Now insert the above line of code into each of the pricing blocks. Your result should
resemble Figure 17.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 96


Figure 17. Arranging both pricing blocks with equal width

To add some spacing between the horizontal stack and the VStack above it, you can use
the .padding modifier, like this:

Figure 18. Adding some paddings for the stack view

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 97


The .horizontal parameter means we want to add padding to both the leading and
trailing sides of the HStack .

Organizing the Code


Again, before we lay out the rest of the UI components, let's refactor the current code to
make it more organized. If you look at both stacks used to lay out the Basic and Pro
pricing plans, the code is very similar except for the following items:

the name of the pricing plan


the price
the text color
the background color of the pricing block

To streamline the code and improve reusability, we can extract the VStack code block
and make it adaptable to different values of the pricing plan.

Go back to the code editor. Hold down the control key and click on the VStack of the
Basic plan. Once Xcode extracts the code, rename the subview from ExtractedView to
PricingView .

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 98


Figure 19. Extracting the subview

As previously stated, the PricingView should be flexible enough to display different


pricing plans. We will add four variables to the PricingView struct. Update PricingView

as follows:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 99


struct PricingView: View {

var title: String


var price: String
var textColor: Color
var bgColor: Color

var body: some View {


VStack {
Text(title)
.font(.system(.title, design: .rounded))
.fontWeight(.black)
.foregroundColor(textColor)
Text(price)
.font(.system(size: 40, weight: .heavy, design: .rounded))
.foregroundColor(textColor)
Text("per month")
.font(.headline)
.foregroundColor(textColor)
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 100)
.padding(40)
.background(bgColor)
.cornerRadius(10)
}
}

We added variables for the title, price, text, and background color of the pricing block.
Furthermore, we used these variables in the code to update the title, price, text, and
background color accordingly.

Once you make the changes, you'll see an error indicating that there are some missing
arguments for the PricingView .

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 100


Figure 20. Xcode indicates an error on the PricingView

Earlier, we introduced four variables in the PricingView . When calling PricingView , we


must provide the values for these parameters. So, update the initialization of
PricingView() and add the parameters, like this:

PricingView(title: "Basic", price: "$9", textColor: .white, bgColor: .purple)

Also, you can replace the VStack of the Pro plan using PricingView like this:

PricingView(title: "Pro", price: "$19", textColor: .black, bgColor: Color(red: 240/


255, green: 240/255, blue: 240/255))

The layout of the pricing blocks is the same but the underlying code, as you can see, is
much cleaner and easier to read.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 101


Figure 21. ContentView after refactoring the code

Using ZStack
Now that you've laid out the pricing blocks and refactored the code, there is still one
thing missing for the Pro pricing block. We want to overlay a message in yellow on the
pricing block. To do that, we can use the ZStack view, which allows you to overlay a view
on top of an existing view.

Embed the PricingView of the Pro plan within a ZStack and add the Text view, like
this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 102


ZStack {
PricingView(title: "Pro", price: "$19", textColor: .black, bgColor: Color(red:
240/255, green: 240/255, blue: 240/255))

Text("Best for designer")


.font(.system(.caption, design: .rounded))
.fontWeight(.bold)
.foregroundColor(.white)
.padding(5)
.background(Color(red: 255/255, green: 183/255, blue: 37/255))
}

The order of the views embedded in the ZStack determines how the views are overlaid
with each other. For the code above, the Text view will overlay on top of the pricing
view. On the canvas, you should see the pricing layout like this:

Figure 22. ContentView after refactoring the code

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 103


To adjust the position of the text, you can use the .offset modifier. Insert the following
line of code at the end of the Text view (see figure 23):

.offset(x: 0, y: 87)

The Best for designer label will move to the bottom of the block. A negative value of y

will move the label to the top part of the block if you want to re-position it.

Figure 23. Position the text view using .offset

Optionally, if you want to adjust the spacing between the Basic and Pro pricing blocks,
you can specify the spacing parameter within the HStack , like this:

HStack(spacing: 15) {
...
}

Exercise #1

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 104


We haven't finished yet. Before we proceed, let's have a simple exercise. Your task is to
lay out the Team pricing plan as shown in Figure 24. The image used is a system image
with the name "wand.and.rays" from SF Symbols.

Additionally, I want to discuss how we handle optionals in SwiftUI and introduce another
view component called Spacer .

Figure 24. Adding the Team plan

Please don't look at the solution yet, try to develop your own solution.

Handling Optionals in SwiftUI


Have you tried the exercise and come up with your own solution? The layout of the Team
plan is very similar to the Basic & Pro plans. You could replicate the VStack of these two
plans and create the Team plan. However, let me show you a more elegant solution.

We can reuse the PricingView to create the Team plan. However, as you are aware, the
Team plan has an icon that sits above the title. In order to lay out this icon, we need to
modify PricingView to accomodate an icon. Since the icon is not mandatory for a pricing

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 105


plan, we declare an optional in PricingView :

var icon: String?

If you're new to Swift, an optional means that the variable may or may not have a value.
In the example above, we defined a variable named icon of type String? , which means
it's an optional string. The call to the method is expected to pass the image name if the
pricing plan is required to display an icon. Otherwise, this variable is set to nil by
default.

So, how do you handle an optional in SwiftUI? In Swift, there are a couple of ways to
unwrap an optional. One way is to check if the optional has a non-nil value and then
unwrap the value by using the exclamation mark. For example, we need to check if icon

has a value before displaying an image. We can write the code like this:

if icon != nil {

Image(systemName: icon!)
.font(.largeTitle)
.foregroundColor(textColor)

A better and more common way to handle optional is to use if let . The same piece of
code can be rewritten like this:

if let icon = icon {

Image(systemName: icon)
.font(.largeTitle)
.foregroundColor(textColor)

To support the rendering of an icon, the final code of PricingView should be updated as
below:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 106


struct PricingView: View {

var title: String


var price: String
var textColor: Color
var bgColor: Color
var icon: String?

var body: some View {


VStack {

if let icon = icon {

Image(systemName: icon)
.font(.largeTitle)
.foregroundColor(textColor)

Text(title)
.font(.system(.title, design: .rounded))
.fontWeight(.black)
.foregroundColor(textColor)
Text(price)
.font(.system(size: 40, weight: .heavy, design: .rounded))
.foregroundColor(textColor)
Text("per month")
.font(.headline)
.foregroundColor(textColor)
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 100)
.padding(40)
.background(bgColor)
.cornerRadius(10)
}
}

Once you made this change, you can create the Team plan by using ZStack and
PricingView . You put the code in ContentView and insert it after .padding(.horiontal) :

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 107


ZStack {
PricingView(title: "Team", price: "$299", textColor: .white, bgColor: Color(re
d: 62/255, green: 63/255, blue: 70/255), icon: "wand.and.rays")
.padding()

Text("Perfect for teams with 20 members")


.font(.system(.caption, design: .rounded))
.fontWeight(.bold)
.foregroundColor(.white)
.padding(5)
.background(Color(red: 255/255, green: 183/255, blue: 37/255))
.offset(x: 0, y: 110)
}

Using Spacer
When comparing your current UI with that of Figure 1, you may notice a couple of
differences:

1. The Choose Your Plan label is not left-aligned.


2. The Choose Your Plan label and the pricing plans are not aligned to the top of the
screen.

In UIKit, you would define auto layout constraints to position the views. However,
SwiftUI doesn't have auto layout. Instead, it provides a view called Spacer for you to
create complex layouts.

A flexible space that expands along the major axis of its containing stack layout, or
on both axes if not contained in a stack.

- SwiftUI documentation
(https://developer.apple.com/documentation/swiftui/spacer)

To fix the left alignment, let's update the HeaderView like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 108


struct HeaderView: View {
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 2) {
Text("Choose")
.font(.system(.largeTitle, design: .rounded))
.fontWeight(.black)
Text("Your Plan")
.font(.system(.largeTitle, design: .rounded))
.fontWeight(.black)
}

Spacer()
}
.padding()
}
}

Here we embed the original VStack and add a Spacer within a HStack . By using a
Spacer , we push the VStack to the left. Figure 25 illustrates how the spacer works.

Figure 25. Using Spacer in HStack

You may now know how to fix the second difference. The solution is to add a spacer at the
end of the VStack of ContentView like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 109


struct ContentView: View {
var body: some View {
VStack {
HeaderView()

HStack(spacing: 15) {
...
}
.padding(.horizontal)

ZStack {
...
}

// Add a spacer
Spacer()
}
}
}

Figure 26 illustrates how the spacer works visually.

Figure 26. Using spacer in VStack

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 110


Exercise #2
Now that you have familiarized yourself with the functionalities of VStack , HStack , and
ZStack , your final exercise is to create a layout resembling the one depicted in Figure 28.
For the icons, I suggest utilizing system images from SF Symbols. Feel free to choose
different images that align with your preferences, rather than strictly adhering to the
ones I used. As a helpful tip, you can utilize the .scale modifier to adjust the scale of a
view. For instance, by applying .scale(0.5) to a view, it automatically resizes the view to
half of its original size.

Figure 28. Your exercise

To access the sample project, please download it from the following link:

Demo project (https://www.appcoda.com/resources/swiftui5/SwiftUIStacks.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 111


Solution to exercise #2
(https://www.appcoda.com/resources/swiftui5/SwiftUIStacksExercise.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 112


Chapter 5
Understanding ScrollView and
Building a Carousel UI
After going through the previous chapter, I believe you should now understand how to
build a complex UI using stacks. Of course, it will take you a lot of practice before you can
master SwiftUI. Therefore, before delving into ScrollView to make the views scrollable,
let's begin this chapter with a challenge. Your task is to create a card view similar to the
one depicted in Figure 1.

Figure 1. The card view

By utilizing stacks, image views, and text views, you should be able to construct the
desired UI. While I'll guide you through the implementation step by step later on, I
encourage you to take some time to tackle the exercise and devise your own solution.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 113


Once you create the card view, I will discuss ScrollView with you and build a scrollable
interface using the card view. Figure 2 shows you the complete UIs.

Figure 2. Building a scrollable UI with ScrollView

Creating a Card-like UI
If you haven't opened Xcode, fire it up and create a new project using the App template
(under iOS). In the next screen, set the product name to SwiftUIScrollView (or whatever
name you like) and fill in all the required values. Make sure you select SwiftUI for the
Interface option.

So far, we have coded the user interface in the ContentView.swift file. It's very likely you
wrote your solution there. That's completely fine, however, I would like to introduce a
better way to organize your code. For the implementation of the card view, let's create a

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 114


separate file. In the project navigator, right click SwiftUIScrollView and choose New
File...

Figure 3. Creating a new file

In the User Interface section, choose the SwiftUI View template and click Next to create
the file. Name the file CardView and save it in the project folder.

Figure 4. Choose the SwiftUI View template

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 115


The code in CardView.swift looks very similar to that of ContentView.swift . Similarly, you
can preview the UI in the canvas.

Figure 5. Just like ContentView.swift, you can preview CardView.swift in the canvas

Preparing the Image Files


Now we're ready to code the card view. But first, you need to prepare the image files and
import them in the asset catalog. If you don't want to prepare your own images, you can
download the sample images from
https://www.appcoda.com/resources/swiftui/SwiftUIScrollViewImages.zip. Once you
unzip the image archive, select Assets and drag all the images to the asset catalog.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 116


Figure 6. Adding the image files to the asset catalog

Implementing the Card View


Now switch back to the CardView.swift file. If you look at figure 1 again, the card view is
composed of two parts: the upper part contains the image, while the lower part contains
the text description.

Let's start with the image section. I'll make the image resizable and scale it to fit the
screen while retaining the aspect ratio. You write the code like this:

struct CardView: View {


var body: some View {
Image("swiftui-button")
.resizable()
.aspectRatio(contentMode: .fit)
}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 117


If you forgot what these two modifiers do, go back and read the chapter about the Image

view. Next, let's implement the text description. You may write the code like this:

VStack(alignment: .leading) {
Text("SwiftUI")
.font(.headline)
.foregroundColor(.secondary)
Text("Drawing a Border with Rounded Corners")
.font(.title)
.fontWeight(.black)
.foregroundColor(.primary)
.lineLimit(3)
Text("Written by Simon Ng".uppercased())
.font(.caption)
.foregroundColor(.secondary)
}

You need to use Text to create the text view. Since we have three text views in the
description, which are vertically aligned, we can use a VStack to encapsulate them. For
the VStack , we specify the alignment as .leading , which aligns the text views to the left
within the stack.

The modifiers available for the Text object have all been discussed in the chapter
dedicated to it. You can refer back to that chapter if you find any of the modifiers
confusing. However, one important topic to highlight is the use of .primary and
.secondary colors.

While you can manually specify standard colors such as .black and .purple using the
foregroundColor modifier, iOS provides a set of system colors that include primary,
secondary, and tertiary variants. By utilizing these color variants, your app can easily
support both light and dark modes. For instance, the primary color of the text view is set
to black by default in light mode. When the app switches to dark mode, the primary color
will automatically adjust to white. This behavior is handled automatically by iOS,
eliminating the need to write extra code to support dark mode. We will delve deeper into
dark mode in a later chapter.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 118


To arrange the image and these text views vertically, we use a VStack to embed them.
The current layout is depicted in the figure below.

Figure 7. Embed the image and text views in a VStack

We are not yet finished! There are still a couple of things we need to implement. First, we
should ensure that the text description block is left-aligned to the edge of the image.

How do you do that?

Based on what we've learned, we can embed the VStack of the text views in a HStack .
And then, we will use a Spacer to push the VStack to the left. Let's see if this works.

If you have updated the code to match the one depicted in Figure 8, the VStack of text
views should now be aligned to the left edge of the screen.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 119


Figure 8. Aligning the text description

It would be better to add some padding around the HStack . Insert the padding modifier
like this (line 34 in figure 9) :

Figure 9. Adding some paddings for the text description

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 120


Lastly, it's the border. We have discussed how to draw a border with rounded corners in
an earlier chapter. We use the overlay modifier and draw the border using
RoundedRectangle . Here is the complete code:

struct CardView: View {


var body: some View {
VStack {
Image("swiftui-button")
.resizable()
.aspectRatio(contentMode: .fit)

HStack {
VStack(alignment: .leading) {
Text("SwiftUI")
.font(.headline)
.foregroundColor(.secondary)
Text("Drawing a Border with Rounded Corners")
.font(.title)
.fontWeight(.black)
.foregroundColor(.primary)
.lineLimit(3)
Text("Written by Simon Ng".uppercased())
.font(.caption)
.foregroundColor(.secondary)
}

Spacer()

}
.padding()
}
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color(.sRGB, red: 150/255, green: 150/255, blue: 150/255,
opacity: 0.1), lineWidth: 1)
)
.padding([.top, .horizontal])
}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 121


In addition to the border, we have also applied padding to the top, left, and right sides.
With these modifications, the card view layout is now complete.

Figure 10. Adding a border and rounded corners

Make the Card View more Flexible


While the card view currently functions as intended, we have hardcoded the image and
text values. To enhance its flexibility, let's refactor the code by declaring variables for the
image, category, heading, and author within the CardView structure:

var image: String


var category: String
var heading: String
var author: String

Next, replace the values of the Image and Text views with the variables like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 122


VStack {
Image(image)
.resizable()
.aspectRatio(contentMode: .fit)

HStack {
VStack(alignment: .leading) {
Text(category)
.font(.headline)
.foregroundColor(.secondary)
Text(heading)
.font(.title)
.fontWeight(.black)
.foregroundColor(.primary)
.lineLimit(3)
Text("Written by \(author)".uppercased())
.font(.caption)
.foregroundColor(.secondary)
}

Spacer()
}
.padding()
}

Once you made the changes, you will see an error in #Preview . This is because we've
introduced some variables in CardView . We have to specify the parameters when using it.

Figure 11. Missing parameters when calling the CardView

Modify the code like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 123


#Preview {
CardView(image: "swiftui-button", category: "SwiftUI", heading: "Drawing a Bor
der with Rounded Corners", author: "Simon Ng")
}

This modification should resolve the error. Great job! You have successfully built a
flexible CardView that can accommodate various images and text inputs.

Introducing ScrollView
Please take another look at Figure 2. That is the user interface we are aiming to
implement. Initially, one might assume that we can embed four card views using a
VStack . To proceed, you can switch to the ContentView.swift file and insert the following
code snippet:

VStack {
CardView(image: "swiftui-button", category: "SwiftUI", heading: "Drawing a Bor
der with Rounded Corners", author: "Simon Ng")
CardView(image: "macos-programming", category: "macOS", heading: "Building a S
imple Editing App", author: "Gabriel Theodoropoulos")
CardView(image: "flutter-app", category: "Flutter", heading: "Building a Compl
ex Layout with Flutter", author: "Lawrence Tan")
CardView(image: "natural-language-api", category: "iOS", heading: "What's New
in Natural Language API", author: "Sai Kambampati")
}

If you did that, the card views will be squeezed to fit the screen because VStack is non-
scrollable, just like that shown in figure 12.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 124


Figure 12. Embedding the card views in a VStack

To support scrollable content, SwiftUI provides a view called ScrollView . When the
content is wrapped within a ScrollView , it becomes scrollable. To implement this, you
simply need to enclose the VStack containing the card views within a ScrollView . By
doing so, the views will become scrollable. In the preview canvas, you can drag the views
to scroll through the content.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 125


Figure 13. Using ScrollView

Exercise #1
Your task is to add a header to the existing scroll view. The result is displayed in figure 14.
If you understand VStack and HStack thoroughly, you should be able to create the
layout.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 126


Figure 14. Exercise #1

Creating a Carousel UI with Horizontal ScrollView


By default, the ScrollView allows you to scroll the content in a vertical orientation.
However, it also supports scrollable content in a horizontal orientation. Let's explore how
to transform the current layout into a carousel UI with a few changes.

Update the ContentView code like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 127


struct ContentView: View {
var body: some View {

ScrollView(.horizontal) {

// Your code for exercise #1

HStack {
CardView(image: "swiftui-button", category: "SwiftUI", heading: "D
rawing a Border with Rounded Corners", author: "Simon Ng")
.frame(width: 300)
CardView(image: "macos-programming", category: "macOS", heading: "
Building a Simple Editing App", author: "Gabriel Theodoropoulos")
.frame(width: 300)
CardView(image: "flutter-app", category: "Flutter", heading: "Buil
ding a Complex Layout with Flutter", author: "Lawrence Tan")
.frame(width: 300)
CardView(image: "natural-language-api", category: "iOS", heading:
"What's New in Natural Language API", author: "Sai Kambampati")
.frame(width: 300)
}
}

}
}

In the code snippet above, we have made three modifications:

1. We specify that the ScrollView should use a horizontal scroll view by passing the
.horizontal value.
2. Since we are using a horizontal scroll view, we also need to change the stack view
from VStack to HStack .
3. For each card view, we set the frame's width to 300 points. This adjustment is
necessary because the image is too wide to fit within the default width.

After making these code changes, you will notice that the card views are now arranged
horizontally, and they are scrollable in a carousel-like manner.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 128


Figure 15. Carousel UI

Hiding the Scroll Indicator


While you're scrolling the views, did you notice there is a scroll indicator near the bottom
of the screen? This indicator is displayed by default. If you want to hide it, you can
change the ScrollView by adding showsIndicators: false to it:

ScrollView(.horizontal, showsIndicators: false)

By setting showIndicators to false , iOS will no longer show the indicator.

Grouping View Content


If you look at the code again, you will see that all the CardView s have a .frame modifier
to limit their width to 300 points. Is there a way we can simplify this code and eliminate
the duplication? Fortunately, SwiftUI provides the Group view for developers to group
related content. More importantly, you can attach modifiers to the group to apply the
changes to each embedded view.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 129


To achieve the same result while eliminating code duplication, you can rewrite the
HStack code as follows:

HStack {
Group {
CardView(image: "swiftui-button", category: "SwiftUI", heading: "Drawing a
Border with Rounded Corners", author: "Simon Ng")
CardView(image: "macos-programming", category: "macOS", heading: "Building
a Simple Editing App", author: "Gabriel Theodoropoulos")
CardView(image: "flutter-app", category: "Flutter", heading: "Building a C
omplex Layout with Flutter", author: "Lawrence Tan")
CardView(image: "natural-language-api", category: "iOS", heading: "What's
New in Natural Language API", author: "Sai Kambampati")
}
.frame(width: 300)
}

Resize the Text Automatically


As you can see in figure 15, the title of the first card is truncated. How do you fix this? In
SwiftUI, you can use the .minimumScaleFactor modifier to automatically downscale text.
Switch over to CardView.swift and attach the following modifier to Text(heading) :

.minimumScaleFactor(0.5)

SwiftUI will automatically scale down the text to fit the available space. The value sets the
minimum amount of scaling that the view permits. In this case, SwiftUI can draw the text
in a font size as small as 50% of the original font size.

Exercise #2
Here comes the final exercise. Modify the existing code and rearrange it to match the
layout shown in Figure 16. Please ensure that the title and date remain visible to users
when they scroll through the card views.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 130


Figure 16. Aligning the views to the top

For reference, you can download the complete project from the following link:

Demo project
(https://www.appcoda.com/resources/swiftui5/SwiftUIScrollView.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 131


Chapter 6
Working with SwiftUI Buttons, Labels
and Gradient
Buttons initiate app-specific actions, have customizable backgrounds, and can
include a title or an icon. The system provides a number of predefined button styles
for most use cases. You can also design fully custom buttons.

- Apple's documentation (https://developer.apple.com/design/human-interface-


guidelines/ios/controls/buttons/)

I don't think I need to explain what a button is, as it is a fundamental UI control present
in all applications. A button has the ability to handle users' touch, triggering a specific
action. If you are already familiar with iOS programming, you will find that the Button

in SwiftUI bears a striking resemblance to UIButton in UIKit. However, SwiftUI's


Button offers greater flexibility and customization options. You will soon grasp the
significance of this distinction.

Throughout this chapter, we will delve into SwiftUI's button control, covering the
following techniques:

1. Creating a simple button and handling user selection


2. Customizing the button's background, padding, and font
3. Adding borders to a button
4. Designing a button that combines both text and an image
5. Constructing a button with a gradient background and shadows
6. Crafting a full-width button
7. Creating a reusable button style
8. Implementing a tap animation

By the end of this chapter, you will have gained a comprehensive understanding of
SwiftUI's button capabilities and the various techniques associated with them.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 132


Creating a New Project with SwiftUI enabled
Let's begin by creating a basic button using SwiftUI. To get started, open Xcode and
create a new project using the App template. In the subsequent screen, enter the desired
project name. I chose SwiftUIButton as an example, but feel free to select any other name
you prefer. It's crucial to ensure that you choose SwiftUI for the Interface option.

Figure 1. Creating a new project

Once you save the project, Xcode should load the ContentView.swift file and display a
preview.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 133


Figure 2. Previewing the default content view

It's very easy to create a button using SwiftUI. Basically, you use the code snippet below
to create a button:

Button {
// What to perform
} label: {
// How the button looks like
}

When creating a button, you need to provide two code blocks:

1. What action to perform - the code to perform after the button is tapped or
selected by the user.
2. How the button looks - the code block that describes the look & feel of the button.

For example, if you just want to turn the Hello World label into a button, you can update
the code like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 134


struct ContentView: View {
var body: some View {
Button {
print("Hello World tapped!")
} label: {
Text("Hello World")
}
}
}

As a side note, you may write the code like this:

struct ContentView: View {


var body: some View {
Button(action: {
print("Hello World tapped!")
}) label: {
Text("Hello World")
}
}
}

Both code snippets are functionally identical; the difference lies in coding style. In this
book, we prefer using the first approach.

Upon implementing a button, the Hello World text transforms into a tappable button, as
you can see in the canvas.

Figure 3. Creating a simple button

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 135


The print statement outputs the message to the console. To bring up the console, you
can click the button at the lower-right corner of Xcode. Alternatively, you can go up to the
Xcode menu and choose View > Debug Area > Activate Console.

Figure 4. The print message is displayed in the console

Customizing the Button's Font and Background


Having learned how to create a basic button, let's explore customizing its appearance
using the built-in modifiers. To modify the background and text color, you can utilize the
background and foregroundColor modifiers, as illustrated below:

Text("Hello World")
.background(Color.purple)
.foregroundColor(.white)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 136


If you want to change the font type, you use the font modifier and specify the font type
(e.g. .title ) like this:

Text("Hello World")
.background(Color.purple)
.foregroundColor(.white)
.font(.title)

After the change, your button should look like the figure below.

Figure 5. Customizing the background and foreground color of a button

As you have noticed, the button's appearance could be improved. Wouldn't it be great to
add some space around the text? To achieve this, you can utilize the padding modifier
like this:

Text("Hello World")
.padding()
.background(Color.purple)
.foregroundColor(.white)
.font(.title)

Once you make the modification, the canvas will automatically update to reflect the
changes. You should see a great improvement in the button's appearance, as it now
incorporates the desired spacing around the text.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 137


Figure 6. Adding padding to the button

The Order of Modifiers is Important


One important point to highlight is that the padding modifier should be positioned
before the background modifier in the code. If you rearrange the code as shown below,
the final outcome will be different.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 138


Figure 7. Placing the padding modifier after the background modifier

If you place the padding modifier after the background modifier, the button will still
receive the desired padding, but without the intended background color. To understand
why this occurs, let's examine the modifiers in the following manner:

Text("Hello World")
.background(Color.purple) // 1. Change the background color to purple
.foregroundColor(.white) // 2. Set the foreground/font color to white
.font(.title) // 3. Change the font type
.padding() // 4. Add the paddings with the primary color (i.e.
white)

On the other hand, if you position the padding modifier before the background modifier,
the modifiers will function in the following manner:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 139


Text("Hello World")
.padding() // 1. Add the paddings
.background(Color.purple) // 2. Change the background color to purple includin
g the padding
.foregroundColor(.white) // 3. Set the foreground/font color to white
.font(.title) // 4. Change the font type

Adding Borders to the Button


This doesn't mean that you always place the padding modifier at the very beginning. The
specific placement depends on your button design requirements. For instance, if you
intend to create a button with borders, the modifiers could be arranged as follows:

Figure 8. A button with borders

You can change the code of the Text control like below:

Text("Hello World")
.foregroundColor(.purple)
.font(.title)
.padding()
.border(Color.purple, width: 5)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 140


In the given code snippet, the foreground color is set to purple, and additional padding is
applied around the text. The border modifier is utilized to specify the border's width and
color. You can experiment with adjusting the value of the width parameter to observe its
effect on the border appearance.

Now, let's consider the following button design presented by a designer. How can we
implement it using the knowledge we have acquired? Take a few moments to figure out
the solution before proceeding to the next paragraph.

Figure 9. A button with both background and border

Okay, here is the solution:

Text("Hello World")
.fontWeight(.bold)
.font(.title)
.padding()
.background(Color.purple)
.foregroundColor(.white)
.padding(10)
.border(Color.purple, width: 5)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 141


To achieve the desired button design, we can utilize two padding modifiers. The first
padding , combined with the background modifier, is responsible for creating a button
with padding and a purple background. The second padding(10) modifier adds extra
padding around the button, while the border modifier defines a rounded border in
purple.

Now, let's explore a more complex example. What if you wanted a button with rounded
borders like this?

Figure 10. A button with a rounded border

SwiftUI provides a convenient modifier called cornerRadius , which allows you to


effortlessly create rounded corners. To render the button's background with rounded
corners, you can simply apply the cornerRadius modifier and specify the desired corner
radius, as demonstrated below:

.cornerRadius(40)

Creating a border with rounded corners requires a slightly different approach, as the
border modifier alone doesn't support rounded corners. We need to draw a separate
border and overlay it onto the button. Here is the final code snippet to achieve the
desired result:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 142


Text("Hello World")
.fontWeight(.bold)
.font(.title)
.padding()
.background(.purple)
.cornerRadius(40)
.foregroundColor(.white)
.padding(10)
.overlay {
RoundedRectangle(cornerRadius: 40)
.stroke(.purple, lineWidth: 5)
}

The overlay modifier enables us to overlay an additional view on top of the current view.
In the provided code, we utilize the stroke modifier of the RoundedRectangle object to
draw a border with rounded corners. The stroke modifier allows customization of the
stroke's color and line width.

Creating a Button with Images and Text


So far, we have focused on text buttons. In a real world project, you or your designer may
want to display an image-based button. The syntax of creating an image button is nearly
the same except that you use the Image control instead of the Text control, as
illustrated below:

Button(action: {
print("Delete button tapped!")
}) {
Image(systemName: "trash")
.font(.largeTitle)
.foregroundColor(.red)
}

For convenience, we can leverage the built-in SF Symbols library to create the image
button. In this example, we utilize the SF Symbol "trash" as the image. To make the
image slightly larger, we apply the .largeTitle option to the font modifier. The

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 143


resulting button should look like this:

Figure 11. An image button

Similarly, if you want to create a circular image button with a solid background color, you
can apply the modifiers we discussed earlier. Figure 12 shows you an example.

Figure 12. A circular image button

You can use both text and image to create a button. Say, you want to put the word
"Delete" next to the icon. Replace the code like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 144


Button {
print("Delete button tapped")
} label: {
HStack {
Image(systemName: "trash")
.font(.title)
Text("Delete")
.fontWeight(.semibold)
.font(.title)
}
.padding()
.foregroundColor(.white)
.background(Color.red)
.cornerRadius(40)
}

In this code snippet, we embed both the image and text controls within a horizontal stack
( HStack ). This arrangement allows the trash icon and the Delete text to be displayed side
by side. The modifiers applied to the HStack specify the background color, padding, and
rounded corners of the button. Figure 13 showcases the resulting button design.

Figure 13. A button with both image and text

Using Label

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 145


The SwiftUI framework also offers a view called Label that lets you place an image and
text side by side. Thus, instead of using HStack , you can use Label to create the same
layout.

Button {
print("Delete button tapped")
} label: {
Label(
title: {
Text("Delete")
.fontWeight(.semibold)
.font(.title)
},
icon: {
Image(systemName: "trash")
.font(.title)
}
)
.padding()
.foregroundColor(.white)
.background(.red)
.cornerRadius(40)
}

Creating a Button with Gradient Background and


Shadow
With SwiftUI, you can easily style the button with a gradient background. Not only can
you define a specific color to the background modifier, you can easily apply a gradient
effect to any button. All you need to do is to replace the following line of code:

.background(.red)

With:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 146


.background(LinearGradient(gradient: Gradient(colors: [.red, .blue]), startPoint:
.leading, endPoint: .trailing))

The SwiftUI framework comes with several built-in gradient effects. The code above
applies a linear gradient from left ( .leading ) to right ( .trailing ). It begins with red on
the left and ends with blue on the right.

Figure 14. A button with gradient background

If you want to apply the gradient from top to bottom, you replace the .leading with
.top and the .trailing with .bottom like this:

.background(LinearGradient(gradient: Gradient(colors: [.red, .blue]), startPoint:


.top, endPoint: .bottom))

Feel free to utilize your preferred colors to render the desired gradient effect. Let's say,
your designer tells you to use the following gradient:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 147


Figure 15. A sample gradient from uigradients.com

There are multiple ways to convert the color code from hex to the compatible format in
Swift. Here is one approach. In the project navigator, choose the asset catalog ( Assets ).
Right click the blank area (under AppIcon) and select New Color Set.

Figure 16. Define a new color set in the asset catalog

Next, choose the color well for Any Appearance and click the Show inspector button.
Then click the Attributes inspector icon to reveal the attributes of a color set. In the name
field, set the name to DarkGreen. In the Color section, change the input method to 8-bit
Hexadecimal.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 148


Figure 17. Editing the attributes of a color set

Now you can set the color code in the Hex field. For this example, enter #11998e to
define the color. Name the color set DarkGreen. Repeat the same procedure to define
another color set. Enter #38ef7d for the additional color. Name this color LightGreen.

Figure 18. Define two color sets

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 149


Now that you've defined two color sets, let's go back to ContentView.swift and update the
code. To use the color set, you just need to specify the name of the color set like this:

Color("DarkGreen")
Color("LightGreen")

To render the gradient with the DarkGreen and LightGreen color sets, all you need is to
update the background modifier like this:

.background(LinearGradient(gradient: Gradient(colors: [Color("DarkGreen"), Color("


LightGreen")]), startPoint: .leading, endPoint: .trailing))

If you've made the change correctly, your button should have a nice gradient background
as shown in figure 19.

Figure 19. Generating a gradient with our own colors

There is one more modifier I want to show you in this section. The shadow modifier
allows you to draw a shadow around the button (or any view). Just add this line of code
after the cornerRadius modifier to see the shadow:

.shadow(radius: 5.0)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 150


Optionally, you can control the color, radius, and position of the shadow. Here is an
example:

.shadow(color: .gray, radius: 20.0, x: 20, y: 10)

Creating a Full-width Button


Larger buttons tend to attract more user attention. In certain cases, you may need to
create a full-width button that spans the entire width of the screen. The frame modifier
is specifically designed to control the size of a view. Whether you aim to create a button
with a fixed size or a button with variable width, you can employ this modifier.

To create a full-width button, you can modify the Button code as follows:

Button {
print("Delete tapped!")
} label: {
Label(
title: {
Text("Delete")
.fontWeight(.semibold)
.font(.title)
},
icon: {
Image(systemName: "trash")
.font(.title)
}
)
.frame(minWidth: 0, maxWidth: .infinity)
.padding()
.foregroundColor(.white)
.background(LinearGradient(gradient: Gradient(colors: [Color("DarkGreen"), Col
or("LightGreen")]), startPoint: .leading, endPoint: .trailing))
.cornerRadius(40)
.padding(.horizontal, 20)
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 151


The modified code snippet is quite similar to the previous one, with the addition of the
frame modifier placed before the padding modifier. In this case, we define a flexible
width for the button by setting the maxWidth parameter to .infinity . Consequently, the
button will expand to fill the width of the container view. By making this adjustment, you
should now achieve a full-width button.

Figure 20. A full-width button

If you want to give the button some more horizontal space, insert a padding modifier
after .cornerRadius(40) :

.padding(.horizontal, 20)

Styling Buttons with ButtonStyle


In a real world app, the same button design will be utilised in multiple buttons. Let's say,
you're creating three buttons: Delete, Edit, and Share that all have the same button style
like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 152


Figure 21. A full-width button

You'll probably write the code like this:

struct ContentView: View {


var body: some View {
VStack {
Button {
print("Share button tapped")
} label: {
Label(
title: {
Text("Share")
.fontWeight(.semibold)
.font(.title)
},
icon: {
Image(systemName: "square.and.arrow.up")
.font(.title)
}
)
.frame(minWidth: 0, maxWidth: .infinity)
.padding()

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 153


.foregroundColor(.white)
.background(LinearGradient(gradient: Gradient(colors: [Color("Dark
Green"), Color("LightGreen")]), startPoint: .leading, endPoint: .trailing))
.cornerRadius(40)
.padding(.horizontal, 20)
}

Button {
print("Edit button tapped")
} label: {
Label(
title: {
Text("Edit")
.fontWeight(.semibold)
.font(.title)
},
icon: {
Image(systemName: "square.and.pencil")
.font(.title)
}
)
.frame(minWidth: 0, maxWidth: .infinity)
.padding()
.foregroundColor(.white)
.background(LinearGradient(gradient: Gradient(colors: [Color("Dark
Green"), Color("LightGreen")]), startPoint: .leading, endPoint: .trailing))
.cornerRadius(40)
.padding(.horizontal, 20)
}

Button {
print("Delete button tapped")
} label: {
Label(
title: {
Text("Delete")
.fontWeight(.semibold)
.font(.title)
},
icon: {
Image(systemName: "trash")
.font(.title)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 154


}
)
.frame(minWidth: 0, maxWidth: .infinity)
.padding()
.foregroundColor(.white)
.background(LinearGradient(gradient: Gradient(colors: [Color("Dark
Green"), Color("LightGreen")]), startPoint: .leading, endPoint: .trailing))
.cornerRadius(40)
.padding(.horizontal, 20)
}
}

}
}

As you can see from the code above, you need to replicate all modifiers for each of the
buttons. What if you or your designer want to modify the button style? You'll need to
modify all the modifiers. That's quite a tedious task and not good coding practice. How
can you generalize the style and make it reusable?

SwiftUI provides a protocol called ButtonStyle for you to create your own button style.
To create a reusable style for our buttons, Create a new struct called
GradientBackgroundStyle that conforms to the ButtonStyle protocol. Insert the following
code snippet and put it right above #Preview :

struct GradientBackgroundStyle: ButtonStyle {

func makeBody(configuration: Self.Configuration) -> some View {


configuration.label
.frame(minWidth: 0, maxWidth: .infinity)
.padding()
.foregroundColor(.white)
.background(LinearGradient(gradient: Gradient(colors: [Color("DarkGree
n"), Color("LightGreen")]), startPoint: .leading, endPoint: .trailing))
.cornerRadius(40)
.padding(.horizontal, 20)
}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 155


The protocol requires us to provide the implementation of the makeBody function that
accepts a configuration parameter. The configuration parameter includes a label

property applies modifiers to change the button's style. In the code above, we apply the
same set of modifiers that we used before.

So, how do you apply the custom style to a button? SwiftUI provides a modifier called
.buttonStyle for you to apply the button style like this:

Button {
print("Delete button tapped")
} label: {
Label(
title: {
Text("Delete")
.fontWeight(.semibold)
.font(.title)
},
icon: {
Image(systemName: "trash")
.font(.title)
}
)
}
.buttonStyle(GradientBackgroundStyle())

Cool, right? The code is now simplified and you can easily apply the button style to any
button with just one line of code.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 156


Figure 22. Applying the custom style using .buttonStyle modifier

You can also determine if the button is pressed by accessing the isPressed property. This
allows you to alter the style of the button when the user taps on it. For example, let's say
we want to make the button a bit smaller when someone presses the button. You add a
line of code like this:

Figure 23. Applying the custom style using .buttonStyle modifier

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 157


The scaleEffect modifier lets you scale up or down a button (and any view). To scale up
the button, you provide a value greater than 1.0. To make the button smaller, enter a
value less than 1.0.

.scaleEffect(configuration.isPressed ? 0.9 : 1.0)

The line of code scales down the button (e.g., 0.9 ) when it is pressed and restores it to
its original size (e.g., 1.0 ) when the user lifts their finger. When you tap the button in
the preview, you should see a smooth animation that scales the button up and down. This
is the power of SwiftUI. You do not need to write any extra lines of code and it comes
with built-in animation.

Exercise
Your exercise is to create an animated button which shows a plus icon. When a user
presses the button, the plus icon will rotate (clockwise/counterclockwise) to become a
cross icon.

Figure 24. Rotate the icon when a user presses it

As a hint, the modifier rotationEffect may be used to rotate the button (or other view).

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 158


Styling a Button in iOS 15 (or later)

Figure 25. A button with rounded corners

I believe you know how to create a button as shown in figure 25. In iOS 15 (or later),
Apple introduced a number of modifiers for the Button view. To create the button, you
can write the code like this:

Button {

} label: {
Text("Buy me a coffee")
}
.tint(.purple)
.buttonStyle(.borderedProminent)
.buttonBorderShape(.roundedRectangle(radius: 5))
.controlSize(.large)

The tint modifier specifies the color of the button. By applying the .borderedProminent

style, iOS renders the button with purple background and display the text in white. The
.buttonBorderShape modifier lets you set the border shape of the button. Here, we set it to
.roundedRectangle to round the button’s corners.

The .controlSize allows you to change the size of the button. The default size is
.regular . Other valid values includes .large , .small , and .mini . The figure below
shows you how the button looks for different sizes.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 159


Figure 26. Buttons with different control sizes

Other than using .roundedRectangle , SwiftUI provides another border shape named
.capsule for developers to create a capsule shape button.

Figure 27. Using the capsule shape

You can also use the .automatic option to let the system adjust the shape of the button.

So far, we use the .borderProminent button style. The new version of SwiftUI provides
other built-in styles including .bordered , .borderless , and .plain . The .bordered style
is the one you will usually use. The figure below displays a sample button using the
.bordered style.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 160


Figure 28. Using the bordered style

Applying Style to Multiple Buttons


With button style, you can easily apply the same style to a group of buttons. Here is an
example:

VStack {
Button(action: {}) {
Text("Add to Cart")
.font(.headline)
}

Button(action: {}) {
Text("Discover")
.font(.headline)
.frame(maxWidth: 300)
}

Button(action: {}) {
Text("Check out")
.font(.headline)
}
}
.tint(.purple)
.buttonStyle(.bordered)
.controlSize(.large)

Using Button Role

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 161


Starting from iOS 15, the SwiftUI framework introduces a new role option for Button .
This option describes the semantic role of the button. Based on the given role, iOS
automatically renders the appropriate look & feel for the button.

For example, if you define the role as .destructive like this:

Button("Delete", role: .destructive) {


print("Delete")
}
.buttonStyle(.borderedProminent)
.controlSize(.large)

iOS will display the delete button in red automatically. Figure 29 shows you the
appearance of the button for different roles and button styles.

Figure 29. Using the bordered style

Summary

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 162


In this chapter, we explored the fundamentals of creating buttons in SwiftUI. Buttons
play a crucial role in any application's user interface. Well-designed buttons not only
enhance the visual appeal of your UI but also elevate the overall user experience of your
app. As you have discovered, by combining SF Symbols, gradients, and animations, you
can effortlessly build attractive and functional buttons that captivate users.

To access the complete Xcode project, you can follow the link below:

Demo project (https://www.appcoda.com/resources/swiftui5/SwiftUIButton.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 163


Chapter 7
Understanding State and Binding
State management is a crucial aspect of application development that every developer
must address. Let's consider the scenario of developing a music player app. When a user
taps the Play button, it should transition to a Stop button. In your implementation, you
need a mechanism to track the application's state, enabling you to determine when to
alter the button's appearance.

Figure 1. Stop and Play buttons

SwiftUI provides several built-in features for state management. One of these features is
the @State property wrapper. When you annotate a property with @State , SwiftUI
automatically stores it within your application. Furthermore, views that utilize this
property are automatically notified of any changes to its value. As a result, when the state
changes, SwiftUI recomputes the affected views and updates the application's appearance
accordingly.

Doesn't that sound great? However, I understand that state management can be a bit
confusing at first. To gain a better understanding of state and binding, it would be helpful
to go through the coding examples and exercises in this chapter. By working on these

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 164


examples, you will develop a stronger grasp of this crucial concept in SwiftUI. Take your
time to explore the exercises. It will help you master this important concept of SwiftUI.

Creating a New Project with SwiftUI enabled


Let's start with a simple example that I just described earlier to see how to switch
between a Play button and a Stop button, by keeping track of the application's state.
First, fire up Xcode and create a new project using the App template. Set the name of the
project to SwiftUIState but you're free to use any other name. Please make sure SwiftUI
is selected as the Interface option.

Figure 2. Creating a new project

Once you save the project, Xcode will load the ContentView.swift file and display a
preview in the canvas. To create the Play button, you can use the following code snippet:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 165


Button {
// Switch between the play and stop button
} label: {
Image(systemName: "play.circle.fill")
.font(.system(size: 150))
.foregroundColor(.green)
}

We make use of the system image play.circle.fill and color the button green.

Figure 3. Previewing the play button

Controlling the Button's State


The button's action is currently empty, but we want to change its appearance from Play
to Stop when the button is tapped. Additionally, we want to change the color of the
button to red when the Stop button is displayed.

So, how can we implement that? Obviously, we need a variable to keep track of the
button's state. Let's name it isPlaying . It's a boolean variable indicating whether the app
is in the Playing state or not. If isPlaying is set to true , the app should display a Stop

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 166


button. If isPlaying is set to false , the app should display a Play button.

Here is the code that implements this behavior:

struct ContentView: View {


private var isPlaying = false

var body: some View {


Button {
// Switch between the play and stop button
} label: {
Image(systemName: isPlaying ? "stop.circle.fill" : "play.circle.fill")
.font(.system(size: 150))
.foregroundColor(isPlaying ? .red : .green)
}
}
}

We modify the image's name and color based on the value of the isPlaying variable. If
you update the code in your project, you will see a Play button in the preview canvas.
However, if you set the default value of isPlaying to true , you will see a Stop button
instead.

Now, the question arises: how can the app monitor changes in the state (i.e., isPlaying )
and automatically update the button? In SwiftUI, achieving this functionality is
straightforward. All you need to do is prefix the isPlaying property with @State :

@State private var isPlaying = false

By declaring the isPlaying property as a state variable, SwiftUI takes care of managing
its storage and monitoring its value changes. Whenever the value of isPlaying changes,
SwiftUI automatically recomputes the views that rely on the isPlaying state. This
ensures that the user interface reflects the updated state accurately.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 167


Only access a state property from inside the view’s body (or from functions called
by it). For this reason, you should declare your state properties as private , to
prevent clients of your view from accessing it

- Apple's official documentation


(https://developer.apple.com/documentation/swiftui/state)

We still haven't implemented the button's action. So, let's do that now:

Button {
// Switch between the play and stop button
isPlaying.toggle()
} label: {
Image(systemName: isPlaying ? "stop.circle.fill" : "play.circle.fill")
.font(.system(size: 150))
.foregroundColor(isPlaying ? .red : .green)
}

In the action closure, we call the toggle() method to toggle the Boolean value between
false and true . This allows us to switch between the Play and Stop button. In the
preview canvas, you can click the play icon to trigger the toggle and observe the button's
appearance change accordingly.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 168


Figure 4. Toggle between the Play and Stop button

Indeed, you may have noticed that SwiftUI renders a fade animation when toggling
between the buttons. This animation is automatically generated and built-in to SwiftUI.
It smoothly transitions the appearance of the button, providing a seamless user
experience.

In subsequent chapters of the book, we will delve further into the topic of animations.
You will learn how to customize and create your own animations using SwiftUI. As you
can see, SwiftUI simplifies the process of implementing UI animations, making it more
accessible for all developers.

Exercise #1
Your exercise is to create a counter button that displays the number of taps. When a user
taps the button, the counter should increase by one and display the total number of taps.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 169


Figure 5. Toggle between the Play and Stop button

Working with Binding


Were you successful in creating the counter button? In this case, instead of declaring a
boolean variable as a state, we use an integer state variable to keep track of the count.
Whenever the button is tapped, the counter will increase by 1. You can refer to figure 6
for the code snippet to accomplish this.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 170


Figure 6. A counter button

Now let's further modify the code to display three counter buttons, as shown in Figure 7.
All three buttons will share the same counter. Regardless of which button is tapped, the
counter will increase by 1, and all the buttons will update to display the updated count.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 171


Figure 7. Three counter buttons

As you can see, all the buttons have the same look and feel. To avoid duplicating code, it
is good practice to extract common views into reusable subviews. In this case, we can
extract the Button view to create an independent subview. Here's an example of how you
can achieve this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 172


struct CounterButton: View {
@Binding var counter: Int

var color: Color

var body: some View {


Button {
counter += 1
} label: {
Circle()
.frame(width: 200, height: 200)
.foregroundStyle(color)
.overlay {
Text("\(counter)")
.font(.system(size: 100, weight: .bold, design: .rounded))
.foregroundStyle(.white)
}
}
}
}

The CounterButton view accepts two parameters: counter and color. You can create a
button in red like this:

CounterButton(counter: $counter, color: .red)

You may have noticed that the counter variable in the CounterButton view is annotated
with @Binding . Additionally, when you create an instance of CounterButton , the value of
the counter parameter is prefixed with a $ sign.

What do these symbols mean?

After extracting the button into a separate view, CounterButton becomes a subview of
ContentView . The counter increment is now performed within the CounterButton view
instead of the ContentView . To manage the state variable in the ContentView , the
CounterButton needs a way to access and update it.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 173


The @Binding annotation indicates that the counter variable is a reference to a value
stored elsewhere in the parent view. It allows the CounterButton to read and update the
value of counter without owning it. The $ sign is a shorthand syntax provided by
SwiftUI to create a two-way binding between the counter parameter and the actual state
variable in the parent view.

By using @Binding and the $ sign, changes made to the counter value within the
CounterButton view will be reflected in the parent view (i.e. ContentView ), and vice versa.
This enables the synchronized updating of the counter value across all the buttons in the
parent view.

Figure 8. Understanding Binding

Now that you understand how bindings work, you can proceed to create the other two
buttons and align them vertically using a VStack . Here's an example of how you can
accomplish this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 174


struct ContentView: View {

@State private var counter = 1

var body: some View {


VStack {
CounterButton(counter: $counter, color: .blue)
CounterButton(counter: $counter, color: .green)
CounterButton(counter: $counter, color: .red)
}
}
}

After making the changes, you can test the app in the preview canvas. Tapping any of the
buttons will increase the count by one, and all the buttons will update to reflect the
updated count.

Figure 9. Testing the three counter buttons

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 175


Exercise #2
Presently, all the buttons share the same count. For this exercise, you are required to
modify the code such that each of the buttons has its own counter. When the user taps
the blue button, the app only increases the counter of the blue button by 1. In addition,
you will need to provide a master counter that sums up the counter of all buttons. Figure
10 shows the sample layout for the exercise.

Presently, all the buttons share the same count. For this exercise, you need to modify the
code so that each button has its own counter. When the user taps the blue button, only
the counter for the blue button should increase by 1. Furthermore, you need to provide a
master counter that sums up the counters of all the buttons. Figure 10 shows the sample
layout for this exercise.

Figure 10. Each button has its own counter

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 176


Summary
The support for state in SwiftUI simplifies state management in application
development. It's important to understand the concepts of @State and @Binding as they
play a significant role in SwiftUI for managing states and updating the user interface.

This chapter introduces the basics of state management in SwiftUI. As you progress, you
will learn more about how to utilize @State for view animations and how to manage
shared states among multiple views.

For reference, you can download the sample project and solution to exercise from the
links below:

Demo project (https://www.appcoda.com/resources/swiftui5/SwiftUICounter.zip)


Exercise (https://www.appcoda.com/resources/swiftui5/SwiftUIMasterCounter.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 177


Chapter 8
Implementing Path and Shape for
Line Drawing and Pie Charts
For experienced developers, you probably have used the Core Graphics APIs to draw
shapes and objects. It's a very powerful framework for you to create vector-based
drawings. SwiftUI also provides several vector drawing APIs for developers to draw lines
and shapes.

In this chapter, you will learn about drawing lines, arcs, pie charts, and donut charts
using the Path and built-in Shape APIs in SwiftUI. The topics covered in this chapter
include:

Understanding the Path type and how to draw lines


Exploring the Shape protocol and creating custom shapes by conforming to the
protocol
Drawing a pie chart
Creating a progress indicator with an open circle
Drawing a donut chart

Figure 1 provides a visual representation of the shapes and charts that we will be creating
in this chapter.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 178


Figure 1. Sample shapes and charts

Understanding Path
In SwiftUI, you can draw lines and shapes using the Path struct. According to Apple's
documentation (https://developer.apple.com/documentation/swiftui/path), a Path is a
struct that represents the outline of a 2D shape. It allows you to define a series of points
and draw lines between them.

Let's take a look at an example using a rectangle, as shown in figure 2. We will walk
through how this rectangle is drawn.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 179


Figure 2. A rectangle with coordinates

If you were to describe how to draw the rectangle step by step, you would probably
provide the following explanation:

1. Start at the point (20, 20).


2. Draw a line from (20, 20) to (300, 20).
3. Continue drawing a line from (300, 20) to (300, 200).
4. Extend the line from (300, 200) to (20, 200).
5. Fill the entire area with a green color.

Following these steps, you can create a rectangular shape by moving to the starting point
and drawing lines to form the outline. Finally, you can fill the shape with the specified
color, resulting in a green rectangle.

That's how Path is works! Let's translate these verbal descriptions into code:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 180


Path() { path in
path.move(to: CGPoint(x: 20, y: 20))
path.addLine(to: CGPoint(x: 300, y: 20))
path.addLine(to: CGPoint(x: 300, y: 200))
path.addLine(to: CGPoint(x: 20, y: 200))
}
.fill(.green)

You create the rectangle shape by initializing a Path object and providing detailed
instructions within a closure. To start, you call the move(to:) method to move to a
specific coordinate. Then, you use the addLine(to:) method to draw a line from the
current point to another point. By default, the path is filled with the default foreground
color, which is black. To fill the shape with a different color, you can apply the .fill

modifier and specify a different color.

To test the code, create a new Xcode project using the App template and name it
SwiftUIShape (or any name you prefer). Then, paste the given code snippet into the body

property. The preview canvas should display a green rectangle.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 181


Figure 3. Drawing a rectangle using Path

Using Stroke to Draw Borders


You're not required to fill the whole area with color. If you only want to draw the lines
without filling the shape, you can use the .stroke modifier instead. This modifier allows
you to specify the line width and color of the stroke. Figure 4 demonstrates the result.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 182


Figure 4. Drawing the lines with stroke

Because we didn't include a step to draw a line from the last point back to the starting
point, the path is left open-ended. To close the path and create a closed shape, you can
call the closeSubpath() method at the end of the Path closure. This method
automatically connects the current point with the starting point of the path, completing
the shape.

Figure 5. Closing the path with closeSubpath()

Drawing Curves

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 183


Path provides several built-in APIs to help you draw different shapes. You are not
limited to drawing straight lines. The addQuadCurve , addCurve , and addArc methods
allow you to create curves and arcs. For example, if you want to draw a dome shape on
top of a rectangle like as shown in figure 6:

Figure 6. A dome with a rectangle bottom

You can write the code like this:

Path() { path in
path.move(to: CGPoint(x: 20, y: 60))
path.addLine(to: CGPoint(x: 40, y: 60))
path.addQuadCurve(to: CGPoint(x: 210, y: 60), control: CGPoint(x: 125, y: 0))
path.addLine(to: CGPoint(x: 230, y: 60))
path.addLine(to: CGPoint(x: 230, y: 100))
path.addLine(to: CGPoint(x: 20, y: 100))
}
.fill(Color.purple)

The addQuadCurve method allows you to draw a curve by defining an anchor point and a
control point. In the context of drawing the dome shape on top of the rectangle (as shown
in figure 6), the anchor points are (40, 60) and (210, 60), while the control point is (125,
0).

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 184


The control point plays a crucial role in determining the shape of the curve. By adjusting
the position of the control point, you can control the curvature and roundness of the
resulting curve. For example, if you move the control point closer to the top of the
rectangle (e.g., (125, 30)), the curve will become less rounded and exhibit a different
appearance.

Feel free to experiment with different control point values to observe how they affect the
shape of the curve. This will provide you with a better understanding of how the control
point influences the appearance of the curve.

Fill and Stroke


What if you want to draw the border of the shape and fill the shape with color at the same
time? The fill and stroke modifiers cannot be used in parallel. You can make use of
ZStack to achieve the same effect. Here is the code:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 185


ZStack {
Path() { path in
path.move(to: CGPoint(x: 20, y: 60))
path.addLine(to: CGPoint(x: 40, y: 60))
path.addQuadCurve(to: CGPoint(x: 210, y: 60), control: CGPoint(x: 125, y: 0
))
path.addLine(to: CGPoint(x: 230, y: 60))
path.addLine(to: CGPoint(x: 230, y: 100))
path.addLine(to: CGPoint(x: 20, y: 100))
}
.fill(Color.purple)

Path() { path in
path.move(to: CGPoint(x: 20, y: 60))
path.addLine(to: CGPoint(x: 40, y: 60))
path.addQuadCurve(to: CGPoint(x: 210, y: 60), control: CGPoint(x: 125, y: 0
))
path.addLine(to: CGPoint(x: 230, y: 60))
path.addLine(to: CGPoint(x: 230, y: 100))
path.addLine(to: CGPoint(x: 20, y: 100))
path.closeSubpath()
}
.stroke(Color.black, lineWidth: 5)
}

We create two Path objects with the same path and overlay one on top of the other using
a ZStack . The one at the bottom is filled with a purple color using the fill modifier,
creating a dome rectangle. The one overlaid on top only draws the borders with a black
color using the stroke modifier. Figure 7 illustrates the resulting shape with a filled
dome and border.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 186


Figure 7. A dome rectangle with borders

Drawing Arcs and Pie Charts


SwiftUI provides a convenient API for drawing arcs, which is particularly useful for
creating shapes like pie charts. To draw an arc, you can use the Path object and its
addArc method. The code snippet below demonstrates how to draw an arc:

Path { path in
path.move(to: CGPoint(x: 200, y: 200))
path.addArc(center: .init(x: 200, y: 200), radius: 150, startAngle: .degrees(0
), endAngle: .degrees(90), clockwise: true)
}
.fill(.green)

Enter this code in the body, you will see an arc that fills with green color in the preview
canvas.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 187


Figure 8. A sample arc

In the code, we first move to the starting point (200, 200). Then we call addArc to create
the arc. The addArc method accepts several parameters:

center - the center point of the circle


radius - the radius of the circle for creating the arc
startAngle - the starting angle of the arc
endAngle - the ending angle of the arc
clockwise - the direction to draw the arc

If you just look at the names of the startAngle and endAngle parameters, you might be a
bit confused about their meaning. To better understand how these parameters work, take
a look at Figure 9. It provides a visual representation of how the startAngle and
endAngle affect the shape of the arc.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 188


Figure 9. Understanding starting and end angle

By using addArc , you can easily create a pie chart with different colored segments. To
achieve this, you can overlay different pie segments using a ZStack . Each segment has
different values for startAngle and endAngle to compose the chart. Here is an example:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 189


ZStack {
Path { path in
path.move(to: CGPoint(x: 187, y: 187))
path.addArc(center: .init(x: 187, y: 187), radius: 150, startAngle: .degre
es(0), endAngle: .degrees(190), clockwise: true)
}
.fill(.yellow)

Path { path in
path.move(to: CGPoint(x: 187, y: 187))
path.addArc(center: .init(x: 187, y: 187), radius: 150, startAngle: .degre
es(190), endAngle: .degrees(110), clockwise: true)
}
.fill(.teal)

Path { path in
path.move(to: CGPoint(x: 187, y: 187))
path.addArc(center: .init(x: 187, y: 187), radius: 150, startAngle: .degre
es(110), endAngle: .degrees(90), clockwise: true)
}
.fill(.blue)

Path { path in
path.move(to: CGPoint(x: 187, y: 187))
path.addArc(center: .init(x: 187, y: 187), radius: 150, startAngle: .degre
es(90), endAngle: .degrees(360), clockwise: true)
}
.fill(.purple)

This code will render a pie chart with 4 segments. If you want to have more segments,
you can create additional Path objects with different angle values. As a side note, the
colors used in the example come from the standard color objects provided in iOS. You
can find the full set of color objects at
https://developer.apple.com/documentation/uikit/uicolor/standard_colors.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 190


Sometimes, you may want to highlight a particular segment by separating it from the rest
of the pie chart. For example, to highlight the purple segment, you can apply the offset

modifier to reposition the segment:

Path { path in
path.move(to: CGPoint(x: 187, y: 187))
path.addArc(center: .init(x: 187, y: 187), radius: 150, startAngle: .degrees(90
), endAngle: .degrees(360), clockwise: true)
}
.fill(.purple)
.offset(x: 20, y: 20)

Optionally, you can overlay a border to further catch people's attention. If you want to
add a label to the highlighted segment, you can also overlay a Text view like this:

Path { path in
path.move(to: CGPoint(x: 187, y: 187))
path.addArc(center: .init(x: 187, y: 187), radius: 150, startAngle: .degrees(90
), endAngle: .degrees(360), clockwise: true)
path.closeSubpath()
}
.fill(.purple)
.offset(x: 20, y: 20)
.overlay(
Text("25%")
.font(.system(.largeTitle, design: .rounded))
.bold()
.foregroundColor(.white)
.offset(x: 80, y: -110)
)

This path has the same starting and end angle as the purple segment, but it only draws
the border and adds a text view to make the segment stand out. Figure 10 shows the end
result.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 191


Figure 10. A pie chart with a highlighted segment

Understanding the Shape Protocol


Before we dive into the Shape protocol, let's start with a simple exercise. Based on what
you have learned, try drawing the following shape using Path .

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 192


Figure 11. Your exercise

Don't look at the solution yet. Try to build one by yourself.

Okay, to build this shape, you create a Path using addLine and addQuadCurve :

Path() { path in
path.move(to: CGPoint(x: 0, y: 0))
path.addQuadCurve(to: CGPoint(x: 200, y: 0), control: CGPoint(x: 100, y: -20))
path.addLine(to: CGPoint(x: 200, y: 40))
path.addLine(to: CGPoint(x: 200, y: 40))
path.addLine(to: CGPoint(x: 0, y: 40))
}
.fill(Color.green)

If you've read the documentation for Path , you may have come across another function
called addRect , which allows you to draw a rectangle with a specific width and height.
Let's use it to create the same shape:

Path() { path in
path.move(to: CGPoint(x: 0, y: 0))
path.addQuadCurve(to: CGPoint(x: 200, y: 0), control: CGPoint(x: 100, y: -20))
path.addRect(CGRect(x: 0, y: 0, width: 200, height: 40))
}
.fill(Color.green)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 193


Let's talk about the Shape protocol. The protocol is very simple with only one
requirement. To adopt it, you must implement the following function:

func path(in rect: CGRect) -> Path

When is it useful to adopt the Shape protocol? To answer this, let's consider a scenario
where you want to create a button with the dome shape but with a flexible size. Is it
possible to reuse the Path that you have just created?

Looking at the code above, you created a Path with absolute coordinates and size.
However, if you want to create the same shape but with a variable size, you can create a
struct that adopts the Shape protocol and implement the path(in:) function. When the
path(in:) function is called by the framework, you will be provided with the rect size.
You can then draw the path within that rect .

In the following code, we create the Dome shape by implementing the path(in:)

function:

struct Dome: Shape {


func path(in rect: CGRect) -> Path {
var path = Path()

path.move(to: CGPoint(x: 0, y: 0))


path.addQuadCurve(to: CGPoint(x: rect.size.width, y: 0), control: CGPoint(
x: rect.size.width/2, y: -(rect.size.width * 0.1)))
path.addRect(CGRect(x: 0, y: 0, width: rect.size.width, height: rect.size.
height))

return path
}
}

By adopting the Shape protocol, we are given the rectangular area for drawing the path.
From the rect parameter, we can obtain the width and height of the rectangular area,
which allows us to compute the control point and draw the rectangular base.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 194


With the dynamic shape created, you can utilize it to create various SwiftUI controls. For
instance, you can create a button with the Dome shape as follows:

Button(action: {
// Action to perform
}) {
Text("Test")
.font(.system(.title, design: .rounded))
.bold()
.foregroundColor(.white)
.frame(width: 250, height: 50)
.background(Dome().fill(Color.red))
}

We apply the Dome shape as the background of the button, setting its width and height
based on the specified frame size.

Figure 12. Creating a button with the Dome shape

Using the Built-in Shapes

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 195


Earlier, we built a custom shape using the Shape protocol. However, SwiftUI provides
several built-in shapes that you can use out of the box, including Circle , Rectangle ,
RoundedRectangle , Ellipse , and more. If you don't need anything too complex, these
shapes are perfect for creating common objects.

Figure 13. A stop button

Let's say you want to create a stop button like the one shown in figure 13. It's composed
of a rounded rectangle and a circle. You can write the code for it like this:

Circle()
.foregroundColor(.green)
.frame(width: 200, height: 200)
.overlay(
RoundedRectangle(cornerRadius: 5)
.frame(width: 80, height: 80)
.foregroundColor(.white)
)

Here, we initialize a Circle view and then overlay a RoundedRectangle view on it.

Creating a Progress Indicator Using Shapes

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 196


By mixing and matching the built-in shapes, you can create various types of vector-based
UI controls for your applications. Let me show you another example. Figure 14 shows a
progress indicator that can be built using the Circle shape.

Figure 14. A progress indicator

This progress indicator is actually composed of two circles. We have a gray outline of a
circle underneath. On top of the grey circle, is an open outline of a circle indicating the
completion progress. In your project, write the code in ContentView like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 197


struct ContentView: View {

private var purpleGradient = LinearGradient(gradient: Gradient(colors: [ Color


(red: 207/255, green: 150/255, blue: 207/255), Color(red: 107/255, green: 116/255,
blue: 179/255) ]), startPoint: .trailing, endPoint: .leading)

var body: some View {

ZStack {
Circle()
.stroke(Color(.systemGray6), lineWidth: 20)
.frame(width: 300, height: 300)

}
}
}

We use the stroke modifier to draw the outline of the grey circle. You can adjust the
value of the lineWidth parameter to make the lines thicker or thinner according to your
preference. The purpleGradient property represents the purple gradient that we will use
later when drawing the open circle.

Figure 15. Drawing a gray circle

Now, insert the following code in ZStack to create the open circle:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 198


Circle()
.trim(from: 0, to: 0.85)
.stroke(purpleGradient, lineWidth: 20)
.frame(width: 300, height: 300)
.overlay {
VStack {
Text("85%")
.font(.system(size: 80, weight: .bold, design: .rounded))
.foregroundColor(.gray)
Text("Complete")
.font(.system(.body, design: .rounded))
.bold()
.foregroundColor(.gray)
}
}

To create an open circle, you can add the trim modifier. By specifying a from value and
a to value, you can indicate which segment of the circle should be shown. In this case,
we want to show a progress of 85%, so we set the from value to 0 and the to value to
0.85.

To display the completion percentage, we overlay a text view in the middle of the circle.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 199


Figure 16. Drawing the progress view

Drawing a Donut Chart


The last example I want to show you is a donut chart. If you have a good understanding
of how the trim modifier works, you may already have an idea of how we are going to
implement the donut chart. By adjusting the values of the trim modifier, we can divide a
circle into multiple segments.

That's the technique we use to create a donut chart, and here is the code:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 200


ZStack {
Circle()
.trim(from: 0, to: 0.4)
.stroke(Color(.systemBlue), lineWidth: 80)

Circle()
.trim(from: 0.4, to: 0.6)
.stroke(Color(.systemTeal), lineWidth: 80)

Circle()
.trim(from: 0.6, to: 0.75)
.stroke(Color(.systemPurple), lineWidth: 80)

Circle()
.trim(from: 0.75, to: 1)
.stroke(Color(.systemYellow), lineWidth: 90)
.overlay(
Text("25%")
.font(.system(.title, design: .rounded))
.bold()
.foregroundColor(.white)
.offset(x: 80, y: -100)
)
}
.frame(width: 250, height: 250)

The first segment represents 40% of the circle. The second segment represents 20% of
the circle, but note that the from value is 0.4 instead of 0. This ensures that the second
segment starts where the first segment ends.

For the last segment, I intentionally set the line width to a larger value so that this
segment stands out from the others. If you prefer a different line width, you can modify
the value of lineWidth from 90 to 80 .

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 201


Figure 17. Drawing the donut chart

Summary
I hope you enjoyed reading this chapter and working on the demo projects. With the
drawing APIs provided by the framework, you have the power to create custom shapes
for your application. There is so much more you can do with Path and Shape , and I've
only scratched the surface in this chapter. I encourage you to apply what you've learned
and continue exploring these powerful APIs—they truly are magical!

For reference, you can download the project files for the shapes chapter below:

Demo project (https://www.appcoda.com/resources/swiftui5/SwiftUIShape.zip)

Note: With the release of iOS 17, SwiftUI now includes built-in features for creating pie
charts and donut charts. Please refer to Chapter 46 to learn more.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 202


Chapter 9
Basic Animations and Transitions
Have you ever used the Magic Move animation in Keynote? With Magic Move, you can
easily create slick animations between slides. Keynote automatically analyzes the objects
between slides and renders the animations automatically. To me, SwiftUI has brought
Magic Move to app development. Animations using the framework are automatic and
magical. You define two states of a view, and SwiftUI will figure out the rest, animating
the changes between the two states.

SwiftUI empowers you to animate changes for individual views and transitions between
views. The framework comes with a number of built-in animations to create different
effects.

In this chapter, you will learn how to animate views using implicit and explicit
animations provided by SwiftUI. As usual, we'll work on a few demo projects and learn
the programming techniques along the way.

Implicit and Explicit Animations


SwiftUI provides two types of animations: implicit and explicit. Both approaches allow
you to animate views and view transitions. For implementing implicit animations, the
framework provides a modifier called animation . You attach this modifier to the views
you want to animate and specify your preferred animation type. Optionally, you can
define the animation duration and delay. SwiftUI will then automatically render the
animation based on the state changes of the views.

Explicit animations offer more fine control over the animations you want to present.
Instead of attaching a modifier to the view, you tell SwiftUI what state changes you want
to animate inside the withAnimation() block.

Feeling a bit confused? That's completely fine. You will have a better understanding after
going through a couple of examples.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 203


Implicit Animations
Let's begin with implicit animations. Create a new project and name it SwiftUIAnimation

(or choose any name you prefer). Make sure to select SwiftUI as the interface framework!

Figure 1. Animate a button's state change

Take a look at figure 1. It's a simple tappable view consisting of a red circle and a heart.
When a user taps the heart or circle, the circle's color changes to light gray and the heart's
color changes to red. Additionally, the heart icon grows bigger in size. We have various
state changes here:

1. The color of the circle changes from red to light gray.


2. The color of the heart icon changes from white to red.
3. The heart icon doubles its original size.

To implement the tappable circle using SwiftUI, add the following code to
ContentView.swift :

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 204


struct ContentView: View {
@State private var circleColorChanged = false
@State private var heartColorChanged = false
@State private var heartSizeChanged = false

var body: some View {

ZStack {
Circle()
.frame(width: 200, height: 200)
.foregroundStyle(circleColorChanged ? Color(.systemGray5) : .red)

Image(systemName: "heart.fill")
.foregroundStyle(heartColorChanged ? .red : .white)
.font(.system(size: 100))
.scaleEffect(heartSizeChanged ? 1.0 : 0.5)
}
.onTapGesture {
circleColorChanged.toggle()
heartColorChanged.toggle()
heartSizeChanged.toggle()
}

}
}

We define three state variables to represent the states of the circle color, heart color, and
heart size, with the initial value set to false . We use a ZStack to overlay the heart image
on top of the circle. SwiftUI comes with the onTapGesture modifier to detect tap gestures,
which can be attached to any view to make it tappable. In the onTapGesture closure, we
toggle the state variables to change the appearance of the view.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 205


Figure 2. Implementing the circle and heart views

In the preview canvas, when you tap the heart view, the color of the circle and heart icon
should change accordingly. However, these changes are not animated by default.

To animate the changes, you need to attach the animation modifier to both the Circle

and Image views.

Circle()
.frame(width: 200, height: 200)
.foregroundStyle(circleColorChanged ? Color(.systemGray5) : .red)
.animation(.default, value: circleColorChanged)

Image(systemName: "heart.fill")
.foregroundStyle(heartColorChanged ? .red : .white)
.font(.system(size: 100))
.scaleEffect(heartSizeChanged ? 1.0 : 0.5)
.animation(.default, value: heartSizeChanged)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 206


SwiftUI monitors the value changes specified in the animation modifier. When a value
changes, it computes and renders the animation, allowing the views to transition
smoothly from one state to another. If you tap the heart again, you should see a slick
animation.

The animation modifier can be applied not only to a single view but also to a group of
views. For example, you can rewrite the code above by attaching the animation modifier
to the ZStack like this:

ZStack {
Circle()
.frame(width: 200, height: 200)
.foregroundColor(circleColorChanged ? Color(.systemGray5) : .red)

Image(systemName: "heart.fill")
.foregroundColor(heartColorChanged ? .red : .white)
.font(.system(size: 100))
.scaleEffect(heartSizeChanged ? 1.0 : 0.5)
}
.animation(.default, value: circleColorChanged)
.animation(.default, value: heartSizeChanged)
.onTapGesture {
self.circleColorChanged.toggle()
self.heartColorChanged.toggle()
self.heartSizeChanged.toggle()
}

In the example, we used the default animation, which is the spring animation on iOS 17.
SwiftUI also offers a variety of built-in animations to choose from, including linear ,
easeIn , easeOut , and easeInOut . The linear animation animates the changes at a
constant speed, while the easing animations have different speeds and acceleration
curves. For more details, you can refer to websites like www.easings.net to see the visual
representations of each easing function.

To use an alternate animation, you just need to set the specific animation in the
animation modifier. Let's say, you want to use the linear animation, you can change
.default to the following:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 207


.animation(.linear, value: circleColorChanged)

To customize the spring animation, you can call the spring function and specify the
parameters for the animation, such as the duration and the type of bounce:

.animation(.spring(.bouncy, blendDuration: 1.0), value: circleColorChanged)

This renders a spring-based animation that gives the heart a bumpy effect. Adjusting
these parameters allows you to create different animation effects.

Explicit Animations
That's how you animate views using implicit animation. Let's see how we can achieve the
same result using explicit animation. As explained before, you need to wrap the state
changes in a withAnimation block. To create the same animated effect, you can write the
code like this:

ZStack {
Circle()
.frame(width: 200, height: 200)
.foregroundColor(circleColorChanged ? Color(.systemGray5) : .red)

Image(systemName: "heart.fill")
.foregroundColor(heartColorChanged ? .red : .white)
.font(.system(size: 100))
.scaleEffect(heartSizeChanged ? 1.0 : 0.5)
}
.onTapGesture {
withAnimation(.default) {
self.circleColorChanged.toggle()
self.heartColorChanged.toggle()
self.heartSizeChanged.toggle()
}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 208


We no longer use the animation modifier; instead, we wrap the code inside onTapGesture

with withAnimation . The withAnimation function takes an animation parameter, where we


specify the type of animation we want to use.

To use the spring animation, you can update the withAnimation block as follows:

withAnimation(.spring(.bouncy, blendDuration: 1.0)) {


self.circleColorChanged.toggle()
self.heartColorChanged.toggle()
self.heartSizeChanged.toggle()
}

With explicit animation, you have fine-grained control over which state changes you want
to animate. If you don't want to animate the size change of the heart icon, you can
exclude that line of code from the withAnimation block:

.onTapGesture {
withAnimation(.spring(.bouncy, blendDuration: 1.0)) {
self.circleColorChanged.toggle()
self.heartColorChanged.toggle()
}

self.heartSizeChanged.toggle()
}

In this case, SwiftUI excludes the scaling animation and only animates the color changes.

You may wonder if we can disable the scale animation by using implicit animation. You
can! You can reorder the .animation modifier to prevent SwiftUI from animating a
certain state change. Here is the code that achieves the same effect:

You may wonder if we can disable the scale animation by using implicit animation. By
reordering the .animation modifier, you can achieve the desired effect. Here's the
modified code:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 209


ZStack {
Circle()
.frame(width: 200, height: 200)
.foregroundStyle(circleColorChanged ? Color(.systemGray5) : .red)
.animation(.spring(.bouncy, blendDuration: 1.0), value: circleColorChanged
)

Image(systemName: "heart.fill")
.foregroundStyle(heartColorChanged ? .red : .white)
.font(.system(size: 100))
.animation(.spring(.bouncy, blendDuration: 1.0), value: heartColorChanged)
.scaleEffect(heartSizeChanged ? 1.0 : 0.5)
}
.onTapGesture {
self.circleColorChanged.toggle()
self.heartColorChanged.toggle()
self.heartSizeChanged.toggle()
}

For the Image view, we place the animation modifier right before scaleEffect . This will
disable the animation. SwiftUI will only animate the color changes and exclude the
scaling animation.

While you can achieve the same animation using implicit animation, in my opinion, it's
more convenient to use explicit animation in this case.

Creating a Loading Indicator Using RotationEffect


The power of SwiftUI animation lies in its simplicity and automation. You don't need to
worry about the details of how the views are animated. Instead, you just need to provide
the start and end states, and SwiftUI takes care of the rest. This allows you to create
various types of animations with ease. Whether it's animating the position, size, opacity,
or color of a view, SwiftUI automatically handles the animation based on the specified
state changes. This abstraction makes it effortless to create dynamic and engaging user
interfaces.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 210


Figure 3. A sample loading indicator

For example, let's create a simple loading indicator that you can commonly find in a real-
world application like "Medium". To create a loading indicator similar to the one shown
in figure 3, we can start with an open ended circle like this:

Circle()
.trim(from: 0, to: 0.7)
.stroke(Color.green, lineWidth: 5)
.frame(width: 100, height: 100)

How do we rotate the circle? We can use the rotationEffect and animation modifiers.
The idea is to apply a rotation effect of 360 degrees to the circle, and animate it to create
the spinning effect. Here's the code to achieve that:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 211


struct ContentView: View {
@State private var isLoading = false

var body: some View {


Circle()
.trim(from: 0, to: 0.7)
.stroke(Color.green, lineWidth: 5)
.frame(width: 100, height: 100)
.rotationEffect(Angle(degrees: isLoading ? 360 : 0))
.animation(.default.repeatForever(autoreverses: false), value: isLoadi
ng)
.onAppear() {
isLoading = true
}
}
}

The rotationEffect modifier is used to specify the rotation degree, in this case, 360
degrees. In the provided code, we have a state variable isLoading that controls the
loading status. When isLoading is set to true , the rotation degree is set to 360, causing
the circle to rotate. The animation modifier is applied with .default animation, but with
a difference. We specify the repeatForever option to make the animation repeat
indefinitely. This repetition creates the continuous loading animation effect.

*Note: If you can't see the animation in the preview canvas, run the app in the
simulator.*

If you wish to adjust the speed of the animation, you can use the linear animation type
and specify a duration. This can be achieved by updating the animation modifier as
follows:

.animation(.linear(duration: 5).repeatForever(autoreverses: false), value: isLoadi


ng)

The greater the duration value, the slower the rotation animation will be.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 212


The onAppear modifier may be new to you. If you have some knowledge of UIKit, this
modifier is similar to viewDidAppear . It is automatically called when the view appears on
the screen. In the code, we set the loading status to true in order to start the animation
when the view is loaded.

Once you have mastered this technique, you can customize the design and create various
versions of loading indicators. For example, you can overlay an arc on a circle to create a
more sophisticated loading indicator.

Figure 4. A sample loading indicator

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 213


struct ContentView: View {

@State private var isLoading = false

var body: some View {


ZStack {

Circle()
.stroke(Color(.systemGray5), lineWidth: 14)
.frame(width: 100, height: 100)

Circle()
.trim(from: 0, to: 0.2)
.stroke(Color.green, lineWidth: 7)
.frame(width: 100, height: 100)
.rotationEffect(Angle(degrees: isLoading ? 360 : 0))
.animation(.linear(duration: 1).repeatForever(autoreverses: false)
, value: isLoading)
.onAppear() {
self.isLoading = true
}
}
}
}

The loading indicator doesn't have to be circular. You can also use Rectangle or
RoundedRectangle to create the indicator. Instead of changing the rotation angle, you can
modify the value of the offset to create an animation like this.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 214


Figure 5. Another example of the loading indicator

To create the animation, we overlay two rounded rectangles together. The rectangle on
top is much shorter than the one below. When the loading begins, we update its offset
value from -110 to 110.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 215


struct ContentView: View {

@State private var isLoading = false

var body: some View {


ZStack {

Text("Loading")
.font(.system(.body, design: .rounded))
.bold()
.offset(x: 0, y: -25)

RoundedRectangle(cornerRadius: 3)
.stroke(Color(.systemGray5), lineWidth: 3)
.frame(width: 250, height: 3)

RoundedRectangle(cornerRadius: 3)
.stroke(Color.green, lineWidth: 3)
.frame(width: 30, height: 3)
.offset(x: isLoading ? 110 : -110, y: 0)
.animation(.linear(duration: 1).repeatForever(autoreverses: false)
, value: isLoading)
}
.onAppear() {
self.isLoading = true
}
}
}

This moves the green rectangle along the line. When you repeat the same animation over
and over, it becomes a loading animation. Figure 6 illustrates the offset values.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 216


Figure 6. Another example of the loading indicator

Creating a Progress Indicator


The loading indicator provides feedback to the user that the app is working on
something, but it doesn't show the actual progress of a task. If you need to provide users
with more information about the progress of a task, you may want to build a progress
indicator.

Figure 7. A progress indicator

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 217


Building a progress indicator is similar to creating a loading indicator, but in this case,
you need a state variable to keep track of the progress. Here is a code snippet for creating
the progress indicator:

struct ContentView: View {


@State private var progress: CGFloat = 0.0

var body: some View {

ZStack {
Text("\(Int(progress * 100))%")
.font(.system(.title, design: .rounded))
.bold()

Circle()
.stroke(Color(.systemGray5), lineWidth: 10)
.frame(width: 150, height: 150)

Circle()
.trim(from: 0, to: progress)
.stroke(Color.green, lineWidth: 10)
.frame(width: 150, height: 150)
.rotationEffect(Angle(degrees: -90))
}
.onAppear() {
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in
self.progress += 0.05
print(self.progress)
if self.progress >= 1.0 {
timer.invalidate()
}
}
}
}
}

Instead of using a boolean state variable, we use a floating-point number ( progress ) to


store the status of the progress. To display the progress, we set the trim modifier with
the value of progress , which determines the portion of the circle to be shown. In a real-

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 218


world application, you would update the value of progress to reflect the actual progress
of an operation. For this demo, we used a timer to increment the progress by every half
second.

Delaying an Animation
Not only does the SwiftUI framework allow you to control the duration of an animation,
you can also delay an animation through the delay function like this:

Animation.default.delay(1.0)

This will introduce a 1-second delay before starting the animation. The delay function
can be applied to other animations as well.

By combining different values for duration and delay, you can create interesting
animations, such as the dot loading indicator shown below.

Figure 8. A dot loading indicator

This indicator consists of five dots, each of which is animated to scale up and down with
different time delays. Here is the code implementation:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 219


struct ContentView: View {
@State private var isLoading = false

var body: some View {


HStack {
ForEach(0...4, id: \.self) { index in
Circle()
.frame(width: 10, height: 10)
.foregroundStyle(.green)
.scaleEffect(self.isLoading ? 0 : 1)
.animation(.linear(duration: 0.6).repeatForever().delay(0.2 *
Double(index)), value: isLoading)
}
}
.onAppear() {
self.isLoading = true
}
}
}

We first use an HStack to lay out the circles horizontally. Since all five circles (dots) are
the same size and color, we use ForEach to create the circles. The scaleEffect modifier is
used to scale the circle's size. By default, it is set to 1, which is its original size. When the
loading starts, the value is updated to 0, which minimizes the dot.

The line of code for rendering the animation may appear complicated, so let's break it
down and examine it step by step:

.animation(.linear(duration: 0.6).repeatForever().delay(0.2 * Double(index)), valu


e: isLoading)

The first part of the code snippet creates a linear animation with a duration of 0.6
seconds. To make the animation run repeatedly, the repeatForever function is called.

Without the delay function, all the dots would scale up and down simultaneously.
However, this is not the desired effect. Each dot should resize independently instead of
all at once. To achieve this, the delay function is called, and a different delay value is

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 220


used for each dot, based on its order in the row.

Feel free to experiment with different values for the duration and delay to customize the
animation to your liking.

Transforming a Rectangle into Circle


Sometimes, you may want to smoothly transform one shape, such as a rectangle, into
another shape, such as a circle. With the built-in shapes and animations in SwiftUI, you
can easily create this type of transformation, as shown in figure 9.

Figure 9. Morphing a rectangle into a circle

The trick to morphing a rectangle into a circle is to use the RoundedRectangle shape and
animate the change of the corner radius. When the corner radius is set to half of the
rectangle's width, assuming the width and height are equal, it transforms into a circle.
Here is the implementation of the morphing button:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 221


struct ContentView: View {
@State private var recordBegin = false
@State private var recording = false

var body: some View {


ZStack {

RoundedRectangle(cornerRadius: recordBegin ? 30 : 5)
.frame(width: recordBegin ? 60 : 250, height: 60)
.foregroundColor(recordBegin ? .red : .green)
.overlay(
Image(systemName: "mic.fill")
.font(.system(.title))
.foregroundStyle(.white)
.scaleEffect(recording ? 0.7 : 1)
)

RoundedRectangle(cornerRadius: recordBegin ? 35 : 10)


.trim(from: 0, to: recordBegin ? 0.0001 : 1)
.stroke(lineWidth: 5)
.frame(width: recordBegin ? 70 : 260, height: 70)
.foregroundStyle(.green)

}
.onTapGesture {
withAnimation(.default) {
self.recordBegin.toggle()
}

withAnimation(.default.repeatForever().delay(0.5)) {
self.recording.toggle()
}
}
}
}

We have two state variables here: recordBegin and recording , which control two
separate animations. The first variable controls the morphing of the button. As explained
before, we utilize the corner radius for the transformation. Initially, the width of the

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 222


rectangle is set to 250 points. When a user taps the rectangle to trigger the
transformation, the frame's width is changed to 60 points. Alongside this change, the
corner radius is set to 30 points, which is half of the width.

This is how we transform a rectangle into a circle. SwiftUI automatically renders the
animation of this transformation.

The recording state variable handles the scaling of the microphone image. When in the
recording state, we change the scaling ratio from 1 to 0.7. By repeatedly running the same
animation, it creates the pulsing animation effect.

Please note that the code above uses the explicit approach to animate the views.
However, it's not mandatory. If you prefer, you can use the implicit animation approach
to achieve the same result.

Understanding Transitions
What we have discussed so far is animating a view that already exists in the view
hierarchy. We achieved this by scaling the view up and down to animate its size.

SwiftUI allows developers to do more than that. You can define how a view is inserted or
removed from the view hierarchy, creating smooth transitions. In SwiftUI, this is known
as transitions. By default, the framework uses fade-in and fade-out transitions for view
insertion and removal. Additionally, SwiftUI provides several built-in transitions such as
slide, move, opacity, and more. You also have the freedom to develop your own custom
transitions or combine different types of transitions to create unique effects.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 223


Figure 10. A sample transition created using SwiftUI

Building a Simple Transition


Let's take a look at a simple example to better understand what a transition is and how it
works with animations. Create a new project named SwiftUITransition and update the
ContentView like this:

Let's explore a simple example to gain a better understanding of transitions and how they
work in conjunction with animations. To begin, create a new project named
SwiftUITransition and update the ContentView with the following code snippet:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 224


struct ContentView: View {

var body: some View {


VStack {
RoundedRectangle(cornerRadius: 10)
.frame(width: 300, height: 300)
.foregroundStyle(.green)
.overlay(
Text("Show details")
.font(.system(.largeTitle, design: .rounded))
.bold()
.foregroundStyle(.white)

RoundedRectangle(cornerRadius: 10)
.frame(width: 300, height: 300)
.foregroundStyle(.purple)
.overlay(
Text("Well, here is the details")
.font(.system(.largeTitle, design: .rounded))
.bold()
.foregroundStyle(.white)
)
}
}
}

In the code above, we lay out two squares vertically using VStack . At first, the purple
rectangle should be hidden. It's displayed only when a user taps the green rectangle (i.e.
Show details). In order to show the purple square, we need to make the green square
tappable.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 225


Figure 11. Layout two rectangles vertically

To do that, we need to declare a state variable to determine whether the purple square is
shown or not. Insert this line of code in ContentView :

@State private var show = false

Next, to hide the purple square, we wrap the purple square within a if clause like this:

if show {
RoundedRectangle(cornerRadius: 10)
.frame(width: 300, height: 300)
.foregroundStyle(.purple)
.overlay(
Text("Well, here is the details")
.font(.system(.largeTitle, design: .rounded))
.bold()
.foregroundStyle(.white)
)
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 226


In the VStack , we add the onTapGesture function to detect taps on the green rectangle
and trigger an animation for the state change. It's important to associate the transition
with an animation for it to work properly.

.onTapGesture {
withAnimation(.default) {
self.show.toggle()
}
}

Once a user taps the stack, we toggle the show variable to display the purple square.
When you run the app in the simulator or preview canvas, you will only see the green
square initially. Tapping the green square will smoothly fade in and out the purple
rectangle, thanks to the transition animation.

Figure 12. The fade transition

As mentioned, if you do not specify the transition you want to use, SwiftUI will default to
the fade in and out transition. To use an alternative transition, you can attach the
transition modifier to the purple square, like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 227


if show {
RoundedRectangle(cornerRadius: 10)
.frame(width: 300, height: 300)
.foregroundStyle(.purple)
.overlay(
Text("Well, here is the details")
.font(.system(.largeTitle, design: .rounded))
.bold()
.foregroundStyle(.white)
)
.transition(.scale(scale: 0, anchor: .bottom))
}

The transition modifier takes a parameter of type AnyTransition . In this case, we use
the scale transition with the anchor set to .bottom . That's all you need to modify the
transition. To test the animations, it is recommended to run the app in a simulator rather
than relying solely on the preview canvas, as the preview may not always render the
transition correctly.

Figure 13. Scaling transition

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 228


In addition to .scale , the SwiftUI framework comes with several built-in transitions
including .opaque , .offset , .move , and .slide . Replace the .scale transition with
the .offset transition like this:

.transition(.offset(x: -600, y: 0))

This time, the purple square slides in from the left when it's inserted into the VStack .

Combining Transitions
You can combine two or more transitions together by calling the combined(with:) method
to create an even more slick transition. For example, to combine the offset and scale
animation, you write the code like this:

.transition(.offset(x: -600, y: 0).combined(with: .scale))

Here is another example that combines three transitions:

.transition(.offset(x: -600, y: 0).combined(with: .scale).combined(with: .opacity)


)

Sometimes you need to define a reusable animation. You can define an extension on
AnyTransition like this:

extension AnyTransition {
static var offsetScaleOpacity: AnyTransition {
AnyTransition.offset(x: -600, y: 0).combined(with: .scale).combined(with:
.opacity)
}
}

Then you can use the offsetScaleOpacity animation in the transition modifier directly:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 229


.transition(.offsetScaleOpacity)

Test the transition again in the preview canvas or using the simulator. Does it look great?

Figure 14. Combining the scale, offset, and opacity transition

Asymmetric Transitions
The transitions we just discussed are all symmetric, meaning that the insertion and
removal of the view use the same transition. For example, if you apply the scale transition
to a view, SwiftUI scales up the view when it's inserted into the view hierarchy, and scales
it back down to the original size when it's removed.

But what if you want to use a scale transition when the view is inserted and an offset
transition when the view is removed? This is known as Asymmetric Transitions in
SwiftUI. Using this type of transition is straightforward. You just need to call the
.asymmetric method and specify both the insertion and removal transitions. Here is the
sample code:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 230


.transition(.asymmetric(insertion: .scale(scale: 0, anchor: .bottom), removal: .of
fset(x: -600, y: 0)))

Again, if you need to reuse the transition, you can define an extension on AnyTransition

like this:

extension AnyTransition {
static var scaleAndOffset: AnyTransition {
AnyTransition.asymmetric(
insertion: .scale(scale: 0, anchor: .bottom),
removal: .offset(x: -600, y: 00)
)
}
}

Add this code after the ContentView block and before the #Preview block. Run the app
using the built-in simulator or in the preview canvas. You should see the scale transition
when the purple square appears on screen. When you tap the rectangles again, the purple
rectangle will slide off the screen.

Figure 15. Assymetric transition demo

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 231


Exercise #1: Using Animation and Transition to Build a
Fancy Button
Now that you have learned transitions and animations, let me challenge you to build a
fancy button that displays the current state of an operation. If you can't see the animation
below, please click this link (https://www.appcoda.com/wp-
content/uploads/2019/10/swiftui-animation-16.gif) to see the animation.

Figure 16. A fancy button

This button has three states:

The original state: it shows a Submit button in green.


The processing state: it displays a rotating circle and updates its label to Processing.
The complete state: it displays the Done button in red.

It's quite a challenging project that will test your knowledge of SwiftUI animation and
transitions. You will need to combine everything you've learned so far to work out the
solution.

In the demo button shown in figure 16, the processing takes around 4 seconds. You do
not need to perform a real operation. To help you with this exercise, I've provided the
following code to simulate an operation.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 232


private func startProcessing() {
self.loading = true

// Simulate an operation by using DispatchQueue.main.asyncAfter


// In a real world project, you will perform a task here.
// When the task finishes, you set the completed status to true
DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
self.completed = true
}
}

Exercise #2: Animated View Transitions


You've learned how to implement view transitions. Now, let's integrate a transition with
the card view project that you built in chapter 5 and create a view transition as shown
below: when a user taps the card, the current view will scale down and fade away. The
next view will be brought to the front with a scale-up animation.

Figure 17. Animated view transition

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 233


If you can't understand the animation above, you can click this link
(https://www.appcoda.com/wp-content/uploads/2019/10/swiftui-view-animation.gif)
to see the desired result.

Summary
Animation plays a special role in mobile UI design. Well-thought-out animations
improve user experience and add meaning to UI interactions. A seamless and effortless
transition between two views can delight and impress your users. With over 2 million
apps on the App Store, it can be challenging to make your app stand out. However, a
well-designed UI with animations can make a significant difference.

Even for experienced developers, creating slick animations is not an easy task.
Fortunately, the SwiftUI framework has simplified the development of UI animations
and transitions. You simply define how the view should look at the beginning and the
end, and SwiftUI takes care of the rest, rendering smooth and visually appealing
animations.

In this chapter, I have covered the basics of animation and transition in SwiftUI. As you
can see, you have already built some delightful animations and transitions with just a few
lines of code.

I hope you have enjoyed reading this chapter and find the techniques useful. For
reference, you can download the sample projects and exercise solutions below:

Animation Demo project


(https://www.appcoda.com/resources/swiftui5/SwiftUIAnimation.zip)

Transition Demo project


(https://www.appcoda.com/resources/swiftui5/SwiftUITransition.zip)

Exercise #1
(https://www.appcoda.com/resources/swiftui5/SwiftUIAnimationExercise1.zip)

Exercise #2
(https://www.appcoda.com/resources/swiftui5/SwiftUIAnimationExercise2.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 234


Chapter 10
Understanding List, ForEach and
Identifiable
In UIKit, UITableView is one of the most common UI controls in iOS. If you have
experience developing apps with UIKit, you are familiar with the use of table views for
presenting lists of data. This UI control is commonly used in content-based apps such as
newspaper apps. Figure 1 illustrates some examples of list/table views that can be found
in popular apps like Instagram, Twitter, Airbnb, and Apple News.

Figure 1. Sample list views

Instead of using UITableView , we use List in SwiftUI to present rows of data. If you've
built a table view with UIKit before, you are aware that it can take you significant effort to
implement a simple table view and even more effort to customize the layout of the cells.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 235


SwiftUI simplifies this entire process. With just a few lines of code, you can easily list
data in table form. Even if you need to customize the layout of the rows, it only requires
minimal effort.

Feeling confused? No worries. You'll understand what I mean in a while.

In this chapter, we will start with a simple list. Once you grasp the basics, I will show you
how to present a list of data with a more complex layout, as shown in Figure 2.

Figure 2. Building a simple and complex list

Creating a Simple List


Let's begin with a simple list. First, fire up Xcode and create a new project using the App
template. In the next screen, set the product name to SwiftUIList (or whatever name you
like) and fill in all the required values. Make sure you select SwiftUI for the Interface

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 236


option.

Xcode will generate the "Hello World" code in the ContentView.swift file. Replace the
"Hello World" text object with the following:

struct ContentView: View {


var body: some View {
List {
Text("Item 1")
Text("Item 2")
Text("Item 3")
Text("Item 4")
}
}
}

That's all the code you need to build a simple list or table. When you embed the text
views in a List , the list view will present the data in rows. Here, each row shows a text
view with different description.

Figure 3. Creating a simple list

The same code snippet can be written like this using ForEach :

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 237


struct ContentView: View {
var body: some View {
List {
ForEach(1...4, id: \.self) { index in
Text("Item \(index)")
}
}
}
}

Since the text views are very similar, you can use ForEach in SwiftUI to create views in a
loop.

A structure that computes views on demand from an underlying collection of of


identified data.

- Apple's official documentation


(https://developer.apple.com/documentation/swiftui/foreach)

You can provide ForEach with a collection of data or a range. However, it's important to
inform ForEach how to uniquely identify each item in the collection. This is achieved
through the id parameter. Why does ForEach need this identifier? SwiftUI has the
ability to automatically update the UI when some or all items in the collection change. To
accomplish this, it needs a unique identifier to track and update the items accurately.

In the code snippet above, we pass a range of values to ForEach to iterate through. The
identifier is set to the value itself (i.e., 1, 2, 3, or 4). The index parameter holds the
current value of the loop. For example, if it starts with a value of 1, the index parameter
will be 1.

Within the closure, you define the code that renders the views. In this case, we create a
text view with a description that varies based on the value of index in the loop. This is
how we create four items in the list with different titles.

Now, let me show you an alternative technique. The same code snippet can be further
simplified as follows:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 238


struct ContentView: View {
var body: some View {
List {
ForEach(1...4, id: \.self) {
Text("Item \($0)")
}
}
}
}

You can omit the index parameter and use the shorthand $0 , which refers to the first
parameter of the closure.

Let's further simplify the code by directly passing the collection of data to the List view.
Here is the updated code:

struct ContentView: View {


var body: some View {
List(1...4, id: \.self) {
Text("Item \($0)")
}
}
}

As you can see, you only need a couple lines of code to build a simple list/table.

Creating a List View with Text and Images


Now that you know how to create a simple list, let's explore how to work with a more
complex layout. In most cases, the items in a list view contain both text and images. How
do you implement that? If you're familiar with Image , Text , VStack , and HStack , you
should have some ideas about how to create a complex list.

If you've read our book, Beginning iOS Programming with Swift, this example should be
very familiar to you. Let's use it as an example and see how easy it is to build the same
table using SwiftUI.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 239


Figure 4. A simple table view showing rows of restaurants

To build the table using UIKit, you would need to create a table view or table view
controller and then customize the prototype cell. Additionally, you would have to code
the table view data source to provide the data. That's quite a few steps to build a table UI.
Let's now see how the same table view is implemented in SwiftUI.

First, download the image pack from


https://www.appcoda.com/resources/swiftui/SwiftUISimpleTableImages.zip. Unpack
the zip file and import all the images to the asset catalog.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 240


Figure 5. Import images to the asset catalog

Now, switch over to ContentView.swift to code the UI. First, let's declare two arrays in
ContentView . These arrays will be used to store restaurant names and images. Here is the
complete code snippet:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 241


struct ContentView: View {

var restaurantNames = ["Cafe Deadend", "Homei", "Teakha", "Cafe Loisl", "Petit


e Oyster", "For Kee Restaurant", "Po's Atelier", "Bourke Street Bakery", "Haigh's
Chocolate", "Palomino Espresso", "Upstate", "Traif", "Graham Avenue Meats And Deli"
, "Waffle & Wolf", "Five Leaves", "Cafe Lore", "Confessional", "Barrafina", "Donos
tia", "Royal Oak", "CASK Pub and Kitchen"]

var restaurantImages = ["cafedeadend", "homei", "teakha", "cafeloisl", "petite


oyster", "forkeerestaurant", "posatelier", "bourkestreetbakery", "haighschocolate"
, "palominoespresso", "upstate", "traif", "grahamavenuemeats", "wafflewolf", "five
leaves", "cafelore", "confessional", "barrafina", "donostia", "royaloak", "caskpub
kitchen"]

var body: some View {


List(1...4, id: \.self) {
Text("Item \($0)")
}
}
}

Both arrays have the same number of items. The restaurantNames array stores the names
of the restaurants, and the restaurantImages array stores the names of the images you
just imported. To create a list view like the one shown in figure 4, all you need to do is
update the body property like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 242


var body: some View {
List(restaurantNames.indices, id: \.self) { index in
HStack {
Image(self.restaurantImages[index])
.resizable()
.frame(width: 40, height: 40)
.cornerRadius(5)
Text(self.restaurantNames[index])
}
}
.listStyle(.plain)
}

We've made a few changes in the code. First, instead of a fixed range, we pass the array of
restaurant names (i.e., restaurantNames.indices ) to the List view. The restaurantNames

array has 21 items, so we'll have a range from 0 to 20 (arrays are 0-indexed). This
approach works when both arrays are of the same size, as the index of one array is used
as an index for the other array.

In the closure, the code was updated to create the row layout. I won't go into the details,
as the code is similar to the stack views we've created before. To change the style of the
List view, we applied the listStyle modifier and set the style to plain .

With less than 10 lines of code, we have created a list (or table) view with a custom
layout.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 243


Figure 6. A list view with custom row layout

Working with a Collection of Data


As mentioned before, List can take in a range or a collection of data. You've learned
how to work with ranges. Let's now see how to use List with an array of restaurant
objects.

Instead of holding the restaurant data in two separate arrays, we'll create a Restaurant

struct to better organize the data. This struct has two properties: name and image . Insert
the following code at the end of the ContentView.swift file:

struct Restaurant {
var name: String
var image: String
}

With this struct, we can combine both restaurantNames and restaurantImages arrays into
a single array. Delete the restaurantNames and restaurantImages variables and replace
them with this variable in the ContentView :

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 244


var restaurants = [ Restaurant(name: "Cafe Deadend", image: "cafedeadend"),
Restaurant(name: "Homei", image: "homei"),
Restaurant(name: "Teakha", image: "teakha"),
Restaurant(name: "Cafe Loisl", image: "cafeloisl"),
Restaurant(name: "Petite Oyster", image: "petiteoyster"),
Restaurant(name: "For Kee Restaurant", image: "forkeerestaurant"),
Restaurant(name: "Po's Atelier", image: "posatelier"),
Restaurant(name: "Bourke Street Bakery", image: "bourkestreetbakery"
),
Restaurant(name: "Haigh's Chocolate", image: "haighschocolate"),
Restaurant(name: "Palomino Espresso", image: "palominoespresso"),
Restaurant(name: "Upstate", image: "upstate"),
Restaurant(name: "Traif", image: "traif"),
Restaurant(name: "Graham Avenue Meats And Deli", image: "grahamaven
uemeats"),
Restaurant(name: "Waffle & Wolf", image: "wafflewolf"),
Restaurant(name: "Five Leaves", image: "fiveleaves"),
Restaurant(name: "Cafe Lore", image: "cafelore"),
Restaurant(name: "Confessional", image: "confessional"),
Restaurant(name: "Barrafina", image: "barrafina"),
Restaurant(name: "Donostia", image: "donostia"),
Restaurant(name: "Royal Oak", image: "royaloak"),
Restaurant(name: "CASK Pub and Kitchen", image: "caskpubkitchen")
]

If you're new to Swift, each item in the array represents a restaurant object containing
both the name and image for each restaurant. Once you have replaced the array, you may
encounter an error in Xcode, complaining that the restaurantNames variable is missing.
This is expected because we have just removed it.

Now update the body variable like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 245


var body: some View {
List(restaurants, id: \.name) { restaurant in
HStack {
Image(restaurant.image)
.resizable()
.frame(width: 40, height: 40)
.cornerRadius(5)
Text(restaurant.name)
}
}
.listStyle(.plain)
}

Take a look at the parameters we pass to List . Instead of passing the range, we now
pass the restaurants array and specify the name property as the identifier for each
restaurant. The List will iterate through the array and provide us with the current
restaurant being processed in the closure. Within the closure, we define how we want to
present each restaurant row. In this case, we use a HStack to display both the restaurant
image and name.

The resulting UI remains unchanged, but the underlying code has been modified to
leverage List with a collection of data.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 246


Figure 7. Same UI as figure 6

Working with the Identifiable Protocol


To help you better understand the purpose of the id parameter in List , let's make a
minor change to the restaurants array. Currently, we are using the name of the
restaurant as the identifier. However, what happens if we have two records with the same
restaurant name? To illustrate this, let's change the 11th item in the restaurants array
from Upstate to Homei:

Restaurant(name: "Homei", image: "upstate")

Take note that we are only changing the value of the name property to "Homei" while
keeping the image as "upstate" for the 11th item in the restaurants array. Please check
the preview pane again to see the result.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 247


Figure 8. Two restaurants have the same name

Do you see the issue in figure 8? We now have two records with the name Homei, and as
a result, both records show the same image. In the code, we told the List to use the
restaurant's name as the unique identifier. However, when two restaurants have the
same name, iOS considers them to be the same restaurant and reuses the same view.

So, how do you fix this issue?

That's pretty easy. You need to provide each restaurant with a unique identifier. You can
update the Restaurant struct by adding a property called id of type UUID . This will
ensure that each restaurant has a unique identifier. Here's the updated code:

struct Restaurant {
var id = UUID()
var name: String
var image: String
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 248


In the code, we added an id property to the Restaurant struct and initialized it with a
unique identifier using the UUID() function. This ensures that each restaurant has a
unique ID. A UUID is composed of 128-bit number, so theoretically the chance of having
two same indentifers is almost zero.

To make things work correctly, we need to update the List view by changing the value
of the id parameter from \.name to \.id . This tells the List to use the id property
of each restaurant as the identifier. Here's the updated code:

List(restaurants, id: \.id)

This tells the List view to use the id property of the restaurants as the unique
identifier. If you take a look at the preview, you'll see that the second Homei record now
correctly shows the upstate image.

Figure 9. The bug is now fixed showing the correct image

We can further simplify the code by making the Restaurant struct conform to the
Identifiable protocol. This protocol has only one requirement: the type implementing
the protocol should have some sort of id as a unique identifier. Update the Restaurant

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 249


struct to implement the Identifiable protocol like this:

struct Restaurant: Identifiable {


var id = UUID()
var name: String
var image: String
}

Since Restaurant already provides a unique id property, it conforms to the protocol


requirement.

What's the purpose of implementing the Identifiable protocol here? By conforming to


the Identifiable protocol, you can initialize the List without the need to explicitly
specify the id parameter. This simplifies the code! Here is the updated code:

List(restaurants) { restaurant in
HStack {
Image(restaurant.image)
.resizable()
.frame(width: 40, height: 40)
.cornerRadius(5)
Text(restaurant.name)
}
.listStyle(.plain)
}

This is how you use List to present a collection of data.

Refactoring the Code


The code works very well. That said, it's always a good practice to refactor the code to
improve its structure. We can extract the HStack into a separate struct to make it more
reusable. To do this, hold the control key and click on HStack . Then, select Extract
Subview to extract the code. Finally, rename the struct to BasicImageRow .

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 250


Figure 10. Extracting subview

Xcode immediately shows an error after making the change because the extracted
subview, BasicImageRow , doesn't have a restaurant property. To resolve this, update the
BasicImageRow struct to declare the restaurant property, like this:

struct BasicImageRow: View {


var restaurant: Restaurant

var body: some View {


HStack {
Image(restaurant.image)
.resizable()
.frame(width: 40, height: 40)
.cornerRadius(5)
Text(restaurant.name)
}
}
}

Next, update the List view to pass the restaurant parameter:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 251


List(restaurants) { restaurant in
BasicImageRow(restaurant: restaurant)
}

Now everything should work without errors. The list view still looks the same, but the
underlying code is more readable and organized. It's also more adaptable to code
changes. For example, if you decide to create another layout for the row, you can easily
do so by updating the BasicImageRow struct, like this:

struct FullImageRow: View {


var restaurant: Restaurant

var body: some View {


ZStack {
Image(restaurant.image)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(height: 200)
.cornerRadius(10)
.overlay(
Rectangle()
.foregroundStyle(.black)
.cornerRadius(10)
.opacity(0.2)
)

Text(restaurant.name)
.font(.system(.title, design: .rounded))
.fontWeight(.black)
.foregroundStyle(.white)
}
}
}

This row layout is designed to show a larger image with the restaurant name overlaid on
top. Since we've refactored our code, it's very easy to change the app to use the new
layout. All you need to do is replace BasicImageRow with FullImageRow in the closure of
the List , like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 252


List(restaurants) { restaurant in
FullImageRow(restaurant: restaurant)
}

By changing one line of code, the app instantly switches to another layout.

Figure 11. Changing the row layout

You can further mix the row layouts to build a more interesting UI. For example, our list
can use FullImageRow for the first two rows of data, and the rest of the rows will utilize
the BasicImageRow . To achieve this, you can update the List like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 253


List {
ForEach(restaurants.indices, id: \.self) { index in
if (0...1).contains(index) {
FullImageRow(restaurant: self.restaurants[index])
} else {
BasicImageRow(restaurant: self.restaurants[index])
}
}
}
.listStyle(.plain)

Since we need to retrieve the index of the rows, we pass the List the index range of the
restaurant data. In the closure, we check the value of index to determine which row
layout to use.

Figure 12. Building a list view with two different row layouts

Changing the tint color of the line separator

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 254


Starting from iOS 15, Apple provided options for developers to customize the appearance
of the list view. To change the tint color of the line separators, you can use the
listRowSeparatorTint modifier like this:

List(restaurants) { restaurant in
ForEach(restaurants.indices, id: \.self) { index in
if (0...1).contains(index) {
FullImageRow(restaurant: self.restaurants[index])
} else {
BasicImageRow(restaurant: self.restaurants[index])
}
}
.listRowSeparatorTint(.green)
}
.listStyle(.plain)

In the code above, we change the color of the line separators to green.

Hiding the List Separators


You can also use the listRowSeparator modifier and set its value to .hidden to hide the
separators. Here is an example:

List {
ForEach(restaurants.indices) { index in
if (0...1).contains(index) {
FullImageRow(restaurant: self.restaurants[index])
} else {
BasicImageRow(restaurant: self.restaurants[index])
}
}

.listRowSeparator(.hidden)
}
.listStyle(.plain)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 255


The listRowSeparator modifier should be embedded inside the List view. To make the
line separators appear again, you can set the value of the modifier to .visible .
Alternatively, you can simply remove the listRowSeparator modifier.

If you want to have finer control over the line separators, you can use an alternate version
of .listRowSeparator by specifying the edges parameter. For example, if you want to
keep the separator only at the top of the list view, you can write the code like this:

.listRowSeparator(.hidden, edges: .bottom)

Customizing the Background of the Scrolling Area

Figure 13. Changing the color of the scrollable area

Starting from iOS 16, you can customize the color of the scrollable area of the list view.
Simply set the background color using the background modifier and attach the
scrollContentBackground modifier to the List view. By setting the parameter to .hidden ,
you can change the scroll view's background to yellow.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 256


List(restaurants) { restaurant in
.
.
.
}
.background(.yellow)
.scrollContentBackground(.hidden)

Other than using a solid color, you can use an image as the background. Update the code
like this to give it a try:

List(restaurants) { restaurant in
.
.
.
}
.background {
Image("homei")
.resizable()
.scaledToFill()
.clipped()
.ignoresSafeArea()
}
.scrollContentBackground(.hidden)

We use the background modifier to set the background image, and then we set the
scrollContentBackground modifier to .hidden to make the scrollable area transparent.

Exercise
Before you move on to the next chapter, challenge yourself by building the list view
shown in figure 13. It may appear complicated, but if you have a solid understanding of
this chapter, you should be able to build the UI. Take some time to work on this exercise.
I guarantee you'll learn a lot!

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 257


To save you time searching for your own images, you can download the image pack for
this exercise from
https://www.appcoda.com/resources/swiftui/SwiftUIArticleImages.zip.

Figure 13. Building a list view with complex row layout

For reference, you can download the complete list project and the solution to the exercise
from the following links:

Demo project (https://www.appcoda.com/resources/swiftui5/SwiftUIList.zip)


Solution to exercise
(https://www.appcoda.com/resources/swiftui5/SwiftUIListExercise.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 258


Chapter 11
Working with Navigation UI and
Navigation Bar Customization
In most apps, you will have experienced a navigational interface. This type of UI typically
consists of a navigation bar and a list of data, allowing users to navigate to a detail view
by tapping on the content.

In UIKit, we implement this type of interface using UINavigationController . In SwiftUI,


Apple introduced the NavigationView component, which is now called NavigationStack in
iOS 16 (or up). In this chapter, I will guide you through the implementation of navigation
views and demonstrate how to perform customizations. As usual, we will work on a
couple of demo projects to provide you with hands-on experience with NavigationStack .

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 259


Figure 1. Sample navigation interface for our demo projects

Preparing the Starter Project


Let's get started and implement a demo project that we have built earlier with a
navigation UI. So, first download the starter project from
https://www.appcoda.com/resources/swiftui5/SwiftUINavigationListStarter.zip. Once
downloaded, open the project and check out the preview. You should be very familiar
with this demo app. It just displays a list of restaurants.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 260


Figure 2. The starter project should display a simple list view

What we're going to do is embed this list view in a navigation stack.

Implementing a Navigation Stack


Prior to iOS 16, the SwiftUI framework provided a view called NavigationView that
allowed you to create a navigation UI. To embed a list view in a NavigationView , you
simply needed to wrap the List with a NavigationView like this:

NavigationView {
List {
ForEach(restaurants) { restaurant in
BasicImageRow(restaurant: restaurant)
}
}
.listStyle(.plain)
}

In iOS 16 (or later), Apple replaced NavigationView with NavigationStack . Although you
can still use NavigationView to create a navigation view, it is recommended to use
NavigationStack as NavigationView will eventually be removed from the SDK.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 261


To create a navigation view using NavigationStack , you can write the same piece of code
like this:

NavigationStack {
List {
ForEach(restaurants) { restaurant in
BasicImageRow(restaurant: restaurant)
}
}
.listStyle(.plain)
}

Once you have made the change, you should see an empty navigation bar. To assign a
title to the bar, insert the navigationBarTitle modifier like below:

NavigationStack {
List {
ForEach(restaurants) { restaurant in
BasicImageRow(restaurant: restaurant)
}
}
.listStyle(.plain)

.navigationTitle("Restaurants")
}

Now the app has a navigation bar with a large title.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 262


Figure 3. A basic navigation UI

Passing Data to a Detail View Using NavigationLink


So far, we have added a navigation bar to the list view. Typically, a navigation interface is
used to allow users to navigate to a detail view, where they can view the details of a
selected item. For this demo, we will build a simple detail view that shows a larger image
of the restaurant.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 263


Figure 4. The content view and detail view

Let's start with the detail view. Insert the following code at the end of the
ContentView.swift file to create the detail view:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 264


struct RestaurantDetailView: View {
var restaurant: Restaurant

var body: some View {


VStack {
Image(restaurant.image)
.resizable()
.aspectRatio(contentMode: .fit)

Text(restaurant.name)
.font(.system(.title, design: .rounded))
.fontWeight(.black)

Spacer()
}
}
}

The detail view is similar to other SwiftUI views and conforms to the View protocol. Its
layout is simple, displaying the restaurant image and name. The RestaurantDetailView

struct also takes a Restaurant object as a parameter to retrieve the image and name of
the restaurant.

Now that the detail view is ready, the question is how to pass the selected restaurant from
the content view to this detail view?

SwiftUI provides a special button called NavigationLink , which can detect user touches
and trigger the navigation presentation. The basic usage of NavigationLink is as follows:

NavigationLink(destination: DetailView()) {
Text("Press me for details")
}

You specify the destination view in the destination parameter and customize its
appearance in the closure. For the demo app, the NavigationLink should navigate to the
RestaurantDetailView when any of the restaurants is tapped. To achieve this, we can apply
the NavigationLink to each of the rows. Update the List view as follows:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 265


List {
ForEach(restaurants) { restaurant in
NavigationLink(destination: RestaurantDetailView(restaurant: restaurant))
{
BasicImageRow(restaurant: restaurant)
}
}
}
.listStyle(.plain)

In the code above, we specify that the NavigationLink should navigate to the
RestaurantDetailView when users select a restaurant. We also pass the selected restaurant
to the detail view for display. With these few lines of code, we are able to build a
navigation interface and perform data passing seamlessly.

Figure 5. You should see the disclosure indicator for each row

In the canvas, you should notice that each row of data has been added with a disclosure
icon, indicating that it is a navigational link. In the preview canvas, you should be able to
navigate to the detail view after selecting one of the restaurants. Furthermore, you can

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 266


navigate back to the content view by clicking the back button, which is automatically
rendered by NavigationStack .

Customizing the Navigation Bar


First, let's talk about the display mode of the navigation bar. By default, the navigation
bar is set to appear as a large title. However, if you want to keep the navigation bar
compact and disable the use of the large title, you can add the
navigationBarTitleDisplayMode modifier right below navigationTitle .

.navigationBarTitleDisplayMode(.inline)

The parameter specifies the appearance of the navigation bar, determining whether it
should appear as a large title bar or a compact title. By default, it is set to .automatic ,
which means that the large title is used. In the code above, we set it to .inline ,
indicating that a compact bar should be used instead.

Figure 6. Setting the display mode to .inline to use the compact bar

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 267


Change the display mode to .automatic and the navigation bar will become a large title
bar again.

.navigationBarTitleDisplayMode(.automatic)

Configuring Font and Color


Next, let's change the title's font and color. Currently, SwiftUI does not provide a direct
modifier to configure the font and color of the navigation bar. However, we can utilize the
UINavigationBarAppearance API provided by UIKit to achieve this customization.

To change the title color to red and the font to Arial Rounded MT Bold, we can create a
UINavigationBarAppearance object in the init() function and configure the desired
attributes. Insert the following function in ContentView :

init() {
let navBarAppearance = UINavigationBarAppearance()
navBarAppearance.largeTitleTextAttributes = [.foregroundColor: UIColor.red, .f
ont: UIFont(name: "ArialRoundedMTBold", size: 35)!]
navBarAppearance.titleTextAttributes = [.foregroundColor: UIColor.red, .font:
UIFont(name: "ArialRoundedMTBold", size: 20)!]

UINavigationBar.appearance().standardAppearance = navBarAppearance
UINavigationBar.appearance().scrollEdgeAppearance = navBarAppearance
UINavigationBar.appearance().compactAppearance = navBarAppearance
}

The largeTitleTextAttributes property is used to configure the text attributes of the


large-size title, while the titleTextAttributes property is used for setting the text
attributes of the standard-size title. Once we have configured the navBarAppearance , we
assign it to the three appearance properties: standardAppearance , scrollEdgeAppearance ,
and compactAppearance .

If desired, you can create separate appearance objects for scrollEdgeAppearance and
compactAppearance and assign them individually.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 268


Figure 7. Changing the font type and color for both large-size and standard-size titles

Back Button Image and Color


The back button of the navigation view is typically set to blue by default, and it uses a
chevron icon to indicate "Go back." However, using the UINavigationBarAppearance API,
you can customize not only the color but also the indicator image of the back button.

Figure 8. A standard back button

Let's see how this customization works. To change the indicator image, you can call the
setBackIndicatorImage method and provide your own UIImage . Here, I'll set it to the
system image arrow.turn.up.left .

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 269


navBarAppearance.setBackIndicatorImage(UIImage(systemName: "arrow.turn.up.left"),
transitionMaskImage: UIImage(systemName: "arrow.turn.up.left"))

For the back button color, you can change it by setting the tint property like this:

NavigationStack {
.
.
.
}
.tint(.black)

Test the app again. The back button should be like that shown in figure 9.

Figure 9. Customizing the appearance of the back button

Custom Back Button


Instead of using the UIKit APIs to customize the back button, an alternative approach is
to hide the default back button and create our own back button in SwiftUI. To hide the
back button, you can use the .navigationBarBackButtonHidden modifier and set its value to

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 270


true . Here's an example of how you can do it in the detail view:

.navigationBarBackButtonHidden(true)

SwiftUI also provides a modifier called toolbar for creating your own navigation bar
items. For example, you can create a back button with the name of the selected
restaurant like this:

.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
dismiss()
} label: {
Text("\(Image(systemName: "chevron.left")) \(restaurant.name)")
.foregroundColor(.black)
}
}
}

In the closure of the .toolbar modifier, we create a ToolbarItem object with the
placement set to .navigationBarLeading . This specifies that the button should be placed
on the leading edge of the navigation bar.

To put the following code into action and update RestaurantDetailView like below:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 271


struct RestaurantDetailView: View {
@Environment(\.dismiss) var dismiss

var restaurant: Restaurant

var body: some View {


VStack {
Image(restaurant.image)
.resizable()
.aspectRatio(contentMode: .fit)

Text(restaurant.name)
.font(.system(.title, design: .rounded))
.fontWeight(.black)

Spacer()
}

.navigationBarBackButtonHidden(true)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
dismiss()
} label: {
Text("\(Image(systemName: "chevron.left")) \(restaurant.name)"
)
.foregroundColor(.black)
}

}
}
}
}

SwiftUI offers a wide range of built-in environment values. To dismiss the current view
and go back to the previous view, we can retrieve the environment value using the
.dismiss key. Then we can call the dismiss() function to dismiss the current view.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 272


Please note that the .dismiss environment key is only available on iOS 15 or later. If you
need to support an older version of iOS, you can use the environment key
.presentationMode :

@Environment(\.presentationMode) var presentationMode

Then you can call the dismiss function of the presentation mode like this:

presentationMode.wrappedValue.dismiss()

Now test the app in the preview canvas and select any of the restaurants. You will see a
back button with the restaurant name. Tapping the back button will navigate back to the
main screen.

Exercise
To ensure you understand how to build a navigation UI, I have an exercise for you. Please
download the starter project from
https://www.appcoda.com/resources/swiftui5/SwiftUINavigationStarter.zip. Once you
open the project, you'll see a demo app displaying a list of articles.

This project is similar to the one you've worked on before, with the main difference being
the addition of the Article.swift file. This file contains the articles array, which holds
sample data. If you examine the Article struct closely, you'll notice the inclusion of the
content property, which stores the full article content.

Your task is to embed the list in a navigation view and create a detail view. When a user
taps on one of the articles in the content view, it should navigate to the detail view,
displaying the full article. I will show the solution in the next section, but I encourage you
to try to come up with your own solution first.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 273


Figure 10. Building a navigation UI for a Reading app

Building the Detail View


Have you completed the exercise? The detail view is more complicated than the one we
built earlier. Let's see how to create it.

To better organize the code, instead of creating the detail view in the ContentView.swift

file, we will create a separate file for it. In the project navigator, right-click the
SwiftUINavigation folder and select New File... Choose the SwiftUI View template and
name the file ArticleDetailView.swift.

Since the detail view is going to display the full article , we need to have this property for
the caller to pass the article. So, declare an article property in ArticleDetailView :

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 274


var article: Article

Next, update the body like this to lay out the detail view:

var body: some View {


ScrollView {
VStack(alignment: .leading) {
Image(article.image)
.resizable()
.aspectRatio(contentMode: .fit)

Group {
Text(article.title)
.font(.system(.title, design: .rounded))
.fontWeight(.black)
.lineLimit(3)

Text("By \(article.author)".uppercased())
.font(.subheadline)
.foregroundColor(.secondary)
}
.padding(.bottom, 0)
.padding(.horizontal)

Text(article.content)
.font(.body)
.padding()
.lineLimit(1000)
.multilineTextAlignment(.leading)
}
}
}

We use a ScrollView to wrap all the views and enable scrollable content. I won't go over
the code line by line since you already understand how Text , Image , and VStack work.
However, I want to highlight the use of the Group modifier. This modifier allows you to

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 275


group multiple views together and apply a configuration to the group. In the code above,
we need to apply padding to both Text views. To avoid code duplication, we group both
views together and apply the padding.

Now, you may encounter an error in Xcode complaining about the #Preview section. The
preview won't work because we added the article property in ArticleDetailView . To fix
this error, you need to pass a sample article in the preview. Update the #Preview section
as follows to resolve the error:

#Preview {
ArticleDetailView(article: articles[0])
}

Here, we simply select the first article from the articles array for the preview. Feel free
to modify it to a different value if you want to preview other articles. Once you make this
change, the preview canvas should accurately render the detail view.

Figure 11. The detail view for showing the article

Let's try one more thing. Since this view is going to be embedded in a NavigationView ,
you can modify the preview code to visualize how it looks within a navigation view:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 276


#Preview {
NavigationStack {
ArticleDetailView(article: articles[0])

.navigationTitle("Article")
}
}

By updating the code, you will see a navigation bar in the preview canvas.

Now that we've completed the layout of the detail view, it's time to go back to
ContentView.swift to implement the navigation. Update the ContentView struct like this:

struct ContentView: View {

var body: some View {

NavigationStack {
List(articles) { article in
NavigationLink(destination: ArticleDetailView(article: article)) {
ArticleRow(article: article)
}

.listRowSeparator(.hidden)
}
.listStyle(.plain)

.navigationTitle("Your Reading")
}

}
}

In the code above, we embed the List view in a NavigationStack and apply a
NavigationLink to each of the rows. The destination of the navigation link is set to the
detail view we just created. With this setup, you can preview and interact with the app,
allowing you to navigate to the detail view by selecting an article from the list.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 277


Removing the Disclosure Indicator
The app functions correctly, but there are two issues that you may want to address.
Firstly, the disclosure indicator in the content view looks a bit out of place. To resolve
this, we will disable it. Secondly, there is an empty space appearing above the featured
image in the detail view. Let's address these issues one at a time.

Figure 12. Two issues in the current design

SwiftUI does not offer a direct option to disable or hide the disclosure indicator. To work
around this issue, we won't apply NavigationLink directly to the article row. Instead, we
will create a ZStack with two layers. Let's update the NavigationStack of the ContentView

as follows:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 278


NavigationStack {
List(articles) { article in
ZStack {
ArticleRow(article: article)

NavigationLink(destination: ArticleDetailView(article: article)) {


EmptyView()
}
.opacity(0)

.listRowSeparator(.hidden)
}
}
.listStyle(.plain)

.navigationTitle("Your Reading")
}

The lower layer represents the article row, while the upper layer is an empty view. By
applying the NavigationLink to the empty view, we prevent iOS from rendering the
disclosure button. Once you make this change, the disclosure indicator will disappear
while still allowing navigation to the detail view.

Now let's discuss the root cause of the second issue.

Please switch over to ArticleDetailView.swift . Although I didn't mention the issue during
the design of the detail view, you may have noticed it in the preview (see figure 13).

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 279


Figure 13. Empty space in the header

The reason for the empty space above the image is the presence of the navigation bar.
This space is actually occupied by a large-size navigation bar with a blank title. When the
app navigates from the content view to the detail view, the navigation bar transitions to a
standard-size bar. To resolve this issue, we can explicitly specify the use of a standard-
size navigation bar.

Insert the following line of code after the closing bracket of the ScrollView :

.navigationBarTitleDisplayMode(.inline)

By setting the navigation bar to the inline mode, the navigation bar will be minimized,
resulting in a more compact appearance. Please proceed to ContentView.swift and test
the app again. The detail view should now look much better without the empty space
above the image.

An even more Elegant UI with a Custom Back Button

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 280


Though you can customize the back button indicator image using a built-in property,
sometimes you may want to build a custom back button that navigates back to the
content view. The question is how can it be done programmatically?

In this last section, I want to show you how to build an even more elegant detail view by
hiding the navigation bar and building your own back button. First, let's take a look at the
final design displayed in figure 14. Doesn't it look great?

Figure 14. The revised design of the detail view

To lay out this screen, we have to tackle two issues:

1. Extending the scroll view to the very top of the screen


2. Creating a custom back button and triggering the navigation programmatically

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 281


iOS has a concept known as safe areas to aid in the layout of views. Safe areas help
ensure that views are positioned within the visible portion of the interface. For example,
safe areas prevent views from hiding the status bar. If your UI includes a navigation bar,
the safe area will automatically be adjusted to prevent you from positioning views that
would otherwise hide the navigation bar.

![Figure 15. Safe areas](images/navigation/swiftui-navigation-15.jpg)

To position content that extends outside the safe areas, you can use the ignoresSafeArea

modifier. In our project, we want the scroll view to extend beyond the top edge of the safe
area. To achieve this, we apply the modifier as follows:

.ignoresSafeArea(.all, edges: .top)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 282


The ignoresSafeArea modifier can also accept other values such as .bottom and
.leading for the edges parameter. If you want to ignore the entire safe area, you can
simply use .ignoresSafeArea() . By applying this modifier to the ScrollView , we can hide
the navigation bar and create a visually appealing detail view.

Figure 16. Applying the modifiers to the scroll view

Now let's move on to the second issue, which is creating our own back button. This is a
bit trickier but still achievable. Here's how we can implement it:

1. Hide the original back button


2. Create a custom button and assign it as the left button of the navigation bar

To hide the default back button, SwiftUI provides the navigationBarBackButtonHidden

modifier. Setting its value to true will hide the back button:

.navigationBarBackButtonHidden(true)

Once the back button is hidden, you can replace it with your own button. The toolbar

modifier allows you to configure the navigation bar items. In the closure, we create the
custom back button using ToolbarItem and assign the button as the left button of the

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 283


navigation bar. Here is the code:

.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
// Navigate to the previous screen
}) {
Image(systemName: "chevron.left.circle.fill")
.font(.largeTitle)
}
.tint(.white)
}
}

You can attach the above modifiers to the ScrollView . Once the change is applied, you
should see our custom back button in the preview canvas.

Figure 17. Creating our own back button

You may have noticed that the action closure of the button was left empty. The back
button has been laid out nicely but the problem is that it doesn't function!

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 284


The original back button rendered by NavigationView can automatically navigate back to
the previous screen. We need to programmatically navigate back. Thanks to the
environment values built into the SwiftUI framework. You can refer to an environment
binding named dismiss for dismissing the current view.

Now declare a dismiss variable in ArticleDetailView to capture the environment value:

@Environment(\.dismiss) var dismiss

Next, in the action of our custom back button, insert this line of code:

dismiss()

Here, we call the dismiss() method to dismiss the detail view when the back button is
tapped. Run the app and test it again. You should now be able to navigate between the
content view and the detail view.

Summary
Navigation UI is a common and crucial concept in mobile app development.
Understanding how to navigate between screens and present content is key to building a
functional and user-friendly app. While the data in the examples provided may be static,
the principles and techniques discussed can be applied to apps with dynamic content as
well. With this understanding, you are well-equipped to build a simple content-based app
and expand upon it with dynamic data and additional features.

For reference, you can download the complete project here:

Demo project for the first project


(https://www.appcoda.com/resources/swiftui5/SwiftUINavigationList.zip)
Demo project for the second project
(https://www.appcoda.com/resources/swiftui5/SwiftUINavigation.zip)

To further study navigation view, you can also refer to the documentation provided by
Apple:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 285


https://developer.apple.com/tutorials/swiftui/building-lists-and-navigation
https://developer.apple.com/documentation/swiftui/navigationstack

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 286


Chapter 12
Playing with Modal Views, Floating
Buttons and Alerts
Earlier, we created a navigation interface that allows users to navigate from the content
view to the detail view. The view transition is smoothly animated and completely handled
by iOS. When a user triggers the transition, the detail view slides from right to left
seamlessly. Navigation UI is just one of the commonly used UI patterns. In this chapter, I
will introduce another design technique for presenting content modally.

For iPhone users, modal views are a familiar concept. They are commonly used to
present forms for user input. For example, the Calendar app presents a modal view for
users to create a new event. Similarly, the built-in Reminders and Contacts apps also use
modal views to prompt for user input.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 287


Figure 1. Sample modal views in Calendar, Reminders, and Contact apps

From a user experience perspective, a modal view is typically triggered by tapping a


button. Once again, the transition animation of the modal view is handled by iOS. When
presenting a full-screen modal view, it smoothly slides up from the bottom of the screen.

If you're a long-time iOS user, you may notice that the visual appearance of the modal
views shown in figure 1 differs from the traditional ones. In previous versions of iOS,
modal views covered the entire screen. However, starting with iOS 13, modal views are
displayed in a card-like format by default. The modal view no longer covers the entire
screen but instead partially covers the underlying content view. You can still see the top
edge of the content/parent view. Additionally, with this update, modal views can be
dismissed by swiping down from anywhere on the screen. Enabling this gesture requires
no additional code as it is built-in and provided by iOS. However, if you prefer to dismiss
a modal view using a button, you can still implement that functionality.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 288


So, what will we be focusing on in this chapter?

I will demonstrate how to present the same detail view we implemented in the previous
chapter using a modal view. While modal views are commonly used for presenting forms,
they can also be used to present other types of information. In addition to modal views,
we will explore how to create a floating button within the detail view. While modal views
can be dismissed using the swipe gesture, I want to provide a Close button for users to
easily dismiss the detail view. Furthermore, we will also discuss Alerts, which are another
type of modal view.

Figure 2. Presenting the detail screen using modal views

We got a lot to discuss in this chapter. Let's get started.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 289


Understanding Sheet in SwiftUI
The sheet presentation style appears as a card that partially covers the underlying
content and dims all uncovered areas to prevent interaction with them. The top
edge of the parent view or a previous card is visible behind the current card to help
people remember the task they suspended when they opened the card.

- Apple's official documentation (https://developer.apple.com/design/human-


interface-guidelines/ios/app-architecture/modality/)

Before we delve into the implementation, let me provide you with a brief introduction to
the card-like presentation of modal views. In SwiftUI, the card presentation style is used
to achieve this effect, and it is the default presentation style for modal views.

To present a modal view using the card presentation style, you apply the sheet modifier,
as shown below:

.sheet(isPresented: $showModal) {
DetailView()
}

It takes in a boolean value to indicate whether the modal view is presented. If


isPresented is set to true , the modal view will be automatically presented in the form of
card.

Another way to present the modal view is like this:

.sheet(item: $itemToDisplay) {
DetailView()
}

The sheet modifier also allows you to trigger the display of modal views by passing an
optional binding. If the optional has a value, iOS will bring up the modal view. If you
remember our discussion on actionSheet in an earlier chapter, you will find that the
usage of sheet is very similar to actionSheet .

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 290


Preparing the Starter Project
That's enough background information. Let's move on to the actual implementation of
our demo project. To begin, please download the starter project from
https://www.appcoda.com/resources/swiftui5/SwiftUIModalStarter.zip. Once
downloaded, open the project and check out the preview. You should be very familiar
with this demo app. The app still has a navigation bar, but the navigation link has been
removed.

Figure 3. Starter project

Implementing the Modal View Using isPresented


As discussed earlier, the sheet modifier provides us with two ways to present a modal
view. I'll show you how both approaches work. Let's start with the isPresented approach.
For this approach, we need a state variable of type Bool to keep track of the status of the
modal view. Declare this variable in ContentView :

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 291


@State var showDetailView = false

By default, it's set to false . The value of this variable will be set to true when one of the
rows is clicked. Later, we will make this change in the code.

When presenting the detail view, the view requires us to pass the selected article. So, we
also need to declare a state variable to store the user's selection. In ContentView , declare
another state variable for this purpose:

@State var selectedArticle: Article?

To implement the modal view, we attach the sheet modifier to the List like this:

NavigationStack {
List(articles) { article in
ArticleRow(article: article)

.listRowSeparator(.hidden)
}
.listStyle(.plain)
.sheet(isPresented: $showDetailView) {

if let selectedArticle = self.selectedArticle {


ArticleDetailView(article: selectedArticle)
}
}

.navigationTitle("Your Reading")
}

The presentation of the modal view depends on the value of the showDetailView property.
This is why we specify it in the isPresented parameter. The closure of the sheet

modifier describes the layout of the view to be presented. Here we present the
ArticleDetailView .

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 292


The remaining item is to detect the user's touch. When building the navigation UI, we
utilize NavigationLink to handle touch. However, this special button is designed for the
navigation interface. In SwiftUI, there is a handler called onTapGesture which can be used
to recognize a tap gesture. You can attach this handler to each of the ArticleRow

instances to detect the users' touch. Modify the NavigationStack in the body variable like
this:

NavigationStack {
List(articles) { article in
ArticleRow(article: article)
.onTapGesture {
self.showDetailView = true
self.selectedArticle = article
}

.listRowSeparator(.hidden)
}
.listStyle(.plain)
.sheet(isPresented: $showDetailView) {

if let selectedArticle = self.selectedArticle {


ArticleDetailView(article: selectedArticle)
}
}

.navigationTitle("Your Reading")
}

In the closure of onTapGesture , we set the showDetailView to true . This is used to trigger
the presentation of the modal view. We also store the selected article in the
selectedArticle variable.

Test the app in the preview canvas. You should be able to bring up the detail view
modally.

Note: For the very first time the app brings up the modal view, it shows a blank view.
Wipe down the dialog to dismiss it, select another article (not the same article) and you
should get the correct rendering. This is a known issue and we will discuss the fix in the

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 293


later section.

Figure 4. Presenting the detail view modally

Implementing the Modal View with Optional Binding


The sheet modifier also offers an alternative approach to present the modal view.
Instead of using a boolean value to control the visibility of the modal view, the modifier
allows you to use an optional binding for the same purpose.

You can replace the sheet modifier like this:

.sheet(item: $selectedArticle) { article in


ArticleDetailView(article: article)
}

In this case, when using the sheet modifier with an optional binding, it requires you to
pass the binding of the selectedArticle variable. This means that the modal view will
only be presented if the selectedArticle variable has a value. The code in the closure

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 294


describes the layout of the modal view, which is slightly different from the code we wrote
earlier.

With this approach, the sheet modifier will pass the selected article as a parameter to
the closure. The article parameter contains the selected article, which is guaranteed to
have a value. This allows us to directly initialize an ArticleDetailView using the selected
article.

Since we no longer use the showDetailView variable, you can remove this line of code:

@State var showDetailView = false

And remove the self.showDetailView = true from the .onTapGesture closure.

.onTapGesture {
self.showDetailView = true
...
}

After changing the code, you can test the app again. Everything should work like the first
version but the underlying code is cleaner than the original code.

Creating a Floating Button for Dismissing the Modal


View
The modal view already has built-in support for the swipe-down gesture, allowing users
to naturally swipe down to close it. However, it's important to consider that not all users
may be familiar with this gesture, especially newcomers to iPhone. Therefore, it would be
beneficial to provide an alternative way to dismiss the modal view, such as a Close
button.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 295


Figure 5. The close button for dismissing the modal view

Let's proceed with adding the Close button to the modal view as an alternative dismissal
method. Switch over to ArticleDetailView.swift .

Do you know how to position the button at the top-right corner? Try to come up with
your own implementation without peeking at my code.

Similar to the NavigationStack , we can dismiss the modal view by using the dismiss

environment value. To achieve this, declare the following variable in ArticleDetailView :

@Environment(\.dismiss) var dismiss

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 296


For the close button, we can attach the overlay modifier to the scroll view like this (place
the code snippet before ignoresSafeArea ):

.overlay(

HStack {
Spacer()

VStack {
Button {
dismiss()
} label: {
Image(systemName: "chevron.down.circle.fill")
.font(.largeTitle)
.foregroundStyle(.white)
}

.padding(.trailing, 20)
.padding(.top, 40)

Spacer()
}
}
)

The button will be overlaid on top of the scroll view, creating a floating effect. Even if you
scroll down the view, the button remains fixed in the same position. To position the
button at the top-right corner, we can utilize a combination of HStack , VStack , and
Spacer . To dismiss the view, simply call the dismiss() function.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 297


Figure 6. Implementing the close button

Switch over to ContentView and run it in the canvas. You should be able to dismiss the
modal view by clicking the close button.

Using Alerts
In addition to the card-like modal views, Alerts are another type of modal view that
completely blocks the screen when presented. Unlike other modal views, an alert requires
the user to choose one of the options provided before it can be dismissed. Figure 7 shows
a sample alert that we're going to implement in our demo project. We will display this
alert after the user taps the close button.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 298


Figure 7. Displaying an alert

In SwiftUI, you create an alert using the .alert modifier. Here is an example of .alert :

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 299


.alert("Warning", isPresented: $showAlert, actions: {
Button {
dismiss()
} label: {
Text("Confirm")
}

Button(role: .cancel, action: {}) {


Text("Cancel")
}
}, message: {
Text("Are you sure you want to leave?")
})

The sample code initializes an alert view with the title "Warning" and displays the
message "Are you sure you want to leave?" to the user. The alert view includes two
buttons: Confirm and Cancel.

Here is the code to create the alert as shown in figure 7:

.alert("Reminder", isPresented: $showAlert, actions: {


Button {
dismiss()
} label: {
Text("Yes")
}

Button(role: .cancel, action: {}) {


Text("No")
}

}, message: {
Text("Are you sure you are finished reading the article?")
})

It's similar to the previous code snippet, except that the labels of the buttons are
different. This alert asks the user whether they have finished reading the article. If the
user chooses Yes, the modal view will be closed. Otherwise, the modal view will stay

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 300


open.

Now that we have the code for creating the alert, the question is how can we trigger the
display of the alert? SwiftUI provides the alert modifier that you can attach to any view.
Again, you use a boolean variable to control the display of the alert. So, declare a state
variable in ArticleDetailView :

@State private var showAlert = false

Next, attach the alert modifier as displayed earlier to the ScrollView .

There is still one thing left. When should we trigger this alert? In other words, when
should we set showAlert to true ?

Obviously, the app should display the alert when someone taps the close button. So,
replace the close button's action like this:

Button {
self.showAlert = true
} label: {
Image(systemName: "chevron.down.circle.fill")
.font(.largeTitle)
.foregroundColor(.white)
}

Instead of dismissing the modal view directly, we instruct iOS to show the alert by setting
showAlert to true . You're now ready to test the app. When you tap the close button,
you'll see the alert. The modal view will be dismissed if you choose "Yes."

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 301


Figure 8. Tapping the close button will show you the alert

Displaying a Full Screen Modal View


Starting with iOS 13, the modal view doesn't cover the whole screen by default. If you
want to present a full-screen modal view, you can use the .fullScreenCover modifier
introduced in iOS 14. Instead of using .sheet to bring up a modal view, you can apply
the .fullScreenCover modifier like this:

.fullScreenCover(item: $selectedArticle) { article in


ArticleDetailView(article: article)
}

Summary
You've learned how to present a modal view, implement a floating button, and show an
alert. The latest release of iOS continues to encourage people to interact with the device
using gestures and provides built-in support for common gestures. Without writing a line
of code, you can let users swipe down the screen to dismiss a modal view.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 302


The API design of both the modal view and alert is very similar. It monitors a state
variable to determine whether the modal view (or alert) should be triggered. Once you
understand this technique, the implementation shouldn't be difficult for you.

For reference, you can download the complete modal project here:

Demo project (https://www.appcoda.com/resources/swiftui5/SwiftUIModal.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 303


Chapter 13
Building a Form with Picker, Toggle
and Stepper
Mobile apps use forms to interact with users and solicit required data from them. Every
day, when using your iPhone, it's very likely you will come across a mobile form. For
example, a calendar app may present you a form to fill in the information for a new event.
A shopping app asks you to provide the shipping and payment information by showing
you a form. As a user, I can't deny that I hate filling out forms. That said, as a developer,
these forms help us interact with users and ask for information to complete certain
operations. Developing a form is definitely an essential skill you need to grasp.

In the SwiftUI framework, there is a special UI control called Form. With this new
control, you can easily build a form. In this chapter, I will show you how to build a form
using this Form component. While building out a form, you will also learn how to work
with common controls like picker, toggle, and stepper.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 304


Figure 1. Building a Setting screen

Okay, let's discuss the project we'll be working on. Take a look at Figure 1. We're going to
build a Settings screen for the Restaurant app that we've been working on in earlier
chapters. This screen will provide users with options to configure their order and filter
preferences. This type of form is commonly seen in real-life projects. Once you
understand how it works, you will be able to create your own form in your app projects.

In this chapter, our focus will be on implementing the layout of the form. You'll learn
how to use the Form component to structure the Setting screen. We'll also implement a
picker for selecting a sort preference, as well as a toggle and a stepper for indicating filter
preferences. Once you understand how to lay out a form, in the next chapter, I'll show
you how to make the app fully functional by updating the list based on the user's
preferences. You'll learn how to store user preferences, share data between views, and
monitor data updates using @Environment .

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 305


Preparing the Starter Project
To save you time from building the restaurant list again, I have prepared a starter project
for you. You can download it from
https://www.appcoda.com/resources/swiftui5/SwiftUIFormStarter.zip. After
downloading, open the SwiftUIForm.xcodeproj file with Xcode. You can preview
ContentView.swift in the canvas, and you'll notice a familiar UI with additional detailed
information for a restaurant.

Figure 2. The restaurant list view

The Restaurant struct now has three more properties: type, phone, and priceLevel. I
think both type and phone are self explanatory. Price level stores an integer of range 1 to
5 reflecting the average cost of the restaurant. The restaurants array has been
prepopulated with some sample data. For later testing, some of the restaurants have
isFavorite and isCheckIn set to true . This is why you see some check-in and favorite
indicators displayed in the preview.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 306


Building the Form UI
As mentioned earlier, SwiftUI provides the Form component specifically designed for
building form user interfaces. It serves as a container for organizing and grouping
various controls for data entry, such as toggles and pickers. To better illustrate its usage,
let's dive into the implementation and explore its features firsthand.

To begin, let's create a separate file for the Settings screen. In the project navigator, right-
click the SwiftUIForm folder and select "New File...". Choose the SwiftUI View template
and name the file SettingView.swift.

Figure 3. Creating a new SwiftUI file

Now, let's start by creating the form. Replace SettingView with this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 307


struct SettingView: View {
var body: some View {
NavigationStack {
Form {
Section(header: Text("SORT PREFERENCE")) {
Text("Display Order")
}

Section(header: Text("FILTER PREFERENCE")) {


Text("Filters")
}
}

.navigationBarTitle("Settings")
}
}
}

To organize the form layout, you can utilize the Form container. Within the Form , you
can add sections and various form components such as text fields, pickers, toggles, and
more. In the code snippet provided, we create two sections: Sort Preference and Filter
Preference. Each section contains a text view. If you have correctly implemented the
code, the canvas should display a preview similar to the one shown in figure 4.

Figure 4. Create a simple form with two sections

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 308


Creating a Picker View
When presenting a form, you certainly want to secure some information. It's useless if we
just present a Text component. In our form, we will use three types of UI controls for
user input: a picker view, a toggle, and a stepper. Let's start with the sort preference and
implement a picker view.

For the sort preference, users will be able to choose the display order of the restaurant
list. We will offer three options for them to choose from:

1. Alphabetically
2. Show Favorite First
3. Show Check-in First

To handle this input, we can utilize a Picker control. We can start by declaring an array
named displayOrders in the SettingView file:

private var displayOrders = [ "Alphabetical", "Show Favorite First", "Show Check-i


n First"]

To use a picker, you also need to declare a state variable to store the user's selected
option. In SettingView , declare the variable like this:

@State private var selectedOrder = 0

Here, 0 means the first item of displayOrders . Now replace the SORT PREFERENCE
section like this:

Section(header: Text("SORT PREFERENCE")) {


Picker(selection: $selectedOrder, label: Text("Display order")) {
ForEach(0 ..< displayOrders.count, id: \.self) {
Text(self.displayOrders[$0])
}
}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 309


This is how you create a picker container in SwiftUI. You need to provide two values: the
binding of the selection ( $selectedOrder ) and the text label describing each option. Inside
the closure, you display the available options using Text views.

In the canvas, you should see that the Display Order is initially set to Alphabetical. This
is because selectedOrder is set to the default value of 0 . If you click the Play button to
test the view, tapping the Display Order row will bring you to the next screen, showing
you all the available options. You can select any of the options (e.g. Show Favorite First)
for testing. When you go back to the Setting screen, the Display Order will reflect your
selection. This is the power of the @State keyword. It automatically monitors changes
and helps you store the state of the selection.

Figure 5. Using Picker view for display order selection

Working with Toggle Switches

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 310


Next, let's move on to the input for setting the filter preference. First, we will implement
a toggle switch to enable or disable the "Show Check-in Only" filter. A toggle has only two
states: ON or OFF. This control is useful for prompting users to choose between two
mutually exclusive options.

Creating a toggle switch using SwiftUI is quite straightforward. Similar to the Picker

control, we need to declare a state variable to store the current setting of the toggle.
Declare the following variable in SettingView :

@State private var showCheckInOnly = false

Then, update the FILTER PREFERENCE section like this:

Section(header: Text("FILTER PREFERENCE")) {


Toggle(isOn: $showCheckInOnly) {
Text("Show Check-in Only")
}
}

You use the Toggle view to create a toggle switch and pass it the current state of the
toggle. In the closure, you present the description of the toggle. Here, we simply use a
Text view.

The canvas should now show a toggle switch under the Filter Preference section. If you
test the app, you should be able to switch it between the ON and OFF states. Similarly,
the state variable showCheckInOnly will always keep track of the user's selection.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 311


Figure 6. Showing a toggle switch

Using Steppers
The last UI control in the setting form is a Stepper. Referring to figure 1, users can filter
the restaurants by setting the pricing level. Each restaurant has a pricing indicator with a
range of 1 to 5. Users can adjust the price level to narrow down the number of restaurants
displayed in the list view.

In the setting form, we will implement a stepper for users to adjust this setting. Basically,
a Stepper in iOS shows a text field, and plus and minus buttons to perform increment
and decrement actions on the text field.

To implement a stepper in SwiftUI, we first need a state variable to hold the current value
of the stepper. In this case, this variable stores the user's price level filter. Declare the
state variable in SettingView like this:

@State private var maxPriceLevel = 5

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 312


By default, we set the maxPriceLevel to 5 . Update the FILTER PREFERENCE section
like this:

Section(header: Text("FILTER PREFERENCE")) {


Toggle(isOn: $showCheckInOnly) {
Text("Show Check-in Only")
}

Stepper(onIncrement: {
self.maxPriceLevel += 1

if self.maxPriceLevel > 5 {
self.maxPriceLevel = 5
}
}, onDecrement: {
self.maxPriceLevel -= 1

if self.maxPriceLevel < 1 {
self.maxPriceLevel = 1
}
}) {
Text("Show \(String(repeating: "$", count: maxPriceLevel)) or below")
}
}

You create a stepper by initiating a Stepper component. For the onIncrement parameter,
you specify the action to perform when the + button is clicked. In the code, we simply
increase maxPriceLevel by 1. Conversely, the code specified in the onDecrement parameter
will be executed when the - button is clicked.

Since the price level is in the range of 1 to 5, we perform a check to make sure the value of
maxPriceLevel is between the value of 1 and 5. In the closure, we display the text
description of the filter preference. The maximum price level is indicated by dollar signs.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 313


Figure 7. Implementing a stepper

Test the app in the preview canvas. The number of $ signs will be adjusted when you click
the + / - button.

Presenting the Form


Now that you've completed the form UI, the next step is to present the form to users. For
the demo, we will present this form as a modal view. In the content view, we will add a
Setting button in the navigation bar to trigger the setting view.

Switch over to ContentView.swift . Assuming you're familiar with the concept of modal
views, I will not explain the code in depth. First, we need a variable to keep track of the
state (i.e. shown or not shown) of the modal view. Insert the following line of code to
declare the state variable:

@State private var showSettings: Bool = false

Next, insert the following modifiers in the NavigationStack (place it after


navigationTitle ):

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 314


.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
self.showSettings = true
}, label: {
Image(systemName: "gear").font(.title2)
})
.tint(.black)
}
}
.sheet(isPresented: $showSettings) {
SettingView()
}

The navigationBarItems modifier allows you to add a button to the navigation bar. You
can position the button on the leading or trailing side of the navigation bar. In this case,
we use the trailing parameter to position the button on the top-right corner. The
sheet modifier is then used to present the SettingView as a modal view.

In the canvas, you should now see a gear icon in the navigation bar. When you click the
gear icon, it should bring up the Setting view.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 315


Figure 8. Adding the Setting button

Exercise
The only way to dismiss the Setting view is by using the swipe-down gesture. In the
modal view chapter, you learned how to dismiss a modal view programmatically. As a
refresher exercise, please create two buttons (Save & Cancel) in the navigation bar.
Although you are not required to implement the functionality of these buttons, when a
user taps either of the buttons, simply dismiss the setting view.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 316


Figure 9. Adding two buttons (Save & Cancel) in the navigation bar

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 317


What's Coming Next
I hope you understand how the Form component works and that you now know how to
build a form UI with components like Picker and Stepper. Currently, the app can't store
the user preferences permanently. Every time you launch the app, the settings are reset
to their original values. In the next chapter, I will show you how to save these settings in
local storage. Furthermore, we will update the list view to reflect the user's preferences.

For reference, you can download the complete form project from the following link:

Demo project (https://www.appcoda.com/resources/swiftui5/SwiftUIForm.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 318


Chapter 14
Data Sharing with Combine and
Environment Objects
In the previous chapter, you learned how to layout a form using the Form component.
However, the form is not yet functional. Regardless of the options selected, the list view
doesn't change to reflect the user's preferences. In this chapter, we will continue
developing the settings screen and make the app fully functional by updating the
restaurant list based on the user's preferences.

In the upcoming sections, we will cover the following topics:

1. How to use enum to better organize our code


2. How to store the user's preference permanently using UserDefaults
3. How to share data using Combine and @Environment

If you haven't completed the exercise from the previous chapter, I encourage you to
spend some time on it. However, if you can't wait to read this chapter, you can download
the project from the following link:
https://www.appcoda.com/resources/swiftui5/SwiftUIForm.zip.

Refactoring the Code with Enum


Currently, we store the three options of the display order in an array. While this approach
works, there is a better way to improve the code.

An enumeration defines a common type for a group of related values and enables
you to work with those values in a type-safe way within your code.

- Apple's official documentation (https://docs.swift.org/swift-


book/LanguageGuide/Enumerations.html)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 319


Since this group of fixed values is related to the display order, we can use an enum to
hold them, with each case assigned an integer value, like this:

enum DisplayOrderType: Int, CaseIterable {


case alphabetical = 0
case favoriteFirst = 1
case checkInFirst = 2

init(type: Int) {
switch type {
case 0: self = .alphabetical
case 1: self = .favoriteFirst
case 2: self = .checkInFirst
default: self = .alphabetical
}
}

var text: String {


switch self {
case .alphabetical: return "Alphabetical"
case .favoriteFirst: return "Show Favorite First"
case .checkInFirst: return "Show Check-in First"
}
}
}

What makes enum great is that we can work with these values in a type-safe way within
our code. Additionally, enum in Swift is a first-class type in its own right. This means that
we can create instance methods to provide additional functionality related to the values.
Later on, we will add a function for handling the filtering. For now, let's create a new
Swift file named SettingStore.swift to store the enum . You can right-click SwiftUIForm

in the project navigator and choose New File... to create the file.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 320


Figure 1. Creating a new Swift file

After creating SettingStore.swift , insert the code snippet above in the file. Next, go back
to SettingView.swift . We will update the code to use the DisplayOrder enumeration
instead of the displayOrders array.

First, delete this line of code from SettingView :

private var displayOrders = [ "Alphabetical", "Show Favorite First", "Show Check-i


n First"]

Next, update the default value of selectedOrder to DisplayOrderType.alphabetical like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 321


@State private var selectedOrder = DisplayOrderType.alphabetical

Here, we set the default display order to alphabetical. Comparing this to the previous
value, the code becomes more readable after switching to use an enumeration. Next, you
also need to change the code in the Sort Preference section. Specifically, update the code
in the ForEach loop as follows:

Section(header: Text("SORT PREFERENCE")) {


Picker(selection: $selectedOrder, label: Text("Display order")) {
ForEach(DisplayOrderType.allCases, id: \.self) {
orderType in
Text(orderType.text)
}
}
}

Since we have adopted the CaseIterable protocol in the DisplayOrder enum, we can
obtain all the display orders by accessing the allCases property, which contains an array
of all the enum's cases.

Now you can test the Settings screen again. It should work and look the same. However,
the underlying code is more manageable and readable.

Saving the User Preferences in UserDefaults


Right now, the app can't save the user's preferences permanently. Whenever you restart
the app, the Settings screen resets to its default settings.

There are multiple ways to store the settings. For saving small amounts of data like user
settings on iOS, the built-in "defaults" database is a good option. This "defaults" system
allows an app to store user preferences in key-value pairs. To interact with this defaults
database, you use a programmatic interface called UserDefaults .

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 322


In the SettingStore.swift file, we will create a SettingStore class to provide some
convenience methods for saving and loading the user's preferences. Insert the following
code snippet in SettingStore.swift :

final class SettingStore {

init() {
UserDefaults.standard.register(defaults: [
"view.preferences.showCheckInOnly" : false,
"view.preferences.displayOrder" : 0,
"view.preferences.maxPriceLevel" : 5
])
}

var showCheckInOnly: Bool = UserDefaults.standard.bool(forKey: "view.preferenc


es.showCheckInOnly") {
didSet {
UserDefaults.standard.set(showCheckInOnly, forKey: "view.preferences.s
howCheckInOnly")
}
}

var displayOrder: DisplayOrderType = DisplayOrderType(type: UserDefaults.stand


ard.integer(forKey: "view.preferences.displayOrder")) {
didSet {
UserDefaults.standard.set(displayOrder.rawValue, forKey: "view.prefere
nces.displayOrder")
}
}

var maxPriceLevel: Int = UserDefaults.standard.integer(forKey: "view.preferenc


es.maxPriceLevel") {
didSet {
UserDefaults.standard.set(maxPriceLevel, forKey: "view.preferences.max
PriceLevel")
}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 323


Let me briefly explain the code. In the init method, we initialize the defaults system
with some default values. These values will only be used if the user's preferences are not
found in the database.

In the code above, we declare three properties ( showCheckInOnly , displayOrder , and


maxPriceLevel ) which are saved in key-value pairs with UserDefaults . The default value
is loaded from the default system for the specific key. In the didSet , we use the set

method of UserDefaults ( UserDefaults.standard.set() ) to save the value in the user


default.

With the SettingStore ready, let's switch over to the SettingView.swift file to implement
the Save operation. First, declare a property in SettingView for the SettingStore :

var settingStore: SettingStore

For the Save button, find the Save button code (in the ToolbarItem(placement:

.navigationBarTrailing) block) and replace the existing code with this:

Button {
self.settingStore.showCheckInOnly = self.showCheckInOnly
self.settingStore.displayOrder = self.selectedOrder
self.settingStore.maxPriceLevel = self.maxPriceLevel
dismiss()

} label: {
Text("Save")
.foregroundStyle(.primary)
}

We added three lines of code to the exiting save button to save the user's preference. To
load the user's preferences when the Settings view is brought up, you can add the
onAppear modifier to the NavigationStack like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 324


.onAppear {
self.selectedOrder = self.settingStore.displayOrder
self.showCheckInOnly = self.settingStore.showCheckInOnly
self.maxPriceLevel = self.settingStore.maxPriceLevel
}

The onAppear modifier will be called when the view appears. We load the user's settings
from the defaults system in its closure.

Before you can test the changes, you have to update the #Preview section like this:

#Preview {
SettingView(settingStore: SettingStore())
}

Now, switch over to ContentView.swift and declare the settingStore property:

var settingStore: SettingStore

And then update the sheet modifier like this:

.sheet(isPresented: $showSettings) {
SettingView(settingStore: self.settingStore)
}

Lastly, update the #Preview section like this:

#Preview {
ContentView(settingStore: SettingStore())
}

We initialize a SettingStore and pass it to SettingView . This is required because we've


added the settingStore property in SettingView .

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 325


If you compile and run the app now, Xcode will show you an error. There is one more
change we need to make before the app can run properly.

Figure 2. An error in SwiftUIFormApp.swift

Go to SwiftUIFormApp.swift and add this property to create a SettingStore instance:

var settingStore = SettingStore()

Next, change the line code in the WindowGroup block to the following to fix the error:

ContentView(settingStore: settingStore)

You should now be able to execute app and play around with the settings. Once you save
the settings, they are stored permanently in the local defaults system. You can stop the
app and launch it again. The saved settings should be loaded in the Setting screen.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 326


Figure 3. The Setting screen should load your user preference

Sharing Data Between Views Using @Environment


Now that the user's preferences have been saved in the local defaults system, the list view
does not change in accordance with the user's settings. There are various ways to solve
this problem.

Let's recap what we have so far. When a user taps the "Save" button on the "Settings"
screen, we save the selected options in the local defaults system. The "Settings" screen is
then dismissed, and the app brings the user back to the list view. We have two options to
solve the problem. We can either instruct the list view to reload the settings, or the list
view must be capable of monitoring the changes of the defaults system and trigger an
update of the list.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 327


Along with the introduction of SwiftUI, Apple also released a new framework called
Combine. According to Apple, this framework provides a declarative API for processing
values over time. In the context of this demo, Combine lets you easily monitor a single
object and get notified of changes. Working along with SwiftUI, we can trigger an update
of a view without writing a line of code. Everything is handled behind the scenes by
SwiftUI and Combine.

So, how can the list view know the user's preference is modified and trigger the update
itself?

Let me introduce three keywords:

1. @EnvironmentObject - Technically, this is known as a property wrapper, but you


may consider this keyword as a special marker. When you declare a property as an
environment object, SwiftUI monitors the value of the property and invalidates the
corresponding view whenever there are changes. @EnvironmentObject works pretty
much the same as @State , but when a property is declared as an environment
object, it becomes accessible to all views in the entire app. For example, if your app
has many views that share the same piece of data (e.g., user settings), environment
objects work great for this purpose. You do not need to pass the property between
views; instead, you can access it automatically.
2. ObservableObject - This is a protocol of the Combine framework. When you
declare a property as an environment object, the type of that property must
implement this protocol. To answer our question: how can we let the list view know
that the user's preferences have changed? By implementing this protocol, the object
can serve as a publisher that emits the changed value(s). The subscribers that
monitor the value change will be notified.
3. @Published - A property wrapper that works along with ObservableObject . When a
property is prefixed with @Publisher , this indicates that the publisher should inform
all subscribers whenever the property's value is changed.

I know it's a bit confusing. You will have a better understanding once we go through the
code.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 328


Let's start with SettingStore.swift . Since both the settings view and the list view need to
monitor the change of user preferences, SettingStore should implement the
ObservableObject protocol and announce the change of the defaults property. In the
beginning of the SettingStore.swift file, we have to first import the Combine framework:

import Combine

The SettingStore class should adopt the ObservableObject protocol. Update the class
declaration like this:

final class SettingStore: ObservableObject {

Next, insert the @Published annotation for all the properties like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 329


@Published var showCheckInOnly: Bool = UserDefaults.standard.bool(forKey: "view.pr
eferences.showCheckInOnly") {
didSet {
UserDefaults.standard.set(showCheckInOnly, forKey: "view.preferences.showC
heckInOnly")
}
}

@Published var displayOrder: DisplayOrderType = DisplayOrderType(type: UserDefaults


.standard.integer(forKey: "view.preferences.displayOrder")) {
didSet {
UserDefaults.standard.set(displayOrder.rawValue, forKey: "view.preferences
.displayOrder")
}
}

@Published var maxPriceLevel: Int = UserDefaults.standard.integer(forKey: "view.pr


eferences.maxPriceLevel") {
didSet {
UserDefaults.standard.set(maxPriceLevel, forKey: "view.preferences.maxPric
eLevel")
}
}

By using the @Published property wrapper, the publisher informs subscribers whenever
there is a value change of the property (e.g., an update of displayOrder ).

As you can see, it's pretty easy to inform a changed value with Combine. We haven't
actually written any new code but simply adopted a required protocol and inserted a
marker.

Now let's switch over to SettingView.swift . The settingStore should now be declared as
an environment object so that we can share the data with other views. Update the
settingStore variable as follows:

@EnvironmentObject var settingStore: SettingStore

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 330


You do not need to update any code related to the Save button. However, when you set a
new value for the setting store (e.g. update showCheckInOnly from true to false), this
update will be published and let all subscribers know.

Because of the change, we need to update #Preview to the following:

#Preview {
SettingView().environmentObject(SettingStore())
}

Here, we inject an instance of SettingStore into the environment for the preview.

Okay, all our work has been on the Publisher side. What about the Subscriber? How can
we monitor the change of defaults and update the UI accordingly?

In the demo project, the list view is the Subscriber side. It needs to monitor the changes
of the setting store and re-render the list view to reflect the user's setting. Now let's open
ContentView.swift to make some changes. Similar to what we've just done, the
settingStore should now declared as an environment object:

@EnvironmentObject var settingStore: SettingStore

Due to the change, the code in the sheet modifier should be modified to grab this
environment object:

.sheet(isPresented: $showSettings) {
SettingView().environmentObject(self.settingStore)
}

Also, for testing purposes, the preview code should be updated accordingly to inject the
environment object:

#Preview {
ContentView().environmentObject(SettingStore())
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 331


Lastly, open SwiftUIFormApp.swift and update the line of code inside WindowGroup like
this:

struct SwiftUIFormApp: App {

var settingStore = SettingStore()

var body: some Scene {


WindowGroup {
ContentView().environmentObject(settingStore)
}
}
}

Here, we inject the setting store into the environment by calling the environmentObject

method. Now the instance of setting store is available to all views within the app. In other
words, both the Setting and List views can access it automatically.

Implementing the Filtering Options


We have now implemented a common setting store that can be accessed by all views.
What's great is that any changes in the setting store automatically notify all the views that
monitor for updates. Although you may not experience any visual differences, the setting
store notifies the changes to the list view when you update the options in the setting
screen.

Our final task is to implement the filtering and sorting options to display only the
restaurants that match the user preferences. Let's begin with the implementation of these
two filtering options:

Show check-in only


Show restaurants below a certain price level

In ContentView.swift , let's create a new function called showShowItem to handle the


filtering:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 332


private func shouldShowItem(restaurant: Restaurant) -> Bool {
return (!self.settingStore.showCheckInOnly || restaurant.isCheckIn) && (restau
rant.priceLevel <= self.settingStore.maxPriceLevel)
}

This function takes in a restaurant object and tells the caller whether the restaurant
should be displayed. In the code above, we check if the "Show Check-in Only" option is
selected and verify the price level of the given restaurant.

Next, wrap the BasicImageRow with a if clause like this:

if self.shouldShowItem(restaurant: restaurant) {
BasicImageRow(restaurant: restaurant)
.contextMenu {

...

}
}

Here, we first call the shouldShowItem function we just implemented to check if the
restaurant should be displayed.

Now run the app to have a quick test. In the settings screen, turn on the Show Check-in
Only option and configure the price level option to show restaurants with a price level of
3 (i.e., $$$) or below. Once you tap the Save button, the list view should be automatically
refreshed (with animation) and show you the filtered records.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 333


Figure 4. The list view now refreshes its items when you change the filter preference

Implementing the Sort Option


Now that we have completed the implementation of the filtering options, let's work on
the sorting option. In Swift, you can sort a sequence of elements using the sort(by:)

method. When using this method, you need to provide a predicate that returns true

when the first element should be ordered before the second.

For instance, to sort the restaurants array in alphabetical order, you can use the
sort(by:) method as follows:

restaurants.sorted(by: { $0.name < $1.name })

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 334


Here, $0 is the first element and $1 is the second element. In this case, a restaurant with
the name "Upstate" is larger than a restaurant with the name "Homei". Therefore,
"Homei" will be placed in front of "Upstate" in the sequence.

Conversely, if you want to sort the restaurants in alphabetical descending order, you can
write the code like this:

restaurants.sorted(by: { $0.name > $1.name })

How can we sort the array to show "check-in" first or show "favorite" first? We can use
the same method but provide a different predicate like this:

restaurants.sorted(by: { $0.isFavorite && !$1.isFavorite })


restaurants.sorted(by: { $0.isCheckIn && !$1.isCheckIn })

To better organize our code, we can put these predicates in the DisplayOrderType enum.
In SettingStore.swift , add a new function in DisplayOrderType like this:

func predicate() -> ((Restaurant, Restaurant) -> Bool) {


switch self {
case .alphabetical: return { $0.name < $1.name }
case .favoriteFirst: return { $0.isFavorite && !$1.isFavorite }
case .checkInFirst: return { $0.isCheckIn && !$1.isCheckIn }
}
}

This function simply returns the predicate, which is a closure, for the corresponding
display order. Now we are ready to make the final change. Go back to ContentView.swift

and change the ForEach statement from:

ForEach(restaurants) {
...
}

To:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 335


ForEach(restaurants.sorted(by: self.settingStore.displayOrder.predicate())) {
...
}

That's it! Test the app and change the sorting preference. When you update the sort
option, the list view will get notified and re-order the restaurants accordingly.

What's Coming Next


Are you aware that SwiftUI and Combine work together to help us write better code? In
the last two sections of this chapter, we didn't have to write a lot of code to implement the
filtering and sorting options. Combine handles the heavy lifting of event processing.
When combined with SwiftUI, it becomes even more powerful and saves you the trouble
of developing your own implementation to monitor the state changes of objects and
trigger UI updates. Everything is nearly automatic and taken care of by these two new
frameworks.

In the next chapter, we will continue to explore Combine by building a registration


screen. You will gain further understanding of how Combine can help you write cleaner
and more modular code.

For reference, you can download the complete project here:

Demo project
(https://www.appcoda.com/resources/swiftui5/SwiftUIFormData.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 336


Chapter 15
Building a Registration Form with
Combine and View Model
Now that you have a basic idea about Combine, let's explore how Combine can make
SwiftUI really shine. When developing a real-world app, it's very common to have a user
registration page for people to sign up and create an account. In this chapter, we will
build a simple registration screen with three text fields. Our focus is on form validation,
so we will not perform an actual sign up. You'll learn how we can leverage the power of
Combine to validate each of the input fields and organize our code in a view model.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 337


Figure 1. User registration demo

Before we dive into the code, take a look at figure 1. That is the user registration screen
we're going to build. Under each of the input fields, it lists out the requirements. As soon
as the user fills in the information, the app validates the input in real-time and crosses
out the requirement if it's been fulfilled. The sign up button is disabled until all the
requirements are matched.

If you have experience in Swift and UIKit, you know there are various types of
implementation to handle the form validation. In this chapter, however, we're going to
explore how you can utilize the Combine framework to perform form validation.

Layout the Form using SwiftUI

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 338


Let's begin this chapter with an exercise: use what you've learned so far and layout the
form UI shown in figure 1. To create a text field in SwiftUI, you can use the TextField

component. For the password fields, SwiftUI provides a secure text field called
SecureField .

To create a text field, you initiate a TextField with a field name and a binding. This
renders an editable text field with the user's input stored in your given binding. Similar to
other form fields, you can modify its look & feel by applying the associated modifiers.
Here is a sample code snippet:

TextField("Username", text: $username)


.font(.system(size: 20, weight: .semibold, design: .rounded))
.padding(.horizontal)

The usage of these two components are very similar except that the secure field
automatically masks the user's input:

SecureField("Password", text: $password)


.font(.system(size: 20, weight: .semibold, design: .rounded))
.padding(.horizontal)

I know these two components are new to you, but try your best to build the form before
looking at the solution.

Are you able to create the form? Even if you can't finish the exercise, that's completely
fine. Download this project from
https://www.appcoda.com/resources/swiftui5/SwiftUIFormRegistrationUI.zip. I will go
through my solution with you.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 339


Figure 2. The starter project

Open the ContentView.swift file and preview the layout in the canvas. Your rendered view
should look like that shown in figure 2. Now, let's briefly go over the code. Let's start with
the RequirementText view.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 340


struct RequirementText: View {

var iconName = "xmark.square"


var iconColor = Color(red: 251/255, green: 128/255, blue: 128/255)

var text = ""


var isStrikeThrough = false

var body: some View {


HStack {
Image(systemName: iconName)
.foregroundColor(iconColor)
Text(text)
.font(.system(.body, design: .rounded))
.foregroundColor(.secondary)
.strikethrough(isStrikeThrough)
Spacer()
}
}
}

First, why do I create a separate view for the requirements text (see figure 3)? If you look
at all of the requirements text, each requirement has an icon and a description. Instead of
creating each of the requirements text from scratch, we can generalize the code and build
a generic view for it.

Figure 3. A sample text field and its requirement text

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 341


The RequirementText view has four properties including iconName , iconColor , text , and
isStrikeThrough . It's flexible enough to support different styles of requirements text. If
you accept the default icon and color, you can simply create a requirement text like this:

RequirementText(text: "A minimum of 4 characters")

This will render the square with an x in it (xmark.square) and the text as shown in figure
3. In some cases, the requirement text should be crossed out and display a different
icon/color. The code can be written like this:

RequirementText(iconName: "lock.open", iconColor: Color.secondary, text: "A minimu


m of 8 characters", isStrikeThrough: true)

You specify a different system icon name, color, and set the isStrikeThrough option to
true . This will allow you to create a requirement text like that displayed in figure 4.

Figure 4. The requirement text is crossed out

Now that you understand how the RequirementText view works and why I created that,
let's take a look at the FormField view. Again, if you look at all the text fields, they all
have a common style - a text field with rounded font style. This is the reason why I
extracted the common code and created a FormField view.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 342


struct FormField: View {
var fieldName = ""
@Binding var fieldValue: String

var isSecure = false

var body: some View {

VStack {
if isSecure {
SecureField(fieldName, text: $fieldValue)
.font(.system(size: 20, weight: .semibold, design: .rounded))
.padding(.horizontal)

} else {
TextField(fieldName, text: $fieldValue)
.font(.system(size: 20, weight: .semibold, design: .rounded))
.padding(.horizontal)
}

Divider()
.frame(height: 1)
.background(Color(red: 240/255, green: 240/255, blue: 240/255))
.padding(.horizontal)

}
}
}

Since this generic FormField needs to take care of both text fields and secure fields, it has
a property named isSecure . If it's set to true , the form field will be created as a secure
field. In SwiftUI, you can make use of the Divider component to create a line. In the
code, we use the frame modifier to change its height to 1 point.

To create the username field, you write the code like this:

FormField(fieldName: "Username", fieldValue: $username)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 343


For the password field, the code is very similar except that the isSecure parameter is set
to true:

FormField(fieldName: "Password", fieldValue: $password, isSecure: true)

Okay, let's head back to the ContentView struct and see how the form is laid out.

struct ContentView: View {

@State private var username = ""


@State private var password = ""
@State private var passwordConfirm = ""

var body: some View {


VStack {
Text("Create an account")
.font(.system(.largeTitle, design: .rounded))
.bold()
.padding(.bottom, 30)

FormField(fieldName: "Username", fieldValue: $username)


RequirementText(text: "A minimum of 4 characters")
.padding()

FormField(fieldName: "Password", fieldValue: $password, isSecure: true


)
VStack {
RequirementText(iconName: "lock.open", iconColor: Color.secondary,
text: "A minimum of 8 characters", isStrikeThrough: true)
RequirementText(iconName: "lock.open", text: "One uppercase letter"
, isStrikeThrough: false)
}
.padding()

FormField(fieldName: "Confirm Password", fieldValue: $passwordConfirm,


isSecure: true)
RequirementText(text: "Your confirm password should be the same as the
password", isStrikeThrough: false)
.padding()
.padding(.bottom, 50)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 344


Button(action: {
// Proceed to the next screen
}) {
Text("Sign Up")
.font(.system(.body, design: .rounded))
.foregroundColor(.white)
.bold()
.padding()
.frame(minWidth: 0, maxWidth: .infinity)
.background(LinearGradient(gradient: Gradient(colors: [Color(r
ed: 251/255, green: 128/255, blue: 128/255), Color(red: 253/255, green: 193/255, b
lue: 104/255)]), startPoint: .leading, endPoint: .trailing))
.cornerRadius(10)
.padding(.horizontal)

HStack {
Text("Already have an account?")
.font(.system(.body, design: .rounded))
.bold()

Button(action: {
// Proceed to Sign in screen
}) {
Text("Sign in")
.font(.system(.body, design: .rounded))
.bold()
.foregroundColor(Color(red: 251/255, green: 128/255, blue:
128/255))
}
}.padding(.top, 50)

Spacer()
}
.padding()
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 345


First, we have a VStack that holds all the form elements. It begins with the heading,
followed by the form fields and requirement text. I have already explained how the form
fields and requirement text are created, so I will not go through them again. What I
added to the fields is the padding modifier, which is used to add some space between the
text fields.

The Sign up button is created using the Button component and has an empty action. I
intend to leave the action closure blank because our focus is on form validation. Again, I
believe you should know how a button can be customized, so I will not go into detail here.
You can always refer to the Button chapter for more information.

Lastly, we have the description text Already have an account. This text and the Sign in
button are completely optional. I'm mimicking the layout of a common sign-up form.

That's how I laid out the user registration screen. If you've tried out the exercise, you
might have come up with a different solution, and that's completely fine. Here, I just
wanted to show you one approach to building the form. You can use it as a reference and
come up with an even better implementation.

Understanding Combine
Before we dive into the code for form validation, it's better for me to give you some more
background information on the Combine framework. As mentioned in the previous
chapter, this new framework provides a declarative API for processing values over time.

What does it mean by "processing values over time," and what are these values?

Let's use the registration form as an example. The app generates UI events as it interacts
with users. Each keystroke a user enters in the text field triggers an event, creating a
stream of values, as illustrated in figure 5.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 346


Figure 5. A stream of data input

These UI events are one type of "values" the framework refers to. Another example of
these values is network events (e.g. downloading a file from a remote server).

The Combine framework provides a declarative approach for how your app
processes events. Rather than potentially implementing multiple delegate callbacks
or completion handler closures, you can create a single processing chain for a given
event source. Each part of the chain is a Combine operator that performs a distinct
action on the elements received from the previous step.

- Apple's official documentation


(https://developer.apple.com/documentation/combine/receiving_and_handling_
events_with_combine)

Publisher and Subscriber are the two core elements of the Combine framework. With
Combine, a Publisher sends events, and a Subscriber subscribes to receive values from
that Publisher. Let's continue using the text field as an example. When using Combine,
each keystroke the user inputs in the text field triggers a value change event. The
subscriber, which is interested in monitoring these values, can subscribe to receive these
events and perform further operations, such as validation.

For instance, let's say you are writing a form validator that has a property indicating
whether the form is ready to submit. In this case, you can mark that property with the
@Published annotation, like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 347


class FormValidator: ObservableObject {
@Published var isReadySubmit: Bool = false
}

Every time you change the value of isReadySubmit , it publishes an event to the subscriber.
The subscriber receives the updated value and continues processing. For example, the
subscriber may use that value to determine whether the submit button should be enabled
or not.

You may find that @Published works similarly to @State in SwiftUI. While they have
similar functionality in this example, @State is specifically designed for properties that
belong to a particular SwiftUI view. However, if you want to create a custom type that
doesn't belong to a specific view or that can be used across multiple views, you need to
create a class that conforms to ObservableObject and mark the relevant properties with
the @Published annotation.

Combine and MVVM


Now that you have a basic understanding of Combine, let's proceed with implementing
form validation using the framework. Here's what we'll do:

1. Create a view model to represent the user registration form


2. Implement form validation in the view model

You may have a few questions at this point. First, why do we need to create a view model?
Can't we add the form properties and perform the validation directly in ContentView?

Certainly, you can do that. However, as your project grows or the view becomes more
complex, it's a good practice to separate concerns and break down a complex component
into multiple layers.

"Separation of concerns" is a fundamental principle in software development. Instead of


putting everything in a single view, we can separate the view into two components: the
view itself and its view model. The view is responsible for UI layout, while the view model

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 348


holds the state and data to be displayed in the view. The view model also handles data
validation and conversion. This approach aligns with the MVVM (Model-View-
ViewModel) design pattern, which is well-known among experienced developers.

So, what data will this view model hold?

Let's revisit the registration form. We have three text fields:

Username
Password
Password confirm

On top of that, this view model will hold the states of the requirements text, indicating
whether they should be crossed out or not. These requirements include:

A minimum of 4 characters (username)


A minimum of 8 characters (password)
One uppercase letter (password)
Your confirm password should the same as the password (password confirm)

Therefore, the view model will have seven properties, and each of these properties will
publish its value change to those interested in receiving the value. The basic structure of
the view model can be defined as follows:

class UserRegistrationViewModel: ObservableObject {


// Input
@Published var username = ""
@Published var password = ""
@Published var passwordConfirm = ""

// Output
@Published var isUsernameLengthValid = false
@Published var isPasswordLengthValid = false
@Published var isPasswordCapitalLetter = false
@Published var isPasswordConfirmValid = false
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 349


That's the data model for the form view. The username , password , and passwordConfirm

properties hold the value of the username, password, and password confirm text fields
respectively. This class should conform to ObservableObject . All these properties are
annotated with @Published because we want to notify the subscribers whenever there is a
value change and perform the validation accordingly.

Validating the Username with Combine


Okay, that's the data model. But we still haven't dealt with the form validation. How do
we validate the username, password, and password confirm fields in accordance to the
requirements?

With Combine, you have to develop a publisher/subscriber mindset to answer this


question. Let's consider the username field. We actually have two publishers here:
username and isUsernameLengthValid. The username publisher emits value changes
whenever the user enters a keystroke in the username field. The isUsernameLengthValid

publisher informs the subscriber about the validation status of the user input. Nearly all
controls in SwiftUI are subscribers, so the requirements text view will listen to changes in
the validation result and update its style accordingly (e.g. with a strikethrough or
without). Figure 6 illustrates how we use Combine to validate the username input.

Figure 6. The username and isUsernameValid publishers

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 350


What's missing here is something that connects these two publishers. This "something"
should handle the following tasks:

Listen to the username change


Validate the username and return the validation result (true/false)
Assign the result to isUsernameLengthValid

If you transform the requirements above into code, the code snippet would look like this:

$username
.receive(on: RunLoop.main)
.map { username in
return username.count >= 4
}
.assign(to: \.isUsernameLengthValid, on: self)

The Combine framework provides two built-in subscribers: sink and assign. The sink
subscriber creates a general-purpose subscriber to receive values, while the assign
subscriber allows you to create a subscriber that updates a specific property of an object.
In this case, we use the assign subscriber to directly assign the validation result
(true/false) to the isUsernameLengthValid property.

Let's dive deeper into the code above, line by line. $username is the source of value
changes that we want to listen to. Since we're subscribing to the changes of UI events, we
call the receive(on:) function to ensure that the subscriber receives values on the main
thread (i.e., RunLoop.main ).

The value sent by the publisher is the username input by the user. But what the
subscriber is interested in is whether the length of the username meets the minimum
requirement. Here, the map function is an operator in Combine that takes an input,
processes it, and transforms the input into something that the subscriber expects. So,
what we did in the code above is:

The value sent by the publisher is the username input by the user. However, what the
subscriber is interested in is whether the length of the username meets the minimum
requirement. Here, the map function is an operator in Combine that takes an input,

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 351


processes it, and transforms it into something that the subscriber expects. So, in the code
above, we:

1. Take the username as input.


2. Validate the username and verify if it has at least 4 characters.
3. Return the validation result as a boolean (true/false) to the subscriber.

With the validation result, the subscriber simply sets the result to the
isUsernameLengthValid property. Since isUsernameLengthValid is also a publisher, we can
update the RequirementText control like this to subscribe to the changes and update the
UI accordingly:

RequirementText(iconColor: userRegistrationViewModel.isUsernameLengthValid ? Color


.secondary : Color(red: 251/255, green: 128/255, blue: 128/255), text: "A minimum
of 4 characters", isStrikeThrough: userRegistrationViewModel.isUsernameLengthValid
)

Both the icon color and the strike-through status depend on the validation result (i.e.
isUsernameLengthValid ).

This is how we use Combine to validate a form field. Although we haven't applied the
code changes to our project yet, it's important to understand the concept of publishers
and subscribers and how we perform validation using this approach. In the next section,
we will apply what we've learned and make the necessary code changes.

Validate the Passwords with Combine


Now that you understand how the validation of the username field is done, we will apply
a similar implementation for the password and password confirm fields.

The password field has two requirements:

1. The length of password should have at least 8 characters.


2. It should contain at least one uppercase letter.

To meet these requirements, we set up two subscribers like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 352


$password
.receive(on: RunLoop.main)
.map { password in
return password.count >= 8
}
.assign(to: \.isPasswordLengthValid, on: self)

$password
.receive(on: RunLoop.main)
.map { password in
let pattern = "[A-Z]"
if let _ = password.range(of: pattern, options: .regularExpression) {
return true
} else {
return false
}
}
.assign(to: \.isPasswordCapitalLetter, on: self)

The first subscriber subscribes to the verification result of the password length and
assigns it to the isPasswordLengthValid property. The second subscriber handles the
validation of the uppercase letter. We use the range method to test if the password
contains at least one uppercase letter. Once again, the subscriber assigns the validation
result directly to the isPasswordCapitalLetter property.

Now, let's move on to validating the password confirm field. In this case, the requirement
is that the password confirm field should have the same value as the password field. Both
password and passwordConfirm are publishers. To verify if both publishers have the same
value, we use Publisher.combineLatest to receive and combine the latest values from the
publishers. We can then compare the two values to check if they are the same. Here is the
code snippet:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 353


Publishers.CombineLatest($password, $passwordConfirm)
.receive(on: RunLoop.main)
.map { (password, passwordConfirm) in
return !passwordConfirm.isEmpty && (passwordConfirm == password)
}
.assign(to: \.isPasswordConfirmValid, on: self)

Similarly, we assign the validation result to the isPasswordConfirmValid property.

Implementing the UserRegistrationViewModel


Now that I've explained the implementation, let's put everything together into the
project. First, create a new Swift file named UserRegistrationViewModel.swift using the
Swift File template. Replace the whole file's content with the following code:

import Foundation
import Combine

class UserRegistrationViewModel: ObservableObject {


// Input
@Published var username = ""
@Published var password = ""
@Published var passwordConfirm = ""

// Output
@Published var isUsernameLengthValid = false
@Published var isPasswordLengthValid = false
@Published var isPasswordCapitalLetter = false
@Published var isPasswordConfirmValid = false

private var cancellableSet: Set<AnyCancellable> = []

init() {
$username
.receive(on: RunLoop.main)
.map { username in
return username.count >= 4
}
.assign(to: \.isUsernameLengthValid, on: self)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 354


.store(in: &cancellableSet)

$password
.receive(on: RunLoop.main)
.map { password in
return password.count >= 8
}
.assign(to: \.isPasswordLengthValid, on: self)
.store(in: &cancellableSet)

$password
.receive(on: RunLoop.main)
.map { password in
let pattern = "[A-Z]"
if let _ = password.range(of: pattern, options: .regularExpression
) {
return true
} else {
return false
}
}
.assign(to: \.isPasswordCapitalLetter, on: self)
.store(in: &cancellableSet)

Publishers.CombineLatest($password, $passwordConfirm)
.receive(on: RunLoop.main)
.map { (password, passwordConfirm) in
return !passwordConfirm.isEmpty && (passwordConfirm == password)
}
.assign(to: \.isPasswordConfirmValid, on: self)
.store(in: &cancellableSet)
}
}

The code is nearly the same as what we discussed in earlier sections. To use Combine,
you first need to import the Combine framework. In the init() method, we initialize all
the subscribers to listen to the value changes of the text fields and perform the
corresponding validations. One thing you may notice is the cancellableSet variable.
Additionally, for each subscriber, we call the store function at the end.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 355


What does the store function and cancellableSet variable do?

The assign function, which creates the subscriber, returns a cancellable instance. You
can use this instance to cancel the subscription at the appropriate time. The store

function allows us to save the cancellable reference into a set for later cleanup. If you
don't store the reference, the app may end up with memory leak issues.

So, when will the cleanup happen for this demo? Because cancellableSet is defined as a
property of the class, the cleanup and cancellation of the subscriptions will happen when
the class is deinitialized.

Now, switch back to ContentView.swift and update the UI controls. First, replace the
following state variables:

@State private var username = ""


@State private var password = ""
@State private var passwordConfirm = ""

with a view model and name it userRegistrationViewModel :

@ObservedObject private var userRegistrationViewModel = UserRegistrationViewModel(


)

Next, update the text field and the requirement text of username like this:

FormField(fieldName: "Username", fieldValue: $userRegistrationViewModel.username)

RequirementText(iconColor: userRegistrationViewModel.isUsernameLengthValid ? Color


.secondary : Color(red: 251/255, green: 128/255, blue: 128/255), text: "A minimum
of 4 characters", isStrikeThrough: userRegistrationViewModel.isUsernameLengthValid
)
.padding()

The fieldValue parameter is now changed to $userRegistrationViewModel.username . For


the requirement text, SwiftUI monitors the
userRegistrationViewModel.isUsernameLengthValid property and updates the requirement

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 356


text accordingly.

Similarly, update the UI code for the password and password confirm fields like this:

FormField(fieldName: "Password", fieldValue: $userRegistrationViewModel.password,


isSecure: true)

VStack {
RequirementText(iconName: "lock.open", iconColor: userRegistrationViewModel.is
PasswordLengthValid ? Color.secondary : Color(red: 251/255, green: 128/255, blue:
128/255), text: "A minimum of 8 characters", isStrikeThrough: userRegistrationView
Model.isPasswordLengthValid)

RequirementText(iconName: "lock.open", iconColor: userRegistrationViewModel.is


PasswordCapitalLetter ? Color.secondary : Color(red: 251/255, green: 128/255, blue
: 128/255), text: "One uppercase letter", isStrikeThrough: userRegistrationViewMod
el.isPasswordCapitalLetter)
}
.padding()

FormField(fieldName: "Confirm Password", fieldValue: $userRegistrationViewModel.pa


sswordConfirm, isSecure: true)

RequirementText(iconColor: userRegistrationViewModel.isPasswordConfirmValid ? Color


.secondary : Color(red: 251/255, green: 128/255, blue: 128/255), text: "Your confi
rm password should be the same as password", isStrikeThrough: userRegistrationView
Model.isPasswordConfirmValid)
.padding()
.padding(.bottom, 50)

That's it! You're now ready to test the app. If you've made all the changes correctly, the
app should now validate the user input.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 357


Figure 7. The registration form now validates the user input

Summary
I hope you now have gained some basic knowledge of the Combine framework. The
introduction of SwiftUI and Combine has completely changed the way we build apps.
Functional Reactive Programming (FRP) has become increasingly popular in recent
years. This is the first time Apple has released its own functional reactive framework. To
me, it represents a major paradigm shift. The company has finally taken a position on
FRP and recommends that Apple developers embrace this new programming
methodology.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 358


Like the introduction of any new technology, there will be a learning curve. Even if you've
been programming in iOS, it will take time to transition from the delegate-based
programming paradigm to the publishers and subscribers model.

However, once you become comfortable with the Combine framework, you will
appreciate its benefits, as it allows for more maintainable and modular code. As you can
now see, together with SwiftUI, communication between a view and a view model
becomes much easier.

For reference, you can download the complete form validation project here:

Demo project
(https://www.appcoda.com/resources/swiftui5/SwiftUIFormRegistration.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 359


Chapter 16
Working with Swipe-to-Delete,
Context Menu and Action Sheets
Previously, you learned how to present rows of data using a list. In this chapter, we will
delve deeper and explore how to enhance user interaction with the list view. Specifically,
we will learn how to enable users to:

Swipe to delete a row


Tap a row to invoke an action sheet
Touch and hold a row to bring up a context menu

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 360


Figure 1. Swipe to delete (left), context menu, and action sheet (right)

Previously, you learned about swipe-to-delete and action sheets, as shown in Figure 1.
These two user interface elements have been part of iOS for several years. Context
menus, on the other hand, were introduced in iOS 13 and resemble the peek and pop
functionality of 3D Touch. When a user force-touches a view configured with a context
menu, iOS displays a popover menu. As a developer, it's your responsibility to configure
the action items that appear in the menu.

While this chapter focuses on interactions within a list, the techniques I'm about to show
you can also be applied to other user interface controls, such as buttons.

Preparing the Starter Project

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 361


Let's begin by creating the demo. We will build an interactive list based on the restaurant
list app. You can download the starter project from
https://www.appcoda.com/resources/swiftui5/SwiftUIActionSheetStarter.zip. Once you
have downloaded the project, open it and take a look at the preview. It should display a
simple list with text and images. Later, we will add the swipe-to-delete feature, an action
sheet, and a context menu to this demo app.

Figure 2. The starter project should display a simple list view

If you have a keen eye, you may have noticed that the starter project uses ForEach to
implement the list instead of directly passing the collection of data to List . You might
wonder why I made this choice. The main reason is that the onDelete handler, which I
will explain shortly, only works with ForEach . It allows us to easily implement the swipe-
to-delete feature for individual rows in the list.

Implementing Swipe-to-delete

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 362


Assuming you have the starter project ready, let's begin implementing the swipe-to-
delete feature. As I mentioned before, we can activate swipe-to-delete for all rows in a list
by attaching the onDelete handler to the row data. To do this, update the List as
follows:

List {
ForEach(restaurants) { restaurant in
BasicImageRow(restaurant: restaurant)
}
.onDelete { (indexSet) in
self.restaurants.remove(atOffsets: indexSet)
}
}
.listStyle(.plain)

In the closure of onDelete , we pass an indexSet storing the index of the rows to be
deleted. We then call the remove method with the indexSet to delete the specific items
in the restaurants array.

Before the swipe-to-delete feature works, there is one more thing we need to do.
Whenever a user removes a row from the list, the UI should be updated accordingly. In
SwiftUI, we can use the @State keyword to notify SwiftUI to monitor the property and
update the UI whenever the value of the property changes. To do that, add the @State

keyword to the restaurants variable as follows:

@State var restaurants = [ ... ]

Once you have made the change, you're ready to test the delete feature in the preview
canvas. Swipe any of the rows to the left to reveal the Delete button. Tap it and that row
will be removed from the list. By the way, do you notice the nice animation while the row
is being removed? You don't need to write any extra code. This animation is
automatically generated by SwiftUI. Cool, right?

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 363


Figure 3. Deleting an item from the list

If you've implemented the same feature using UIKit, I'm sure you're amazed by SwiftUI.
With just a few lines of code and a keyword, you can implement the swipe-to-delete
feature.

Creating a Context Menu


Next, let's talk about context menus. As mentioned earlier, a context menu is similar to
peek and pop in 3D Touch. However, the great thing about context menus is that they
work on all devices running iOS 13 and later, even if the device doesn't support 3D
Touch. To bring up a context menu, you can use the touch and hold gesture or force
touch on devices that support 3D Touch.

SwiftUI has made it very simple to implement a context menu. All you need to do is
attach the contextMenu container to the view and configure its menu items.

For our demo app, we want to trigger the context menu when users touch and hold any of
the rows. The menu will provide two action buttons for users to choose from: Delete and
Favorite. When the Delete button is selected, the corresponding row will be removed
from the list. The Favorite button will mark the selected row with a star indicator.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 364


To present these two items in the context menu, we need to attach the contextMenu

modifier to each row in the list, like this:

List {
ForEach(restaurants) { restaurant in
BasicImageRow(restaurant: restaurant)
.contextMenu {

Button(action: {
// delete the selected restaurant
}) {
HStack {
Text("Delete")
Image(systemName: "trash")
}
}

Button(action: {
// mark the selected restaurant as favorite
}) {
HStack {
Text("Favorite")
Image(systemName: "star")
}
}
}
}
.onDelete { (indexSet) in
self.restaurants.remove(atOffsets: indexSet)
}
}
.listStyle(.plain)

We haven't implemented any of the button actions yet. However, if you test the app, the
app will bring up the context menu when you touch and hold one of the rows.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 365


Figure 4. Activating the context menu

Let's continue by implementing the delete action. Unlike the onDelete handler, the
contextMenu doesn't give us the index of the selected restaurant. To figure it out, it would
require a little bit of work. Create a new function in ContentView :

private func delete(item restaurant: Restaurant) {


if let index = self.restaurants.firstIndex(where: { $0.id == restaurant.id })
{
self.restaurants.remove(at: index)
}
}

The delete function takes a restaurant object as input and searches for its index in the
restaurants array. To find the index, we use the firstIndex function and specify the
search criteria. The function iterates through the array and compares the id of the given
restaurant with the id values in the array. If there is a match, the firstIndex function
returns the index of the given restaurant. Once we have the index, we can remove the
restaurant from the restaurants array by calling remove(at:) .

Next, insert the following line of code under // delete the selected restaurant :

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 366


self.delete(item: restaurant)

We simply call the delete function when the user selects the Delete button. Now you're
ready to test the app. Click the Play button in the canvas to run the app or simply test it
in the preview canvas. Press and hold one of the rows to bring up the context menu.
Choose Delete and you should see the selected restaurant removed from the list.

Let's move on to the implementation of the Favorite button. When this button is
selected, the app will place a star in the selected restaurant's row. To implement this
feature, we first need to modify the Restaurant struct and add a new property named
isFavorite like this:

struct Restaurant: Identifiable {


var id = UUID()
var name: String
var image: String
var isFavorite: Bool = false
}

This isFavorite property indicates whether the restaurant is marked as a favorite. By


default, it's set to false .

Similar to the Delete feature, we'll create a separate function in ContentView for setting a
favorite restaurant. Insert the following code to create the new function:

private func setFavorite(item restaurant: Restaurant) {


if let index = self.restaurants.firstIndex(where: { $0.id == restaurant.id })
{
self.restaurants[index].isFavorite.toggle()
}
}

The code is very similar to that of the delete function. We first find out the index of the
given restaurant. Once we have the index, we change the value of its isFavorite

property. Here we invoke the toggle function to toggle the value. For example, if the

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 367


original value of isFavorite is set to false , the value will change to true after calling
toggle() .

Next, we have to handle the UI for the row. Whenever the restaurant's isFavorite

property is set to true , the row should present a star indicator. Update the
BasicImageRow struct like this:

struct BasicImageRow: View {


var restaurant: Restaurant

var body: some View {


HStack {
Image(restaurant.image)
.resizable()
.frame(width: 40, height: 40)
.cornerRadius(5)
Text(restaurant.name)

if restaurant.isFavorite {
Spacer()

Image(systemName: "star.fill")
.foregroundColor(.yellow)
}
}
}
}

In the code above, we just add a code snippet in the HStack . If the isFavorite property
of the given restaurant is set to true , we add a spacer and a system image to the row.

That's how we implement the Favorite feature. Lastly, insert the following line of code
under // mark the selected restaurant as favorite to invoke the setFavorite function:

self.setFavorite(item: restaurant)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 368


Now it's time to test. Execute the app in the canvas. Press and hold one of the rows (e.g.
Petite Oyster), and then choose Favorite. You should see a star app appeared at the end
of the row.

Figure 5. Using the Favorite function

Working with Action Sheets


That is how you implement context menus. Lastly, let's see how to create an action sheet
in SwiftUI. The action sheet that we are going to build provides the same options as the
context menu. If you forgot what the action sheet looks like, please refer to figure 1 again.

The SwiftUI framework comes with an ActionSheet view for you to create an action
sheet. Basically, you can create an action sheet like this:

ActionSheet(title: Text("What do you want to do"), message: nil, buttons: [.default


(Text("Delete"))]

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 369


You initialize an action sheet with a title and an optional message. The buttons

parameter accepts an array of buttons. In the sample code above, it provides a default
button titled Delete.

To activate an action sheet, you attach the actionSheet modifier to a button or any view.
If you look into SwiftUI's documentation, you have two ways to bring up an action sheet.

You can control the appearance of an action sheet by using the isPresented parameter:

func actionSheet(isPresented: Binding<Bool>, content: () -> ActionSheet) -> some V


iew

Or through an optional binding:

func actionSheet<T>(item: Binding<T?>, content: (T) -> ActionSheet) -> some View w
here T : Identifiable

We will use both approaches to present the action sheet, so you'll understand when to use
which approach.

For the first approach, we need a Boolean variable to represent the status of the action
and also a variable of the type Restaurant to store the selected restaurant. So, declare
these two variables in ContentView :

@State private var showActionSheet = false

@State private var selectedRestaurant: Restaurant?

By default, the showActionSheet variable is set to false , indicating that the action sheet
is not shown. We will toggle this variable to true when a user selects a row. The
selectedRestaurant variable, as its name suggests, is designed to hold the chosen
restaurant. Both variables are marked with the @State keyword because we want
SwiftUI to monitor their changes and update the UI accordingly.

Next, attach the onTapGesture and actionSheet modifiers to the List view like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 370


List {
ForEach(restaurants) { restaurant in
BasicImageRow(restaurant: restaurant)
.contextMenu {

...
}
.onTapGesture {
self.showActionSheet.toggle()
self.selectedRestaurant = restaurant
}
.actionSheet(isPresented: self.$showActionSheet) {

ActionSheet(title: Text("What do you want to do"), message: nil, b


uttons: [

.default(Text("Mark as Favorite"), action: {


if let selectedRestaurant = self.selectedRestaurant {
self.setFavorite(item: selectedRestaurant)
}
}),

.destructive(Text("Delete"), action: {
if let selectedRestaurant = self.selectedRestaurant {
self.delete(item: selectedRestaurant)
}
}),

.cancel()
])
}
}
.onDelete { (indexSet) in
self.restaurants.remove(atOffsets: indexSet)
}
}

The onTapGesture modifier, attached to each row, is used to detect users' touch. When a
row is tapped, the code block within onTapGesture will be executed. Here, we toggle the
showActionSheet variable and set the selectedRestaurant variable to the selected

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 371


restaurant.

Earlier, I explained the usage of the actionSheet modifier. In the code above, we pass the
isPresented parameter with the binding of showActionSheet . When showActionSheet is set
to true , the code block will be executed. We initialize an ActionSheet with three
buttons: Mark as Favorite, Delete, and Cancel. Action sheets have three types of buttons:
default, destructive, and cancel. The default button type is used for ordinary actions. A
destructive button is similar to a default button but with red font color to indicate
destructive actions, such as delete. The cancel button is a special type for dismissing the
action sheet.

The Mark as Favorite button is our default button. In the action closure, we call the
setFavorite function to add the star. For the destructive button, we use Delete. Similar
to the Delete button of the context menu, we call the delete function to remove the
selected restaurant.

If you've made the changes correctly, you should be able to bring up the action sheet
when you tap one of the rows in the list view. Selecting the Delete button will remove the
row. If you choose the Mark as Favorite option, you will mark the row with a yellow star.

Figure 6. Tapping a row to reveal the action sheet

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 372


Everything works great, but I promised to walk you through the second approach of
using the actionSheet modifier. The first approach, which we have covered, relies on a
Boolean value ( showActionSheet ) to indicate whether the action sheet should be
displayed.

The second approach triggers the action sheet through an optional Identifiable binding:

func actionSheet<T>(item: Binding<T?>, content: (T) -> ActionSheet) -> some View w
here T : Identifiable

In plain English, this means the action sheet will be shown when the item you pass has a
value. In our case, the selectedRestaurant variable is an optional that conforms to the
Identifiable protocol. To use the second approach, you just need to pass the binding of
selectedRestaurant to the actionSheet modifier like this:

.actionSheet(item: self.$selectedRestaurant) { restaurant in

ActionSheet(title: Text("What do you want to do"), message: nil, buttons: [

.default(Text("Mark as Favorite"), action: {


self.setFavorite(item: restaurant)
}),

.destructive(Text("Delete"), action: {
self.delete(item: restaurant)
}),

.cancel()
])
}

If the selectedRestaurant has a value, the app will bring up the action sheet. From the
closure's parameter, you can retrieve the selected restaurant and perform the operations
accordingly.

When you use this approach, you no longer need the boolean variable shownActionSheet .
You can remove it from the code:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 373


@State private var showActionSheet = false

Also, in the tapGesture modifier, remove the line of the code that toggles the
showActionSheet variable:

self.showActionSheet.toggle()

Test the app again. The action sheet looks still the same, but you implemented the action
sheet with a different approach.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 374


Exercise
Now that you have some idea of how to build a context menu, let's have an exercise to
test your knowledge. Your task is to add a Check-in item to the context menu. When a
user selects this option, the app should add a check-in indicator to the selected
restaurant. You can refer to figure 7 for the sample UI. In the sample, I used the system
image named checkmark.seal.fill for the check-in indicator, but you are free to choose
your own image.

Please take some time to work on the exercise before checking out the solution. Have fun!

Figure 7. Adding a check-in feature

For reference, you can download the complete project here:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 375


Demo project and solution to exercise
(https://www.appcoda.com/resources/swiftui5/SwiftUIActionSheet.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 376


Chapter 17
Understanding Gestures
In earlier chapters, you got a taste of building gestures with SwiftUI. We used the
onTapGesture modifier to handle a user's touch and provide a corresponding response. In
this chapter, let's dive deeper and explore how to work with various types of gestures in
SwiftUI.

The SwiftUI framework provides several built-in gestures, such as the tap gesture we
have used before. Additionally, there are gestures like DragGesture,
MagnificationGesture, and LongPressGesture that are ready to use. We will explore a
couple of these gestures and see how to work with them in SwiftUI. Moreover, you will
learn how to build a generic view that supports the drag gesture.

Figure 1. A demo showing the draggable view

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 377


Using the Gesture Modifier
To recognize a particular gesture using SwiftUI, you can attach a gesture recognizer to a
view using the .gesture modifier. Here is a sample code snippet that attaches a
TapGesture using the .gesture modifier:

var body: some View {


Image(systemName: "star.circle.fill")
.font(.system(size: 200))
.foregroundColor(.green)
.gesture(
TapGesture()
.onEnded({
print("Tapped!")
})
)
}

If you want to try out the code, create a new project using the App template and make
sure you select SwiftUI for the Interface option. Then, paste the code into
ContentView.swift .

By modifying the code above slightly and introducing a state variable, we can create a
simple scale animation when the star image is tapped. Here is the updated code:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 378


struct ContentView: View {
@State private var isPressed = false

var body: some View {


Image(systemName: "star.circle.fill")
.font(.system(size: 200))
.scaleEffect(isPressed ? 0.5 : 1.0)
.animation(.easeInOut, value: isPressed)
.foregroundColor(.green)
.gesture(
TapGesture()
.onEnded({
self.isPressed.toggle()
})
)
}
}

When you run the code in the canvas or simulator, you should see a scaling effect. This
demonstrates how to use the .gesture modifier to detect and respond to specific touch
events. If you need a refresher on how animations work, please refer back to chapter 9.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 379


Figure 2. A simple scaling effect

Using Long Press Gesture


One of the built-in gestures is LongPressGesture . This gesture recognizer allows you to
detect a long-press event. For example, if you want to resize the star image only when the
user presses and holds it for at least 1 second, you can use the LongPressGesture to detect
the touch event.

Modify the code in the .gesture modifier like this to implement the LongPressGesture :

.gesture(
LongPressGesture(minimumDuration: 1.0)
.onEnded({ _ in
self.isPressed.toggle()
})
)

In the preview canvas, you have to press and hold the star image for at least a second
before it toggles its size.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 380


The @GestureState Property Wrapper
When you press and hold the star image, the image doesn't give the user any response
until the long-press event is detected. Obviously, there is something we can do to
improve the user experience. What I want to do is to give the user immediate feedback
when he/she taps the image. Any kind of feedback will help to improve the situation.
Let's dim the image a bit when the user taps it. This will let the user know that our app
captures the touch and is doing work. Figure 3 illustrates how the animation works.

Figure 3. Applying a dimming effect when the image is tapped

To implement the animation, you need to keep track of the state of gestures. During the
performance of the long press gesture, we have to differentiate between tap and long
press events. So, how do we do that?

SwiftUI provides a property wrapper called @GestureState which conveniently tracks the
state change of a gesture and lets developers decide the corresponding action. To
implement the animation we just described, we can declare a property using
@GestureState like this:

@GestureState private var longPressTap = false

This gesture state variable indicates whether a tap event is detected during the
performance of the long press gesture. Once you have the variable defined, you can
modify the code of the Image view like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 381


Image(systemName: "star.circle.fill")
.font(.system(size: 200))
.opacity(longPressTap ? 0.4 : 1.0)
.scaleEffect(isPressed ? 0.5 : 1.0)
.animation(.easeInOut, value: isPressed)
.foregroundColor(.green)
.gesture(
LongPressGesture(minimumDuration: 1.0)
.updating($longPressTap, body: { (currentState, state, transaction) in
state = currentState
})
.onEnded({ _ in
self.isPressed.toggle()
})
)

We only made a couple of changes in the code above. First, we added the .opacity

modifier. When the tap event is detected, we set the opacity value to 0.4 so that the
image becomes dimmer.

Second, we added the updating method of the LongPressGesture . During the performance
of the long press gesture, this method will be called. It accepts three parameters: value,
state, and transaction:

The value parameter is the current state of the gesture. This value varies from
gesture to gesture, but for the long press gesture, a true value indicates that a tap is
detected.
The state parameter is actually an in-out parameter that lets you update the value of
the longPressTap property. In the code above, we set the value of state to
currentState . In other words, the longPressTap property always keeps track of the
latest state of the long press gesture.
The transaction parameter stores the context of the current state-processing
update.

After you make the code change, run the project in the preview canvas to test it. The
image immediately becomes dimmer when you tap it. Keep holding it for one second, and
then the image resizes itself.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 382


The opacity of the image is automatically reset to normal when the user releases the long
press. Do you wonder why? This is an advantage of @GestureState . When the gesture
ends, it automatically sets the value of the gesture state property to its initial value,
false in our case.

Using Drag Gesture


Now that you understand how to use the .gesture modifier and @GestureState , let's look
into another common gesture: Drag. We are going to modify the existing code to support
the drag gesture, allowing a user to drag the star image to move it around.

Replace the ContentView struct like this:

struct ContentView: View {


@GestureState private var dragOffset = CGSize.zero

var body: some View {


Image(systemName: "star.circle.fill")
.font(.system(size: 100))
.offset(x: dragOffset.width, y: dragOffset.height)
.animation(.easeInOut, value: dragOffset)
.foregroundColor(.green)
.gesture(
DragGesture()
.updating($dragOffset, body: { (value, state, transaction) in

state = value.translation
})
)
}
}

To recognize a drag gesture, you initialize a DragGesture instance and listen for updates.
In the update function, we use a gesture state property to keep track of the drag event.
Similar to the long press gesture, the closure of the update function accepts three

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 383


parameters. In this case, the value parameter stores the current data of the drag,
including the translation. We set the state variable, which is actually the dragOffset , to
value.translation .

Test the project in the preview canvas and try dragging the image around. When you
release it, the image returns to its original position.

You may be wondering why the image returns to its starting point. As explained in the
previous section, one advantage of using @GestureState is that it resets the value of the
property to its original value when the gesture ends. Therefore, when you end the drag
and release the press, the dragOffset is reset to .zero , which is its original position.

But what if you want the image to stay at the end point of the drag? How can you achieve
that? Take a few minutes to think about how to implement it.

Since the @GestureState property wrapper will reset the property to its original value, we
need another state property to save the final position. Let's declare a new state property
called finalOffset as a CGSize to store the final position of the dragged image:

@State private var position = CGSize.zero

Next, update the body variable like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 384


var body: some View {
Image(systemName: "star.circle.fill")
.font(.system(size: 100))
.offset(x: position.width + dragOffset.width, y: position.height + dragOff
set.height)
.animation(.easeInOut, value: dragOffset)
.foregroundColor(.green)
.gesture(
DragGesture()
.updating($dragOffset, body: { (value, state, transaction) in

state = value.translation
})
.onEnded({ (value) in
self.position.height += value.translation.height
self.position.width += value.translation.width
})
)
}

We have made a couple of changes to the code:

1. We implemented the onEnded function which is called when the drag gesture ends.
In the closure, we compute the new position of the image by adding the drag offset.
2. The .offset modifier was also updated, such that we take the current position into
account.

Now when you run the project and drag the image, the image stays where it is even after
the drag ends.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 385


Figure 4. Drag the image around

Combining Gestures
In some cases, you need to use multiple gesture recognizers in the same view. Let's say,
we want the user to press and hold the image before starting the drag, we have to
combine both long press and drag gestures. SwiftUI allows you to easily combine
gestures to perform more complex interactions. It provides three gesture composition
types including simultaneous, sequenced, and exclusive.

When you need to detect multiple gestures at the same time, you use the simultaneous
composition type. When you combine gestures using the exclusive composition type,
SwiftUI recognizes all the gestures you specify but it will ignore the rest when one of the
gestures is detected.

As the name suggests, if you combine multiple gestures using the sequenced composition
type, SwiftUI recognizes the gestures in a specific order. This is the type of the
composition that we will use to sequence the long press and drag gestures.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 386


To work with multiple gestures, you update the code like this:

struct ContentView: View {


// For long press gesture
@GestureState private var isPressed = false

// For drag gesture


@GestureState private var dragOffset = CGSize.zero
@State private var position = CGSize.zero

var body: some View {


Image(systemName: "star.circle.fill")
.font(.system(size: 100))
.opacity(isPressed ? 0.5 : 1.0)
.offset(x: position.width + dragOffset.width, y: position.height + dra
gOffset.height)
.animation(.easeInOut, value: dragOffset)
.foregroundColor(.green)
.gesture(
LongPressGesture(minimumDuration: 1.0)
.updating($isPressed, body: { (currentState, state, transaction) in

state = currentState
})
.sequenced(before: DragGesture())
.updating($dragOffset, body: { (value, state, transaction) in

switch value {
case .first(true):
print("Tapping")
case .second(true, let drag):
state = drag?.translation ?? .zero
default:
break
}

})
.onEnded({ (value) in

guard case .second(true, let drag?) = value else {


return

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 387


}

self.position.height += drag.translation.height
self.position.width += drag.translation.width
})
)
}
}

You should already be familiar with some parts of the code snippet as we are combining
the previously implemented long press gesture with the drag gesture.

Let's go through the code in the .gesture modifier line by line. We require the user to
press and hold the image for at least one second before they can begin dragging it. We
start by creating the LongPressGesture . Similar to our previous implementation, we have a
isPressed gesture state property that controls the opacity of the image when tapped.

The keyword sequenced is used to link the long press and drag gestures together. We tell
SwiftUI that the LongPressGesture should occur before the DragGesture .

The code in both the updating and onEnded functions looks quite similar, but the value

parameter now contains data from both gestures (i.e., long press and drag). We use a
switch statement to differentiate between the gestures. You can use the .first and
.second cases to determine which gesture is being handled. Since the long press gesture
should be recognized before the drag gesture, the first gesture here refers to the long
press gesture. In the code, we simply print the Tapping message for reference.

When the long press is confirmed, we reach the .second case. Here, we extract the drag
data and update the dragOffset property with the corresponding translation.

When the drag ends, the onEnded function is called. Similarly, we update the final
position by retrieving the drag data (i.e., .second case).

Now you're ready to test the combination of gestures. Run the app in the preview canvas
using the debug preview so that you can see the message in the console. You won't be
able to drag the image until you hold the star image for at least one second.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 388


Figure 5. Dragging only happens when a user presses and holds the image for at least
one second

Refactoring the Code Using Enum


A better way to organize the drag state is by using Enum. This allows you to combine the
isPressed and dragOffset state into a single property. Let's declare an enumeration
called DragState .

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 389


enum DragState {
case inactive
case pressing
case dragging(translation: CGSize)

var translation: CGSize {


switch self {
case .inactive, .pressing:
return .zero
case .dragging(let translation):
return translation
}
}

var isPressing: Bool {


switch self {
case .pressing, .dragging:
return true
case .inactive:
return false
}
}
}

We have three states here: inactive, pressing, and dragging. These states are good
enough to represent the states during the performance of the long press and drag
gestures. For the dragging state, we associate it with the translation of the drag.

With the DragState enum, we can modify the original code like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 390


struct ContentView: View {
@GestureState private var dragState = DragState.inactive
@State private var position = CGSize.zero

var body: some View {


Image(systemName: "star.circle.fill")
.font(.system(size: 100))
.opacity(dragState.isPressing ? 0.5 : 1.0)
.offset(x: position.width + dragState.translation.width, y: position.h
eight + dragState.translation.height)
.animation(.easeInOut, value: dragState.translation)
.foregroundColor(.green)
.gesture(
LongPressGesture(minimumDuration: 1.0)
.sequenced(before: DragGesture())
.updating($dragState, body: { (value, state, transaction) in

switch value {
case .first(true):
state = .pressing
case .second(true, let drag):
state = .dragging(translation: drag?.translation ?? .zero)
default:
break
}

})
.onEnded({ (value) in

guard case .second(true, let drag?) = value else {


return
}

self.position.height += drag.translation.height
self.position.width += drag.translation.width
})
)
}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 391


We have now declared a dragState property to track the state of the drag gesture. By
default, it is set to DragState.inactive . The code is very similar to the previous
implementation, but it has been modified to work with the dragState property instead of
isPressed and dragOffset . For instance, in the .offset modifier, we retrieve the drag
offset from the associated value of the dragging state.

The outcome of the code remains the same. However, it is considered good practice to
use an enum to track complex states of gestures.

Building a Generic Draggable View


So far, we have successfully built a draggable image view. But, what if we want to build a
draggable text view or a draggable circle? Should we just copy and paste all the code to
create the text view or circle?

There is a better way to implement that. Let's see how we can build a generic draggable
view.

In the project navigator, right click the SwiftUIGesture folder and choose New File....
Select the SwiftUI View template and name the file DraggableView .

Declare the DragState enum and update the DraggableView struct like this:

enum DraggableState {
case inactive
case pressing
case dragging(translation: CGSize)

var translation: CGSize {


switch self {
case .inactive, .pressing:
return .zero
case .dragging(let translation):
return translation
}
}

var isPressing: Bool {


switch self {

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 392


case .pressing, .dragging:
return true
case .inactive:
return false
}
}
}

struct DraggableView<Content>: View where Content: View {


@GestureState private var dragState = DraggableState.inactive
@State private var position = CGSize.zero

var content: () -> Content

var body: some View {


content()
.opacity(dragState.isPressing ? 0.5 : 1.0)
.offset(x: position.width + dragState.translation.width, y: position.h
eight + dragState.translation.height)
.animation(.easeInOut, value: dragState.translation)
.gesture(
LongPressGesture(minimumDuration: 1.0)
.sequenced(before: DragGesture())
.updating($dragState, body: { (value, state, transaction) in

switch value {
case .first(true):
state = .pressing
case .second(true, let drag):
state = .dragging(translation: drag?.translation ?? .zero)
default:
break
}

})
.onEnded({ (value) in

guard case .second(true, let drag?) = value else {


return
}

self.position.height += drag.translation.height

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 393


self.position.width += drag.translation.width
})
)
}
}

All of the code is very similar to what we have written before. The key is to declare
DraggableView as a generic view and create a content property that accepts any View .
We then apply the long press and drag gestures to this content view.

Now you can test this generic view by replacing the #Preview code block like this:

#Preview {
DraggableView() {
Image(systemName: "star.circle.fill")
.font(.system(size: 100))
.foregroundColor(.green)
}
}

In the code, we initialize a DraggableView and provide our own content, which in this case
is the star image. By doing so, we create a reusable DraggableView that supports the long
press and drag gestures, and we can use it with any content we want.

So, what if we want to build a draggable text view? You can replace the code snippet with
the following code:

#Preview {
DraggableView() {
Text("Swift")
.font(.system(size: 50, weight: .bold, design: .rounded))
.bold()
.foregroundColor(.red)
}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 394


In the closure, we create a text view instead of an image view. If you run the project in the
preview canvas, you can drag the text view to move it around (remember to long press for
1 second). It's pretty cool, isn't it?

Figure 6. A draggable text view

If you want to create a draggable circle, you can replace the code like this:

#Preview {
DraggableView() {
Circle()
.frame(width: 100, height: 100)
.foregroundColor(.purple)
}
}

That's how you create a generic draggable. Try to replace the circle with other views to
make your own draggable view and have fun!

Exercise

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 395


We've explored three built-in gestures in this chapter: tap, drag, and long press.
However, there are a couple more gestures we haven't explored yet. As an exercise, try to
create a generic scalable view that can recognize the MagnificationGesture and scale any
given view accordingly. Figure 7 shows a sample result.

Figure 7. A scalable image view

Summary
The SwiftUI framework has made gesture handling incredibly easy. As you've learned in
this chapter, the framework provides several ready-to-use gesture recognizers. Enabling
a view to support a specific type of gesture is as simple as attaching the .gesture

modifier to it. Composing multiple gestures has also become much more straightforward.

It's a growing trend to build gesture-driven user interfaces for mobile apps. With the
user-friendly API provided by SwiftUI, you can now empower your apps with useful
gestures to delight your users.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 396


For reference, you can download the complete gesture project here:

Demo project (https://www.appcoda.com/resources/swiftui5/SwiftUIGesture.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 397


Chapter 18
Displaying an Expandable Bottom
Sheet Using Presentation Detents
Bottom sheets have gained popularity recently and can be found in popular apps like
Facebook and Uber. They are an enhanced version of action sheets that slide up from the
bottom of the screen and overlay on top of the original view. Bottom sheets provide
contextual information or additional user options. For example, Instagram displays a
bottom sheet when saving a photo to a collection, Facebook shows a sheet with additional
actions when clicking the ellipsis button of a post, and Uber uses bottom sheets to display
trip pricing.

The size of bottom sheets can vary depending on the information being displayed. Some
bottom sheets, known as backdrops, occupy 80-90% of the screen. Users can interact
with the sheet using drag gestures, sliding it up to expand or down to minimize or
dismiss it.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 398


Figure 1. Uber, Facebook and Instagram all use bottom sheets in their apps

In this chapter, we will create an expandable bottom sheet using SwiftUI gestures. The
demo app features a list of restaurants in the main view. When a user taps on a
restaurant record, a bottom sheet will appear, displaying the details of the selected
restaurant. You can expand the sheet by sliding it up. To dismiss the sheet, you can slide
it down.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 399


Figure 2. Building a expandable bottom sheet

Introducing Presentation Detents


In iOS 15, Apple introduced the UISheetPresentationController class for displaying
expandable bottom sheets in iOS apps. However, this feature was initially limited to the
UIKit framework. In SwiftUI, you had to either create your own component or rely on
third-party libraries to implement bottom sheets.

However, starting from iOS 16, SwiftUI includes a built-in modifier called
presentationDetents that allows for presenting resizable bottom sheets.

To present a bottom sheet using SwiftUI, you can use the sheet view and apply the
presentationDetents modifier. Here is an example of how to use it:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 400


struct BasicBottomSheet: View {
@State private var showSheet = false

var body: some View {


VStack {
Button("Show Bottom Sheet") {
showSheet.toggle()
}
.buttonStyle(.borderedProminent)
.sheet(isPresented: $showSheet) {
Text("This is the expandable bottom sheet.")
.presentationDetents([.medium, .large])
}

Spacer()
}
}
}

You specify a set of detents in the presentationDetents modifier. In the example provided,
the bottom sheet supports both medium and large sizes. When initially presented, the
bottom sheet appears in the medium size. However, you can expand it to the large size by
dragging the sheet upwards.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 401


Figure 3. A sample bottom sheet

Understanding the Starter Project


To save you some time building the demo app from the ground up, I've prepared a starter
project for you. You can download it from
https://www.appcoda.com/resources/swiftui5/SwiftUIBottomSheetStarter.zip. Unzip
the file and open SwiftUIBottomSheet.xcodeproj to get started.

The starter project comes with a set of restaurant images and the restaurant data. If you
look in the Model folder in the project navigator, you should find a file named
Restaurant.swift . This file contains the Restaurant struct and the set of sample
restaurant data.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 402


struct Restaurant: Identifiable {
var id: UUID = UUID()
var name: String
var type: String
var location: String
var phone: String
var description: String
var image: String
var isVisited: Bool

init(name: String, type: String, location: String, phone: String, description:


String, image: String, isVisited: Bool) {
self.name = name
self.type = type
self.location = location
self.phone = phone
self.description = description
self.image = image
self.isVisited = isVisited
}

init() {
self.init(name: "", type: "", location: "", phone: "", description: "", im
age: "", isVisited: false)
}
}

I've created the main view for you that displays a list of restaurants. You can open the
ContentView.swift file to check out the code. I am not going to explain the code in details
as we have gone through the implementation of list in chapter 10.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 403


Figure 4. The list view

Creating the Restaurant Detail View


The first step is to create the restaurant detail view, which will be displayed within the
bottom sheet. This view will contain the restaurant details along with a small handlebar,
as illustrated in figure 4.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 404


Figure 5. The restaurant detail view with a small handlebar

Before we proceed with the implementation, I encourage you to consider it as an exercise


and create the detail view on your own. The detail view is composed of various UI
components such as an image, text, and a scroll view. Since we have covered all these
components, it would be a great opportunity for you to practice and provide your own
implementation.

However, if you would like to refer to my implementation, I'll be glad to assist you. Let's
begin building the detail view, breaking it down into multiple parts for easier
implementation:

The handlebar - it is a small rounded rectangle that serves as a handle for the bottom
sheet.
The title bar - this section contains the title of the detail view.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 405


The header view - this view displays the featured image, restaurant name, and type.
The detail info view - this section displays the restaurant data, including the address,
phone, and description

We will implement each of the above using a separate struct to better organize our
code. Now create a new file using the SwiftUI View template and name it
RestaurantDetailView.swift . All the code discussed below will be put in this new file.

Handlebar
Let's start with the handlebar. It's a small rectangle with rounded corners. We can use a
Rectangle shape and apply rounded corners to it. Open the RestaurantDetailView.swift

file and add the following code snippet to create the handlebar:

struct HandleBar: View {

var body: some View {


Rectangle()
.frame(width: 50, height: 5)
.foregroundStyle(Color(.systemGray5))
.cornerRadius(10)
}
}

Title Bar
Next, it's the title bar. The implementation is simple since it's just a Text view. Let's
create another struct for it:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 406


struct TitleBar: View {

var body: some View {


HStack {
Text("Restaurant Details")
.font(.headline)
.foregroundStyle(.primary)

Spacer()
}
.padding()
}
}

The spacer here is used to align the text to the left.

Header View
The header view consists of an image view and two text views. The text views are overlaid
on top of the image view. To implement the header view, let's create a separate struct

for it. In the RestaurantDetailView.swift file, add the following code snippet:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 407


struct HeaderView: View {
let restaurant: Restaurant

var body: some View {


Image(restaurant.image)
.resizable()
.scaledToFill()
.frame(height: 300)
.clipped()
.overlay(
HStack {
VStack(alignment: .leading) {
Spacer()
Text(restaurant.name)
.foregroundStyle(.white)
.font(.system(.largeTitle, design: .rounded))
.bold()

Text(restaurant.type)
.font(.system(.headline, design: .rounded))
.padding(5)
.foregroundStyle(.white)
.background(Color.red)
.cornerRadius(5)

}
Spacer()
}
.padding()
)
}
}

Since we need to display the restaurant data, the HeaderView has the restaurant

property. For the layout, we create an Image view and set the content mode to
scaleToFill . The height of the image is fixed at 300 points. To ensure the image stays
within its frame, we attach the .clipped() modifier to hide any content that extends
beyond the edges of the image.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 408


For the two labels, we use the .overlay modifier to overlay two Text views on top of the
image.

Detail Info View


Lastly, let's implement the information view. If you examine the address, phone, and
description fields closely, you'll notice that they have a similar structure. Both the
address and phone fields have an icon next to the textual information, while the
description field contains text only.

So, wouldn't it be great to build a view which is flexible to handle both field types? Here is
the code snippet:

struct DetailInfoView: View {


let icon: String?
let info: String

var body: some View {


HStack {
if icon != nil {
Image(systemName: icon!)
.padding(.trailing, 10)
}
Text(info)
.font(.system(.body, design: .rounded))

Spacer()
}.padding(.horizontal)
}
}

The DetailInfoView takes two parameters: icon and info . The icon parameter is
optional, which means it can either have a value or be nil.

To present a data field with an icon, you can use the DetailInfoView as follows:

DetailInfoView(icon: "map", info: self.restaurant.location)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 409


Alternatively, if you only need to present a text-only field like the description field, you
use the DetailInfoView like this:

DetailInfoView(icon: nil, info: self.restaurant.description)

As you can see, by building a generic view to handle similar layout, you make the code
more modular and reusable.

Using VStack to Glue Them All Together


Now that we have built all components, we can combine them by using VStack like this:

struct RestaurantDetailView: View {


let restaurant: Restaurant

var body: some View {


VStack {
Spacer()

HandleBar()

TitleBar()

HeaderView(restaurant: self.restaurant)

DetailInfoView(icon: "map", info: self.restaurant.location)


.padding(.top)
DetailInfoView(icon: "phone", info: self.restaurant.phone)
DetailInfoView(icon: nil, info: self.restaurant.description)
.padding(.top)
}
.background(.white)
.cornerRadius(10, antialiased: true)
}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 410


The code above is self explanatory. We use the components that were built in the earlier
sections and embed them in a vertical stack. Originally, the VStack has a transparent
background. To ensure that the detail view has a white background, we attach the
background modifier.

Before you can test the detail view, you have to modify the code of
RestaurantDetailView_Previews like this:

The code above is self-explanatory. We use the components that were built in the earlier
sections and embed them in a vertical stack. The VStack originally has a transparent
background, but to ensure that the detail view has a white background, we attach the
background modifier.

To test the detail view, you need to modify the code of the #Preview section as follows:

#Preview {
RestaurantDetailView(restaurant: restaurants[0])
}

In the code, we pass a sample restaurant (i.e., restaurants[0] ) for testing. If you've
followed everything correctly, Xcode should display a similar detail view in the preview
canvas, resembling figure 6.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 411


Figure 6. The restaurant detail view

Make It Scrollable
Do you notice that the detail view can't display the full description? To fix the issue, we
have to make the detail view scrollable by embedding the content in a ScrollView like
this:

Do you notice that the detail view cannot display the full description? To fix this issue, we
need to make the detail view scrollable. We can achieve this by embedding the content in
a ScrollView like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 412


struct RestaurantDetailView: View {
let restaurant: Restaurant

var body: some View {


VStack {
Spacer()

HandleBar()

ScrollView(.vertical) {
TitleBar()

HeaderView(restaurant: self.restaurant)

DetailInfoView(icon: "map", info: self.restaurant.location)


.padding(.top)
DetailInfoView(icon: "phone", info: self.restaurant.phone)
DetailInfoView(icon: nil, info: self.restaurant.description)
.padding(.top);
}
.background(.white)
.cornerRadius(10, antialiased: true)
}
}
}

Except for the handlebar, the rest of the views are wrapped within the scroll view. If you
run the app in the preview canvas again, you will notice that the detail view is now
scrollable.

Bring Up the Detail View


Now that the detail view is pretty much done. Let's go back to the list view (i.e.
ContentView.swift ) to bring it up whenever a user selects a restaurant.

In the ContentView struct, declare a state variable called selectedRestaurant to store the
user's chosen restaurant. This variable will be of type Restaurant? to represent an
optional restaurant selection.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 413


@State private var selectedRestaurant: Restaurant?

As you've learned in an earlier chapter, you can attach the onTapGesture modifier to
detect the tap gesture. So, when a tap is recognized, we update the value of
selectedRestaurant like this:

List {
ForEach(restaurants) { restaurant in
BasicImageRow(restaurant: restaurant)
.onTapGesture {
self.selectedRestaurant = restaurant
}
}
}

The detail view, which functions as the bottom sheet, is intended to overlay on top of the
list view. We check if the detail view is enabled and initialize it accordingly. Here is the
code snippet:

NavigationStack {
.
.
.
}
.sheet(item: $selectedRestaurant) { restaurant in
RestaurantDetailView(restaurant: restaurant)
.ignoresSafeArea()
.presentationDetents([.medium, .large])
}

We attach the .sheet modifier to the NavigationStack . In the closure, we create an


instance of RestaurantDetailView and present it as a bottom sheet using the
.presentationDetents modifier. This means that when the user selects a restaurant, the
detail view will be displayed as a bottom sheet overlaying the current view.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 414


Figure 7. Bringing up the detail view

Since the presentation detents support both medium and large sizes, you can drag the
bottom sheet upward to expand it.

Hide the Drag Indicator


The presentationDetents modifier automatically generates a drag indicator near the top
edge of the bottom sheet. Since our detail view already has the handle bar, we can hide
the default indicator. To do so, attach the presentationDragIndicator modifier and set it to
.hidden :

RestaurantDetailView(restaurant: restaurant)
.ignoresSafeArea()
.presentationDetents([.medium, .large])
.presentationDragIndicator(.hidden)

Controlling its Size Using Fraction and Height

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 415


Other than the preset detents such as .medium , you can create a custom detent using
.height and .fraction . Here is another example:

.presentationDetents([.fraction(0.1), .height(200), .medium, .large])

Now the bottom supports 4 different sizes including:

around 10% of the screen height


a fixed height of 200 points
the standard Medium and Large sizes

Figure 8. A sample fixed-size bottom sheet

Storing the Selected Detent

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 416


Every time you dismiss the bottom sheet, the presentation detent is reset to its original
state. In other words, when you open the bottom sheet again, it will always start with the
.height(200) detent.

.presentationDetents([.height(200), .medium, .large])

However, if you want to remember the last selected detent and restore it when the
bottom sheet is reopened, you can declare a state variable to keep track of the currently
selected detent.

@State private var selectedDetent: PresentationDetent = .medium

For the presentationDetents modifier, you specify the binding of the state variable in the
selection parameter. This allows SwiftUI to store the currently selected detent in the
state variable.

.presentationDetents([.height(200), .medium, .large], selection: $selectedDetent)

SwiftUI then stores the last selected detent in the state variable and restore when the
bottom sheet is presented again.

Summary
In this chapter, I have demonstrated how to create a bottom sheet using the new
presentationDetents modifier. This highly anticipated view component has been long
awaited by many developers. With the customizable bottom sheet, you now have the
ability to easily display supplementary content that anchors to the bottom of the screen.

For further reference and to explore the complete implementation of the bottom sheet,
you can download the project from the following link:

Demo project
(https://www.appcoda.com/resources/swiftui5/SwiftUIBottomSheet.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 417


Chapter 19
Creating a Tinder-like UI with
Gestures and Animations
Wasn't it enjoyable to build an expandable bottom sheet? Now, let's take our newfound
knowledge about gestures and apply it to a real-world project. Perhaps you're familiar
with the Tinder app or have encountered a Tinder-like user interface in other
applications. The swiping motion has become a central element of Tinder's UI design and
has emerged as one of the most popular mobile UI patterns. Users can swipe right to like
a photo or swipe left to dislike it.

In this chapter, our goal is to create a simple app featuring a Tinder-like UI. The app will
present users with a deck of travel cards and enable them to utilize the swipe gesture to
either like or dislike a card.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 418


Figure 1. Building a tinder-like user interface

Let's dive into the project and explore the process of implementing this interactive and
engaging user interface. Please note that we are not going to build a fully functional app
but focus only on the Tinder-like UI.

Project Preparation
It would be great if you could use your own images. However, to save you time in
preparing trip images, I have created a starter project for you. You can download it from
https://www.appcoda.com/resources/swiftui5/SwiftUITinderTripStarter.zip. This
project already includes a set of photos for the travel cards.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 419


Figure 2. Preloaded with a set of travel photos

I have also prepared the test data for the demo app and created the Trip.swift file to
represent a trip:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 420


struct Trip {
var destination: String
var image: String
}

#if DEBUG
var trips = [ Trip(destination: "Yosemite, USA", image: "yosemite-usa"),
Trip(destination: "Venice, Italy", image: "venice-italy"),
Trip(destination: "Hong Kong", image: "hong-kong"),
Trip(destination: "Barcelona, Spain", image: "barcelona-spain"),
Trip(destination: "Braies, Italy", image: "braies-italy"),
Trip(destination: "Kanangra, Australia", image: "kanangra-australia"
),
Trip(destination: "Mount Currie, Canada", image: "mount-currie-canad
a"),
Trip(destination: "Ohrid, Macedonia", image: "ohrid-macedonia"),
Trip(destination: "Oia, Greece", image: "oia-greece"),
Trip(destination: "Palawan, Philippines", image: "palawan-philippine
s"),
Trip(destination: "Salerno, Italy", image: "salerno-italy"),
Trip(destination: "Tokyo, Japan", image: "tokyo-japan"),
Trip(destination: "West Vancouver, Canada", image: "west-vancouver-c
anada"),
Trip(destination: "Singapore", image: "garden-by-bay-singapore"),
Trip(destination: "Perhentian Islands, Malaysia", image: "perhentian
-islands-malaysia")
]
#endif

In case if you prefer to use your own images and data, simply replace the images in the
asset catalog and update Trip.swift .

Building the Card Views and Menu Bars


Before implementing the swipe feature, let's start by creating the main UI. I will break
the main screen into three parts:

1. The top menu bar


2. The card view

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 421


3. The bottom menu bar

Figure 3. The main screen

Card View
First, let's create a card view. If you want to challenge yourself, I highly recommend
stopping here and implementing it without following this section. Otherwise, keep
reading.

To better organize the code, we will implement the card view in a separate file. In the
project navigator, create a new file using the SwiftUI View template and name it
CardView.swift .

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 422


The CardView is designed to display different photos and titles. So, declare two variables
to store the data:

let image: String


let title: String

The main screen is going to display a deck of card views. Later, we will use ForEach to
loop through an array of card views and present them. If you still remember the usage of
ForEach , SwiftUI needs to know how to uniquely identify each item in the array.
Therefore, we will make CardView conform to the Identifiable protocol and introduce
an id variable like this:

struct CardView: View, Identifiable {


let id = UUID()
let image: String
let title: String

.
.
.
}

In case you forgot what the Identifiable protocol is, please refer to chapter 10. Now let's
continue implementing the card view and update the body variable like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 423


var body: some View {
Image(image)
.resizable()
.scaledToFill()
.frame(minWidth: 0, maxWidth: .infinity)
.cornerRadius(10)
.padding(.horizontal, 15)
.overlay(alignment: .bottom) {
VStack {

Text(title)
.font(.system(.headline, design: .rounded))
.fontWeight(.bold)
.padding(.horizontal, 30)
.padding(.vertical, 10)
.background(.white)
.cornerRadius(5)
}
.padding([.bottom], 20)
}
}

The card view is composed of an image and a text component, which is overlayed on top
of the image. We set the image to the scaleToFill mode and round the corners using the
cornerRadius modifier. The text component is used to display the destination of the trip.

We have discussed a similar implementation of the card view in chapter 5. If you don't
fully understand the code, please refer to that chapter.

You can't preview the card view yet because you have to provide the values of both image

and title in the preview code. Therefore, update the #Preview section like this:

#Preview {
CardView(image: "yosemite-usa", title: "Yosemite, USA")
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 424


I have used one of the images from the asset catalog for preview purposes. Feel free to
modify the image and title to suit your needs. In the preview canvas, you should now see
the card view similar to Figure 4.

Figure 4. Previewing the card view

Menu Bars and Main UI


With the card view ready, we can move on to implement the main UI. The main UI
consists of the card and two menu bars. To achieve this, we will create separate struct s
for each menu bar.

Let's begin by opening ContentView.swift and implementing the top bar menu. Create a
new struct for the top bar menu like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 425


struct TopBarMenu: View {
var body: some View {
HStack {
Image(systemName: "line.horizontal.3")
.font(.system(size: 30))
Spacer()
Image(systemName: "mappin.and.ellipse")
.font(.system(size: 35))
Spacer()
Image(systemName: "heart.circle.fill")
.font(.system(size: 30))
}
.padding()
}
}

The three icons in the top bar menu are arranged using a horizontal stack with equal
spacing. We will follow a similar implementation for the bottom bar menu. Insert the
following code in ContentView.swift to create the menu bar:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 426


struct BottomBarMenu: View {
var body: some View {
HStack {
Image(systemName: "xmark")
.font(.system(size: 30))
.foregroundColor(.black)

Button {
// Book the trip
} label: {
Text("BOOK IT NOW")
.font(.system(.subheadline, design: .rounded))
.bold()
.foregroundColor(.white)
.padding(.horizontal, 35)
.padding(.vertical, 15)
.background(.black)
.cornerRadius(10)
}
.padding(.horizontal, 20)

Image(systemName: "heart")
.font(.system(size: 30))
.foregroundStyle(.black)
}

}
}

We won't implement the "Book Trip" feature, so the action block is left blank. The rest of
the code should be self-explanatory if you understand how stacks and images work.

Before building the main UI, let me show you a trick to preview these two menu bars. It's
not mandatory to include these bars in the ContentView in order to preview their look and
feel.

Now insert the following code snippets for previewing the top and bottom bar menus:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 427


#Preview("TopBarMenu") {
TopBarMenu()
}

#Preview("BottomBarMenu") {
BottomBarMenu()
}

For the TopBarMenu and BottomBarMenu views, we added two more #Preview sections.
Additionally, we gave each view a distinct name. If you take a look at the preview canvas,
you will see three previews: ContentView, TopBarMenu, and BottomBarMenu. Simply
click on each view to preview its layout. Figure 5 gives you a better idea of what the
preview looks like.

Figure 5. Previewing the menu bars

Okay, let's continue to lay out the main UI. Update the ContentView like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 428


struct ContentView: View {
var body: some View {
VStack {
TopBarMenu()

CardView(image: "yosemite-usa", title: "Yosemite, USA")

Spacer(minLength: 20)

BottomBarMenu()
}
}
}

In the code, we simply arrange the UI components we have built using a VStack . Your
preview should now show you the main screen layout.

Figure 6. Previewing the main UI

Implementing the Card Deck

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 429


With all the preparations done, we finally come to the implementation of the Tinder-like
UI. For those who haven't used the Tinder app before, let me first explain how a Tinder-
like UI works.

You can imagine a Tinder-like UI as a deck of piled photo cards. For our demo app, each
photo represents a destination of a trip. Swiping the topmost card (i.e., the first trip)
slightly to the left or right reveals the next card (i.e., the next trip) underneath. If the user
releases the card, the app brings it back to its original position. However, if the user
swipes hard enough, they can throw away the card, and the app will bring the next card
forward to become the new topmost card.

Figure 7. How the Tinder-like UI works

The main screen we have implemented only contains a single card view. So, how can we
implement the pile of card views?

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 430


The most straightforward way is to overlay each of the card views on top of each other
using a ZStack . Let's update the ContentView struct like this:

struct ContentView: View {

var cardViews: [CardView] = {

var views = [CardView]()

for trip in trips {


views.append(CardView(image: trip.image, title: trip.destination))
}

return views
}()

var body: some View {


VStack {
TopBarMenu()

ZStack {
ForEach(cardViews) { cardView in
cardView
}
}

Spacer(minLength: 20)

BottomBarMenu()
}
}
}

In the provided code, we initialize an array called cardViews that contains all the trips,
which are defined in the Trip.swift file. Within the body variable, we iterate through
each card view in the array and overlay them on top of each other using a ZStack .

In the preview canvas, you should see the same UI as before, but with a different image
displayed.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 431


Figure 8. Building the deck of card views

Why did it display another image? In the current implementation, the displayed image is
determined by the order of the trips in the trips array. Since the last trip in the array is
placed at the top of the ForEach loop, it becomes the topmost card in the deck.

Our card deck has two issues:

1. The first trip of the trips array is supposed to be the topmost card, however, it's
now the lowermost card.
2. We rendered 15 card views for 15 trips. What if we have 10,000 trips or even more in
the future? Should we create one card view for each of the trips? Is there a resource
efficient way to implement the card deck?

Let's first fix the card order issue. SwiftUI provides the zIndex modifier for you to
indicate the order of the views in a ZStack. A view with a higher value of zIndex is placed
on top of those with a lower value. So, the topmost card should have the largest value of
zIndex .

With this in mind, we create the following new function in ContentView :

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 432


private func isTopCard(cardView: CardView) -> Bool {

guard let index = cardViews.firstIndex(where: { $0.id == cardView.id }) else {


return false
}

return index == 0
}

This function takes a card view as input, determines its index within the array, and
checks if it is the topmost card.

With the helper function in place, we can now update the code block for the ZStack to
assign a higher zIndex value to the topmost card. This ensures that the topmost card is
visually displayed above the other cards. Here's the updated code block:

ZStack {
ForEach(cardViews) { cardView in
cardView
.zIndex(self.isTopCard(cardView: cardView) ? 1 : 0)
}
}

We added the zIndex modifier for each of the card views. The topmost card is assigned a
higher value of zIndex . In the preview canvas, you should now see the photo of the first
trip (i.e. Yosemite, USA).

For the second issue, we need to find a solution that allows the card deck to handle a
large number of card views efficiently.

Upon closer examination, it becomes clear that we don't actually need to instantiate
individual card views for each trip photo. Instead, we can create just two card views and
overlay them with each other.

When the topmost card view is swiped away, the card view underneath becomes the new
topmost card. Simultaneously, we can instantiate a new card view with a different photo
and position it behind the current topmost card. By adopting this approach, the app only

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 433


maintains two card views at all times, regardless of the number of photos to display in the
card deck. From the user's perspective, the UI still appears as a pile of cards.

Figure 9. How we use two card views to create a deck

Now that you understand how we are going to construct the card deck, let’s move onto
the implementation.

First, update the cardViews array, we no longer need to initialize all the trips but only the
first two. Later, when the first trip (i.e. the first card) is thrown away, we will add another
one to it.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 434


var cardViews: [CardView] = {

var views = [CardView]()

for index in 0..<2 {


views.append(CardView(image: trips[index].image, title: trips[index].desti
nation))
}

return views
}()

After the code change, the UI should look exactly the same. But in the underlying
implementation, the app now only shows two card views in the deck.

Implementing the Swiping Motion


Before we proceed to dynamically create a new card view, we need to implement the
swipe feature. In case you need a refresher on working with gestures, I recommend
revisiting chapters 17 and 18. We will be reusing some of the code discussed in those
chapters.

To begin, let's define the DragState enum in the ContentView struct. This enum will
represent the various drag states:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 435


enum DragState {
case inactive
case pressing
case dragging(translation: CGSize)

var translation: CGSize {


switch self {
case .inactive, .pressing:
return .zero
case .dragging(let translation):
return translation
}
}

var isDragging: Bool {


switch self {
case .dragging:
return true
case .pressing, .inactive:
return false
}
}

var isPressing: Bool {


switch self {
case .pressing, .dragging:
return true
case .inactive:
return false
}
}

Once again, if you don't understand what an enum is used for, stop here and review the
chapters on gestures. Next, let's define a @GestureState variable to store the drag state,
which is set to inactive by default:

@GestureState private var dragState = DragState.inactive

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 436


Now, update the body part like this:

var body: some View {


VStack {
TopBarMenu()

ZStack {
ForEach(cardViews) { cardView in
cardView
.zIndex(self.isTopCard(cardView: cardView) ? 1 : 0)
.offset(x: self.dragState.translation.width, y: self.dragStat
e.translation.height)
.scaleEffect(self.dragState.isDragging ? 0.95 : 1.0)
.rotationEffect(Angle(degrees: Double( self.dragState.translat
ion.width / 10)))
.animation(.interpolatingSpring(stiffness: 180, damping: 100),
value: self.dragState.translation)
.gesture(LongPressGesture(minimumDuration: 0.01)
.sequenced(before: DragGesture())
.updating(self.$dragState, body: { (value, state, transact
ion) in
switch value {
case .first(true):
state = .pressing
case .second(true, let drag):
state = .dragging(translation: drag?.translation ?
? .zero)
default:
break
}

})

)
}
}

Spacer(minLength: 20)

BottomBarMenu()
.opacity(dragState.isDragging ? 0.0 : 1.0)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 437


.animation(.default, value: dragState.isDragging)
}
}

Basically, we apply the knowledge we gained from the gesture chapter to implement the
dragging functionality. The .gesture modifier is used with two gesture recognizers: long
press and drag. When the drag gesture is recognized, we update the dragState variable
and store the drag translation.

To achieve the dragging effect, we combine the offset , scaleEffect , rotationEffect ,


and animation modifiers. The offset modifier is responsible for updating the position
of the card view during dragging. When the card view is in the dragging state, we slightly
scale it down using the scaleEffect modifier and rotate it at a specific angle using the
rotationEffect modifier. The animation used is set to interpolatingSpring , but you can
experiment with other animations as well.

We also made some modifications to the BottomBarMenu . While the user is dragging the
card view, we hide the bottom bar by applying the .opacity modifier and setting its
value to zero when the dragging state is active.

After making these changes, run the project in a simulator to test it out. You should be
able to drag the card and move it around. When you release the card, it should return to
its original position.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 438


Figure 10. Dragging the card view

Do you notice a problem here? While the drag functionality is working, it's actually
dragging the entire card deck instead of just the topmost card. Additionally, the scaling
effect is being applied to the entire deck rather than just the topmost card.

To address these issues, we need to make some changes to the code of the offset ,
scaleEffect , and rotationEffect modifiers. These modifications will ensure that
dragging only affects the topmost card view and that the scaling effect is applied
exclusively to that card.

Let's proceed with the necessary adjustments to the code.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 439


ZStack {
ForEach(cardViews) { cardView in
cardView
.zIndex(self.isTopCard(cardView: cardView) ? 1 : 0)
.offset(x: self.isTopCard(cardView: cardView) ? self.dragState.transla
tion.width : 0, y: self.isTopCard(cardView: cardView) ? self.dragState.translation
.height : 0)
.scaleEffect(self.dragState.isDragging && self.isTopCard(cardView: car
dView) ? 0.95 : 1.0)
.rotationEffect(Angle(degrees: self.isTopCard(cardView: cardView) ? Do
uble( self.dragState.translation.width / 10) : 0))
.animation(.interpolatingSpring(stiffness: 180, damping: 100), value:
self.dragState.translation)
.gesture(LongPressGesture(minimumDuration: 0.01)
.sequenced(before: DragGesture())
.updating(self.$dragState, body: { (value, state, transaction) in
switch value {
case .first(true):
state = .pressing
case .second(true, let drag):
state = .dragging(translation: drag?.translation ?? .zero)
default:
break
}

})

)
}
}

Just focus on the changes to the offset , scaleEffect , and rotationEffect modifiers.
The rest of the code remains unchanged. We have introduced an additional check within
these modifiers to ensure that the effects are only applied to the topmost card view.

If you run the app again, you should see that the card underneath is visible, and you can
now drag the topmost card without affecting the other cards in the deck.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 440


Figure 11. The dragging effect only applies to the topmost card

Displaying the Heart and xMark icons


Cool! The dragging functionality is now working as expected. However, we still have
more to do. We need to enable the user to swipe right or left to throw away the topmost
card. Additionally, depending on the direction of the swipe, we should display either a
heart or xmark icon on the card.

Let's continue implementing these features. To begin, let's declare a drag threshold in
ContentView :

private let dragThreshold: CGFloat = 80.0

Once the translation of a drag exceeds the threshold, we want to overlay an icon (either a
heart or xmark) on the card. Additionally, when the user releases the card, we need to
remove it from the deck, create a new one, and place the new card at the back of the deck.

To overlay the icon, we can use the .overlay modifier on the cardViews . Insert the
following code below the .zIndex modifier:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 441


.overlay {
ZStack {
Image(systemName: "x.circle")
.foregroundColor(.white)
.font(.system(size: 100))
.opacity(self.dragState.translation.width < -self.dragThreshold && self
.isTopCard(cardView: cardView) ? 1.0 : 0)

Image(systemName: "heart.circle")
.foregroundColor(.white)
.font(.system(size: 100))
.opacity(self.dragState.translation.width > self.dragThreshold && self
.isTopCard(cardView: cardView) ? 1.0 : 0.0)
}
}

By default, both images are hidden by setting their opacity to zero. The translation's
width has a positive value if the drag is to the right, otherwise, it's a negative value.
Depending on the drag direction, the app will reveal either the heart or xmark icon when
the drag's translation exceeds the threshold.

You can run the project to have a quick test. When you exceed the threshold during the
drag, the heart or xmark icon will appear on the card.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 442


Figure 12. The heart icon appears

Removing/Inserting the Cards


Now, when you release the card, it will return to its original position. But how do we
remove the topmost card and add a new card at the same time?

First, let's mark the cardViews array with @State so that we can update its value and
refresh the UI:

@State var cardViews: [CardView] = {

var views = [CardView]()

for index in 0..<2 {


views.append(CardView(image: trips[index].image, title: trips[index].desti
nation))
}

return views
}()

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 443


Next, let's declare another state variable to keep track of the last index of the trip. When
the card deck is first initialized, we display the first two trips stored in the trips array,
so the last index is set to 1 .

@State private var lastIndex = 1

Okay, here comes to the core function for removing and inserting the card views. Define a
new function called moveCard :

private func moveCard() {


cardViews.removeFirst()

self.lastIndex += 1
let trip = trips[lastIndex % trips.count]

let newCardView = CardView(image: trip.image, title: trip.destination)

cardViews.append(newCardView)
}

This function first removes the topmost card from the cardViews array using the
removeFirst() method. Then, it instantiates a new card view with the subsequent trip's
image and adds it to the cardViews array using the append() method. Since cardViews is
defined as a state property, SwiftUI will automatically re-render the card views once the
array's value is changed. This is how we remove the topmost card and insert a new one to
the deck.

For this demo, the intention is to have the card deck continuously display trips. After the
last photo of the trips array is displayed, the app will cycle back to the first element
using the modulus operator % in the code above. This ensures that the card deck keeps
showing trips in a loop, providing a seamless user experience.

Next, update the .gesture modifier and insert the .onEnded function:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 444


.gesture(LongPressGesture(minimumDuration: 0.01)
.sequenced(before: DragGesture())
.updating(self.$dragState, body: { (value, state, transaction) in
.
.
.
})
.onEnded({ (value) in

guard case .second(true, let drag?) = value else {


return
}

if drag.translation.width < -self.dragThreshold ||


drag.translation.width > self.dragThreshold {

self.moveCard()
}
})
)

When the drag gesture ends, the code checks if the drag's translation exceeds the
threshold, and if so, it calls the moveCard() function accordingly.

To test this functionality, run the project in the preview canvas. Drag the image to the
right or left until the heart or xmark icon appears. Then release the drag, and you will
observe that the topmost card is replaced by the next card in the deck.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 445


Figure 13. Removing the topmost card

Fine Tuning the Animations


The app almost works but the animation falls short of expectations. Rather than having
the card view disappear abruptly, we want it to gradually fall out of the screen. To achieve
this effect, we will modify the animation applied to the card view.

To implement the desired animation, we can utilize the transition modifier and apply
an appropriate transition to the card views. By specifying an asymmetric transition, we
can control the animation differently during insertion and removal of the views.

Add the extension, AnyTransition to the bottom of ContentView.swift and define two
transition effects:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 446


extension AnyTransition {
static var trailingBottom: AnyTransition {
AnyTransition.asymmetric(
insertion: .identity,
removal: AnyTransition.move(edge: .trailing).combined(with: .move(edge
: .bottom))
)

static var leadingBottom: AnyTransition {


AnyTransition.asymmetric(
insertion: .identity,
removal: AnyTransition.move(edge: .leading).combined(with: .move(edge:
.bottom))
)
}
}

The reason for using asymmetric transitions is to apply animation specifically when the
card view is removed from the deck. When a new card view is inserted, we want it to
appear instantly without any animation.

The trailingBottom transition is used when the card view is thrown away to the right of
the screen, while we apply the leadingBottom transition when the card view is thrown
away to the left.

Next, declare a state property that holds the transition type. It's set to trailingBottom by
default.

@State private var removalTransition = AnyTransition.trailingBottom

Now attach the .transition modifier to the card view. You can place it after the
.animation modifier:

.transition(self.removalTransition)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 447


Finally, update the code of the .gesture modifier with the onChanged function like this:

.gesture(LongPressGesture(minimumDuration: 0.01)
.sequenced(before: DragGesture())
.updating(self.$dragState, body: { (value, state, transaction) in
switch value {
case .first(true):
state = .pressing
case .second(true, let drag):
state = .dragging(translation: drag?.translation ?? .zero)
default:
break
}

})
.onChanged({ (value) in
guard case .second(true, let drag?) = value else {
return
}

if drag.translation.width < -self.dragThreshold {


self.removalTransition = .leadingBottom
}

if drag.translation.width > self.dragThreshold {


self.removalTransition = .trailingBottom
}

})
.onEnded({ (value) in

guard case .second(true, let drag?) = value else {


return
}

if drag.translation.width < -self.dragThreshold ||


drag.translation.width > self.dragThreshold {

self.moveCard()
}
})

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 448


)

The code sets the removalTransition . The transition type is updated according to the
swipe direction. Now you're ready to run the app again. You should now see an improved
animation when the card is thrown away.

Summary
With SwiftUI, you can easily build some cool animations and mobile UI patterns. This
Tinder-like UI is an examples.

I hope you fully understand what I covered in this chapter, so you can adapt the code to
fit your own project. It’s quite a huge chapter. I wanted to document my thought process
instead of just presenting you with the final solution.

For reference, you can download the complete tinder project here:

Demo project
(https://www.appcoda.com/resources/swiftui5/SwiftUITinderTrip.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 449


Chapter 20
Creating an Apple Wallet like
Animation and View Transition
Do you use Apple's Wallet app? In the previous chapter, we built a simple app with a
Tinder-like UI. In this chapter, we will create an animated UI similar to the one you see
in the Wallet app. When you tap and hold a credit card in the Wallet app, you can use the
drag gesture to rearrange the cards. If you haven't used the app, open Wallet and take a
quick look. Alternatively, you can visit this URL (https://link.appcoda.com/swiftui-
wallet) to check out the animation we're going to build.

Figure 1. Building a Wallet-like animations and view transitions

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 450


In the Wallet app, tapping one of the cards will bring up its transaction history. We will
also create a similar animation to demonstrate view transitions and horizontal scroll
view. This will help you better understand how to implement these features in your own
SwiftUI projects.

Project Preparation
To keep you focused on learning animations and view transitions, begin with this starter
project (https://www.appcoda.com/resources/swiftui5/SwiftUIWalletStarter.zip). The
starter project already bundles the required credit card images and comes with a built-in
transaction history view. If you want to use your own images, please replace them in the
asset catalog.

Figure 2. The starter project bundles the credit card images

In the project navigator, you should find a number of .swift files:

Transaction.swift - the Transaction struct represents a transaction in the wallet

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 451


app. Each transaction has an unique ID, merchant, amount, date, and icon. In
addition to the Transaction struct, we also declare an array of test transactions for
demo purposes.
Card.swift - this file contains the struct of Card .A Card represents the data of a
credit card including the card number, type, expiry date, image, and the customer's
name. Additionally, there is an array of test credit cards in the file. One point to note
is that the card image doesn't include any personal information, only the card brand
(e.g. Visa). Later, we will create a view for a credit card.
TransactionHistoryView.swift - this is the transaction history view displayed in
figure 1. The starter project comes with an implementation of the transaction history
view. We display the transactions in a horizontal scroll view. You've worked with
vertical scroll views before. The trick of creating a horizontal view is to pass a value
of .horizontal during the initialization of a scroll view. Take a look at figure 3 or
simply look at the Swift file for details.
ContentView.swift - this is the default SwiftUI view generated by Xcode.

Figure 3. Using .horizontal to create a horizontal scroll view

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 452


Building a Card View
As mentioned in the previous section, the card images in the asset catalog do not include
any personal information or card numbers. They only display the card logo. We will
create a card view to layout the personal information and card number, as shown in
Figure 4.

Figure 4. A sample card

To create the card view, right click the View group in the project navigator and create a
new file. Choose the SwiftUI View template and name the file CardView.swift . Next,
update the code like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 453


struct CardView: View {
var card: Card

var body: some View {


Image(card.image)
.resizable()
.scaledToFit()
.overlay(

VStack(alignment: .leading) {
Text(card.number)
.bold()
HStack {
Text(card.name)
.bold()
Text("Valid Thru")
.font(.footnote)
Text(card.expiryDate)
.font(.footnote)
}
}
.foregroundColor(.white)
.padding(.leading, 25)
.padding(.bottom, 20)

, alignment: .bottomLeading)
.shadow(color: .gray, radius: 1.0, x: 0.0, y: 1.0)

}
}

We declare a card property to take in the card data. To display the personal information
and card number on the card image, we use the overlay modifier and layout the text
components with a vertical stack view and a horizontal stack view.

To preview the cards, update the #Preview code block like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 454


#Preview(testCards[0].type.rawValue) {
CardView(card: testCards[0])
}

The testCards variable was defined in Card.swift . We just pick one of them for preview.
Xcode will layout the card like that shown in figure 5.

Figure 5. Previewing the card views

Building the Wallet View and Card Deck


Now that we have implemented the card view, let's start to build the wallet view. If you
forgot what the wallet view looks like, take a look at figure 6. We will first layout the card
deck before working on the gestures and animations.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 455


Figure 6. The wallet view

In the project navigator, you should see the ContentView.swift file. Delete it and then
right click the View folder to create a new one. In the dialog, choose SwiftUI View as the
template and name the file WalletView.swift .

If you preview the WalletView or run the app on simulator, Xcode should display an error
because the ContentView is set to the initial view and it was deleted. To fix the error, open
SwiftUIWalletApp.swift and change the following line of code in WindowGroup from:

ContentView()

To:

WalletView()

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 456


Switch back to WalletView.swift . The compilation error will be fixed once you make the
change. Now let's continue to layout the wallet view. First, we'll start with the title bar. In
the WalletView.swift file, insert a new struct for the bar:

struct TopNavBar: View {

var body: some View {


HStack {
Text("Wallet")
.font(.system(.largeTitle, design: .rounded))
.fontWeight(.heavy)

Spacer()

Image(systemName: "plus.circle.fill")
.font(.system(.title))
}
.padding(.horizontal)
.padding(.top, 20)
}
}

The code is very straightforward. We laid out the title and the plus image using a
horizontal stack.

Next, we create the card deck. First, declare a property in the WalletView struct for the
array of credit cards:

var cards: [Card] = testCards

For demo purpose, we simply set the default value to testCards which was defined in the
Card.swift file. To lay out the wallet view, we use both a VStack and ZStack . Update the
body variable like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 457


var body: some View {
VStack {

TopNavBar()
.padding(.bottom)

Spacer()

ZStack {
ForEach(cards) { card in
CardView(card: card)
.padding(.horizontal, 35)
}
}

Spacer()
}
}

If you run the app on simulator or preview the UI directly, you should only see the last
card in the card deck like that shown in figure 7.

Figure 7. Trying to display the card deck

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 458


There are two issues with the current implementation:

1. The cards are now overlapped with each other - we need to figure out a way to
spread out the deck of cards.
2. The Discover card is supposed to be the last card - In a ZStack view, the items stack
on top of each other based on the order in which they are added. The first item being
put into the ZStack becomes the lowermost layer, while the last item is the
uppermost layer. In the testCards array in Card.swift , the first card is the Visa
card, while the last card is the Discover card.

Okay, so how are we going to fix these issues? For the first issue, we can make use of the
offset modifier to spread out the deck of cards. For the second issue, obviously we can
alter the zIndex for each card in the CardView to change the order of the cards. Figure 8
illustrates how the solution works.

Figure 8. Understanding zIndex and offset

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 459


Let's first talk about the z-index. Each card's z-index is the negative value of its index in
the cards array. The last item with the largest array index will have the smallest z-index.
For this implementation, we will create an individual function to handle the computation
of z-index. In the WalletView , insert the following code:

private func zIndex(for card: Card) -> Double {


guard let cardIndex = index(for: card) else {
return 0.0
}

return -Double(cardIndex)
}

private func index(for card: Card) -> Int? {


guard let index = cards.firstIndex(where: { $0.id == card.id }) else {
return nil
}

return index
}

Both functions work together to figure out the correct z-index of a given card. To
compute a correct z-index, the first thing we need is the index of the card in the cards

array. The index(for:) function is designed to get the array index of the given card. Once
we have the index, we can turn it into a negative value. This is what the zIndex(for:)

function does.

Now, you can attach the zIndex modifier to the CardView like this:

CardView(card: card)
.padding(.horizontal, 35)
.zIndex(self.zIndex(for: card))

Once you make the change, the Visa card should move to the top of the deck.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 460


Next, let's fix the first issue to spread out the cards. Each of the cards should be offset by
a certain vertical distance. That distance is computed by using the card's index. Say, we
set the default vertical offset to 50 points. The last card (with the index #4) will be offset
by 200 points (50*4).

Now that you should understand how we are going to spread the cards, let's write the
code. Declare the default vertical offset in WalletView :

private static let cardOffset: CGFloat = 50.0

Next, create a new function called offset(for:) that is used to compute the vertical offset
of the given card:

private func offset(for card: Card) -> CGSize {

guard let cardIndex = index(for: card) else {


return CGSize()
}

return CGSize(width: 0, height: -50 * CGFloat(cardIndex))


}

Finally, attach the offset modifier to the CardView :

CardView(card: card)
.padding(.horizontal, 35)
.offset(self.offset(for: card))
.zIndex(self.zIndex(for: card))

That's how we spread the card using the offset modifier. If everything is correct, you
should see a preview like that shown in figure 9.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 461


Figure 9. Spreading the cards

Adding a Slide-in Animation


Now that we have completed the layout of the wallet view, it's time to add some
animations. The first animation I want to introduce is a slide-in animation. When the app
is first launched, each of the cards will slide in from the far left of the screen. You may
think that this animation is unnecessary, but I want to take this opportunity to show you
how to create an animation and view transition when the app is launched.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 462


Figure 10. The slide-in animation

First, we need a way to trigger the transition animation. Let's declare a state variable at
the beginning of CardView :

@State private var isCardPresented = false

This variable indicates whether the cards should be presented on screen. By default, it's
set to false . Later, we will set this value to true to trigger the view transition.

Each of the cards is a view. To implement an animation like that displayed in figure 10,
we need to attach both the transition and animation modifiers to the CardView like
this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 463


CardView(card: card)
.offset(self.offset(for: card))
.padding(.horizontal, 35)
.zIndex(self.zIndex(for: card))
.transition(AnyTransition.slide.combined(with: .move(edge: .leading)).combined
(with: .opacity))
.animation(self.transitionAnimation(for: card), value: isCardPresented)

For the transition, we combine the default slide transition with the move transition. As
mentioned before, the transition will not be animated without the animation modifier.
This is why we also attach the animation modifier. Since each card has its own
animation, we create a function called transitionAnimation(for:) to compute the
animation. Insert the following code to create the function:

private func transitionAnimation(for card: Card) -> Animation {


var delay = 0.0

if let index = index(for: card) {


delay = Double(cards.count - index) * 0.1
}

return Animation.spring(response: 0.1, dampingFraction: 0.8, blendDuration: 0.


02).delay(delay)
}

In fact, all the cards have a similar animation, which is a spring animation. The difference
is in the delay. The last card of the deck will appear first, thus the value of the delay
should be the smallest. The formula below is how we compute the delay for each of the
cards. The smaller the index, the longer the delay.

delay = Double(cards.count - index) * 0.1

So, how can we trigger the view transition of the card views when the app is launched?
The trick is to attach an id modifier to each card view:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 464


CardView(card: card)
.
.
.
.id(isCardPresented)
.
.animation(self.transitionAnimation(for: card), value: isCardPresented)

The value is set to isCardPresented . Now attach the onAppear modifier to the ZStack :

.onAppear {
isCardPresented.toggle()
}

When the ZStack appears, we change the value of isCardPresented from false to true .
When the id value changes, SwiftUI considers this to be a new view. Thus, this triggers
the view transition animation of the cards.

After applying the changes, hit the Play button to test the app in a simulator. The app
should render the animation when it launches.

Handling the Tap Gesture and Displaying the


Transaction History
When a user taps a card, the app moves the selected card upward and brings up the
transaction history. For those non-selected cards, they are moved off the screen.

To implement this feature, we need two more state variables. Declare these variables in
WalletView :

@State var isCardPressed = false


@State var selectedCard: Card?

The isCardPressed variable indicates if a card is selected, while the selectedCard variable
stores the card selected by the user.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 465


.gesture(
TapGesture()
.onEnded({ _ in
withAnimation(.easeOut(duration: 0.15).delay(0.1)) {
self.isCardPressed.toggle()
self.selectedCard = self.isCardPressed ? card : nil
}
})
)

To handle the tap gesture, we attach the above gesture modifier to the CardView (just
below .animation(self.transitionAnimation(for: card)) ) and use the built-in TapGesture

to capture the tap event. In the code block, we simply toggle the state of isCardPressed

and set the current card to the selectedCard variable.

To move the selected card (and those underneath) upward and the rest of the cards move
off the screen, update the offset(for:) function like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 466


private func offset(for card: Card) -> CGSize {

guard let cardIndex = index(for: card) else {


return CGSize()
}

if isCardPressed {
guard let selectedCard = self.selectedCard,
let selectedCardIndex = index(for: selectedCard) else {
return .zero
}

if cardIndex >= selectedCardIndex {


return .zero
}

let offset = CGSize(width: 0, height: 1400)

return offset
}

return CGSize(width: 0, height: -50 * CGFloat(cardIndex))


}

We added an if clause to check if a card is selected. If the given card is the one selected
by the user, we set the offset to .zero . For the cards right below the selected card, we
also move them upward, hence the offset of .zero . For the rest of the cards, we move
them off the screen by setting the vertical offset to 1400 points.

Now we are ready to write the code for bringing up the transaction history view. As
mentioned at the very beginning, the starter project comes with this transaction history
view. Therefore, you do not need to build it yourself.

We can use the isCardPressed state variable to determine if the transaction history view
should be shown or not. Insert the following code right before Spacer() :

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 467


if isCardPressed {
TransactionHistoryView(transactions: testTransactions)
.padding(.top, 10)
.transition(.move(edge: .bottom))
}

In the code above, we set the transition to .move to bring the view up from the bottom of
the screen. Feel free to change it to suit your preference.

Figure 11. Displaying the transaction history

Rearranging the Cards Using the Drag Gesture


Now comes the core part of this chapter. Let's see how to rearrange the card deck with
the drag gesture. First, let me describe how this feature works in detail:

1. To initiate the dragging action, the user must tap and hold the card. A simple tap will
only bring up the transaction history view.
2. Once the user successfully holds a card, the app will move it a slighly upward. This
provides feedback to the user, indicating that the card is ready to be dragged.
3. As the user drags the card, they should be able to move it across the deck,

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 468


rearranging the order of the cards.
4. After the user releases the card at a certain position, the app will update the position
of all the cards in the card deck based on the new order.

Figure 12. Moving a card across the deck using the drag gesture

Handling the Long Press and Drag Gestures


Now that you understand what we are going to do, let's move onto the implementation. If
you forgot how SwiftUI handles gestures, please go back and read chapter 17. Most of the
techniques that we will use have been discussed in that chapter.

To begin, insert the following code in WalletView.swift to create the DragState enum so
that we can easily keep track of the drag state:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 469


enum DragState {
case inactive
case pressing(index: Int? = nil)
case dragging(index: Int? = nil, translation: CGSize)

var index: Int? {


switch self {
case .pressing(let index), .dragging(let index, _):
return index
case .inactive:
return nil
}
}
var translation: CGSize {
switch self {
case .inactive, .pressing:
return .zero
case .dragging(_, let translation):
return translation
}
}

var isPressing: Bool {


switch self {
case .pressing, .dragging:
return true
case .inactive:
return false
}
}

var isDragging: Bool {


switch self {
case .dragging:
return true
case .inactive, .pressing:
return false
}
}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 470


Next, declare a state variable in WalletView to keep track of the drag state:

@GestureState private var dragState = DragState.inactive

If you've read the chapter about SwiftUI gestures, you should already know how to detect
a long press and drag gesture. However, this time it will be a bit different. We need to
handle the tap gesture, the drag, and the long press gesture at the same time.
Additionally, the app should ignore the tap gesture if the long press gesture is detected.

Now update the gesture modifier of the CardView like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 471


.gesture(
TapGesture()
.onEnded({ _ in
withAnimation(.easeOut(duration: 0.15).delay(0.1)) {
self.isCardPressed.toggle()
self.selectedCard = self.isCardPressed ? card : nil
}
})
.exclusively(before: LongPressGesture(minimumDuration: 0.05)
.sequenced(before: DragGesture())
.updating(self.$dragState, body: { (value, state, transaction) in
switch value {
case .first(true):
state = .pressing(index: self.index(for: card))
case .second(true, let drag):
state = .dragging(index: self.index(for: card), translation: drag?
.translation ?? .zero)
default:
break
}

})
.onEnded({ (value) in

guard case .second(true, let drag?) = value else {


return
}

// Rearrange the cards


})

)
)

SwiftUI allows you to combine multiple gestures exclusively. In the code above, we
specify that either the tap gesture or the long press gesture should be recognized, but not
both at the same time. This ensures that SwiftUI ignores the long press gesture once the
tap gesture is detected.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 472


The code for the tap gesture remains the same as before. For the drag gesture, we
sequence it after the long press gesture. In the updating function, we update the state of
the drag, the translation, and the index of the card using the dragState variable we
defined earlier. I won't go into detail about the code as it was covered in chapter 17.

Before you can drag the card, you have to update the offset(for:) function like this:

private func offset(for card: Card) -> CGSize {

guard let cardIndex = index(for: card) else {


return CGSize()
}

if isCardPressed {
guard let selectedCard = self.selectedCard,
let selectedCardIndex = index(for: selectedCard) else {
return .zero
}

if cardIndex >= selectedCardIndex {


return .zero
}

let offset = CGSize(width: 0, height: 1400)

return offset
}

// Handle dragging
var pressedOffset = CGSize.zero
var dragOffsetY: CGFloat = 0.0

if let draggingIndex = dragState.index,


cardIndex == draggingIndex {
pressedOffset.height = dragState.isPressing ? -20 : 0

switch dragState.translation.width {
case let width where width < -10: pressedOffset.width = -20
case let width where width > 10: pressedOffset.width = 20
default: break
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 473


dragOffsetY = dragState.translation.height
}

return CGSize(width: 0 + pressedOffset.width, height: -50 * CGFloat(cardIndex)


+ pressedOffset.height + dragOffsetY)
}

We added a block of code to handle the dragging. Please bear in the mind that only the
selected card is draggable. Therefore, we need to check if the given card is the one being
dragged by the user before making the offset change.

Earlier, we stored the card's index in the dragState variable. So, we can easily compare
the given card index with the one stored in dragState to figure out which card to drag.

For the dragging card, we add an additional offset both horizontally and vertically.

Run the app to test it out, tap & hold a card and then drag it around.

Figure 13. Dragging a card

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 474


Currently, you should be able to drag the card, however, the card's z-index doesn't change
accordingly. For example, if you drag the Visa card, it always stays on the top of the deck.
Let's fix it by updating the zIndex(for:) function:

private func zIndex(for card: Card) -> Double {


guard let cardIndex = index(for: card) else {
return 0.0
}

// The default z-index of a card is set to a negative value of the card's inde
x,
// so that the first card will have the largest z-index.
let defaultZIndex = -Double(cardIndex)

// If it's the dragging card


if let draggingIndex = dragState.index,
cardIndex == draggingIndex {
// we compute the new z-index based on the translation's height
return defaultZIndex + Double(dragState.translation.height/Self.cardOffset
)
}

// Otherwise, we return the default z-index


return defaultZIndex
}

The default z-index is still set to the negative value of the card's index. For the dragging
card, we need to compute a new z-index as the user drags across the deck. The updated z-
index is calculated based on the translation's height and the default offset of the card (i.e.
50 points).

Run the app and drag the Visa card again. Now the z-index is continuously updated as
you drag the card.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 475


Figure 14. Moving the Visa card to the back

Updating the Card Deck


When you release the card, it returns to its original position. So, how can we reorder the
cards' after the drag?

The trick here is to update the items of the cards array, so as to trigger a UI update.
First, we need to mark the cards variable as a state variable like this:

@State var cards: [Card] = testCards

Next, let's create another new function for rearranging the cards:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 476


private func rearrangeCards(with card: Card, dragOffset: CGSize) {
guard let draggingCardIndex = index(for: card) else {
return
}

var newIndex = draggingCardIndex + Int(-dragOffset.height / Self.cardOffset)


newIndex = newIndex >= cards.count ? cards.count - 1 : newIndex
newIndex = newIndex < 0 ? 0 : newIndex

let removedCard = cards.remove(at: draggingCardIndex)


cards.insert(removedCard, at: newIndex)

When you drag the card over the adjacent cards, we need to update the z-index once the
drag's translation is greater than the default offset. Figure 15 shows the expected
behaviour of the drag.

Figure 15. Dragging the mastercard between the adjacent cards

This is the formula we use to compute the updated z-index:

var newIndex = draggingCardIndex + Int(-dragOffset.height / Self.cardOffset)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 477


Once we have the updated index, the last step is to update the item in the cards array by
removing the dragging card and insert it into the new position. Since the cards array is
now a state variable, SwiftUI updates the card deck and renders the animation
automatically.

Lastly, insert the following line of code under // Rearrange the cards to call the function:

withAnimation {
self.rearrangeCards(with: card, dragOffset: drag.translation)
}

After that, you are ready to run the app to test it out. Congratulations, You've built the
Wallet-like animation.

Summary
After going through this chapter, I hope you have gained a deeper understanding of
SwiftUI animation and view transitions. If you compare SwiftUI with the original UIKit
framework, you'll notice that SwiftUI has made it significantly easier to work with
animations. Remember how you rendered the card animation when the user released the
dragging card? All you had to do was update the state variable, and SwiftUI took care of
the heavy lifting. That's the power of SwiftUI!

For reference, you can download the complete wallet project here:

Demo project (https://www.appcoda.com/resources/swiftui5/SwiftUIWallet.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 478


Chapter 21
Working with JSON, Slider and Data
Filtering
JSON, short for JavaScript Object Notation, is a widely used data format for exchanging
data between client-server applications. As mobile app developers, we often encounter
JSON since it is the preferred format for data exchange in web APIs and backend web
services.

In this chapter, we will discuss how you can work with JSON while building an app using
the SwiftUI framework. If you have never worked with JSON, I would recommend you
read this free chapter from our Intermediate programming book. It provides a detailed
explanation of the two different approaches for handling JSON in Swift.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 479


Figure 1. The demo app

As part of our learning process, we will create a simple JSON app that utilizes a JSON-
based API provided by Kiva.org to explore JSON and its related APIs. Kiva is a non-profit
organization dedicated to connecting people through lending in order to alleviate
poverty. It enables individuals to lend as little as $25 to support various opportunities
worldwide. Kiva offers free web-based APIs that developers can access to retrieve data.

In our demo app, we will leverage the Kiva API to fetch the most recent fundraising loans
and display them in a list view, as shown in Figure 1. Furthermore, we will showcase the
usage of the Slider, one of the many built-in UI controls provided by SwiftUI. By utilizing
the slider, we will implement a data filtering feature in the app, allowing users to filter
the loan data in the displayed list.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 480


Figure 2. A slider control

Understanding JSON and Codable


First things first, let's take a look at the JSON format. If you're unfamiliar with JSON, you
can open a browser and visit the following web API provided by Kiva:

https://api.kivaws.org/v1/loans/newest.json

You should see something like this:

{
"loans": [
{
"activity": "Fruits & Vegetables",
"basket_amount": 25,
"bonus_credit_eligibility": false,
"borrower_count": 1,
"description": {
"languages": [
"en"
]
},
"funded_amount": 0,
"id": 1929744,
"image": {
"id": 3384817,

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 481


"template_id": 1
},
"lender_count": 0,
"loan_amount": 250,
"location": {
"country": "Papua New Guinea",
"country_code": "PG",
"geo": {
"level": "town",
"pairs": "-9.4438 147.180267",
"type": "point"
},
"town": "Port Moresby"
},
"name": "Mofa",
"partner_id": 582,
"planned_expiration_date": "2020-04-02T08:30:11Z",
"posted_date": "2020-03-03T09:30:11Z",
"sector": "Food",
"status": "fundraising",
"tags": [],
"themes": [
"Vulnerable Groups",
"Rural Exclusion",
"Underfunded Areas"
],
"use": "to purchase additional vegetables to increase her currrent sal
es."
},

...

"paging": {
"page": 1,
"page_size": 20,
"pages": 284,
"total": 5667
}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 482


Your results may not be formatted the same but this is what a JSON response looks like.
If you're using Chrome, you can download and install an extension called JSON
Formatter (http://link.appcoda.com/json-formatter) to beautify the JSON response.

Alternatively, you can format the JSON data on Mac by using the following command:

curl https://api.kivaws.org/v1/loans/newest.json | python -m json.tool > kiva-loan


s-data.txt

This will format the response and save it to a text file.

Now that you have seen JSON, let's learn how to parse JSON data in Swift. Starting with
Swift 4, Apple introduced a new way to encode and decode JSON data by adopting a
protocol called Codable .

Codable simplifies the entire process by offering developers a different way to decode (or
encode) JSON. As long as your type conforms to the Codable protocol, along with the
new JSONDecoder , you will be able to decode the JSON data into your specified instances.

Figure 3 illustrates the decoding of sample loan data into an instance of Loan using
JSONDecoder .

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 483


Figure 3. JSONDecoder decodes JSON data and convert it into an instance of Loan

Using JSONDecoder and Codable


Before building the demo app, let's try out JSON decoding on Playgrounds. Fire up
Xcode and create a new App project called SwiftUIKivaLoan . Next, right click the
SwiftUIKivaLoan folder in the project navigator and choose New file.... Under the iOS
tab, select Blank Playground to create a new playground. Once you have created your
Playground file, declare the following json variable:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 484


let json = """
{

"name": "John Davis",


"country": "Peru",
"use": "to buy a new collection of clothes to stock her shop before the holidays."
,
"amount": 150

}
"""

Assuming you're new to JSON parsing, let's make things simple. The above is a simplified
JSON response, similar to that shown in the previous section.

To parse the data, declare the Loan structure like this:

struct Loan: Codable {


var name: String
var country: String
var use: String
var amount: Int
}

As you can see, the structure adopts the Codable protocol. The variables defined in the
structure match the keys of the JSON response. This is how you let the decoder know
how to decode the data.

Now let's see the magic!

Continue to insert the following code in your Playground file:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 485


let decoder = JSONDecoder()

if let jsonData = json.data(using: .utf8) {

do {
let loan = try decoder.decode(Loan.self, from: jsonData)
print(loan)

} catch {
print(error)
}
}

If you run the project, you should see a message displayed in the console. That's a Loan

instance, populated with the decoded values.

Figure 4. Display the decoded loan data in the console

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 486


Let's look into the code snippet again. We instantiate an instance of JSONDecoder and
then convert the JSON string into Loan . The magic happened in this line of code:

let loan = try decoder.decode(Loan.self, from: jsonData)

You just need to call the decode method of the decoder with the JSON data and specify
the type of value to decode (i.e. Loan.self ). The decoder will automatically parse the
JSON data and convert them into a Loan object.

Cool, right?

Working with Custom Property Names


Now, let's jump into something more complicated. What if the name of the property and
the key of the JSON are different? How can you define the mapping?

For example, we modify the json variable like this:

let json = """


{

"name": "John Davis",


"country": "Peru",
"use": "to buy a new collection of clothes to stock her shop before the holidays."
,
"loan_amount": 150

}
"""

As you can see, the key amount is now loan_amount. In order to decode the JSON data,
you can modify the property name from amount to loan_amount . However, we really want
to keep the name amount . In this case, how can we define the mapping?

To define the mapping between the key and the property name, you are required to
declare an enum called CodingKeys that has a raw value of type String and conforms to
the CodingKey protocol.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 487


Now update the Loan structure like this:

struct Loan: Codable {


var name: String
var country: String
var use: String
var amount: Int

enum CodingKeys: String, CodingKey {


case name
case country
case use
case amount = "loan_amount"
}
}

In the enum, you define all the property names of your model and their corresponding
keys in the JSON data. For example, the case amount is defined to map to the key
loan_amount . If both the property name and the key of the JSON data are the same, you
can omit the assignment.

Working with Nested JSON Objects


Now that you understand the basics, let's dive even deeper and decode a more realistic
JSON response. First, update the json variable like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 488


let json = """
{

"name": "John Davis",


"location": {
"country": "Peru",
},
"use": "to buy a new collection of clothes to stock her shop before the holidays."
,
"loan_amount": 150

}
"""

We've added the location key that has a nested JSON object with the nested key
country . So, how do we decode the value of country from the nested object?

Let's modify the Loan structure like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 489


struct Loan: Codable {
var name: String
var country: String
var use: String
var amount: Int

enum CodingKeys: String, CodingKey {


case name
case country = "location"
case use
case amount = "loan_amount"
}

enum LocationKeys: String, CodingKey {


case country
}

init(from decoder: Decoder) throws {


let values = try decoder.container(keyedBy: CodingKeys.self)

name = try values.decode(String.self, forKey: .name)

let location = try values.nestedContainer(keyedBy: LocationKeys.self, forK


ey: .country)
country = try location.decode(String.self, forKey: .country)

use = try values.decode(String.self, forKey: .use)


amount = try values.decode(Int.self, forKey: .amount)

}
}

Similar to what we have done earlier, we have to define an enum CodingKeys . For the
case country , we specify to map to the key location . To handle the nested JSON object,
we also need to define an additional enumeration. In the code above, we name it
LocationKeys and declare the case country , which matches the key country of the nested
object.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 490


Since it is not a direct mapping, we need to implement the initializer of the Decodable

protocol to handle the decoding of all properties. In the init method, we first invoke the
container method of the decoder with CodingKeys.self to retrieve the data related to the
specified coding keys, which are name , location , use and amount .

To decode a specific value, we call the decode method with the specific key (e.g. .name )
and the associated type (e.g. String.self ). The decoding of the name , use and amount

is pretty straightforward. For the country property, the decoding is a little bit tricky. We
have to call the nestedContainer method with LocationKeys.self to retrieve the nested
JSON object. From the values returned, we further decode the value of country .

That is how you decode JSON data with nested objects.

Working with Arrays


The JSON data returned from Kiva API comes with more than one loan. Multiple loans
are structured in the form of an array. Let's see how to decode an array of JSON objects
using Codable.

First, modify the json variable like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 491


let json = """
{
"loans":
[{
"name": "John Davis",
"location": {
"country": "Paraguay",
},
"use": "to buy a new collection of clothes to stock her shop before the holidays."
,
"loan_amount": 150
},
{
"name": "Las Margaritas Group",
"location": {
"country": "Colombia",
},
"use": "to purchase coal in large quantities for resale.",
"loan_amount": 200
}]
}
"""

In the example above, there are two loans in the json variable. How do you decode it
into an array of Loan ?

To do that, declare another struct named LoanStore that also adopts Codable :

struct LoanStore: Codable {


var loans: [Loan]
}

This LoanStore only has a loans property that matches the key loans of the JSON data.
And, its type is defined as an array of Loan .

To decode the loans, modify this line of code from:

let loan = try decoder.decode(Loan.self, from: jsonData)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 492


to:

let loanStore = try decoder.decode(LoanStore.self, from: jsonData)

The decoder will automatically decode the loans JSON objects and store them into the
loans array of LoanStore . To print out the loans replace the line print(loan) with

for loan in loanStore.loans {


print(loan)
}

You should see a similar message as shown in figure 5.

Figure 5. Print out the loans array

That's how you decode JSON using Swift.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 493


Note: For reference, the Playgrounds project is included in the final Xcode project. You
can find the download link at the end of this chapter.

Building the Kiva Loan App


Alright, you should now understand how to handle JSON decoding. Let's begin to build
the demo app and see how you apply the skills you just learned.

Let's begin by creating the model class to store the latest loans. We will handle the
implementation of user interface later.

Retrieving the Latest Loans from Kiva


First, create a new file using the Swift File template and name it Loan.swift . This file
stores the Loan structure that adopts the Codable protocol for JSON decoding.

Insert the following code in the file:

struct Loan: Identifiable {


var id = UUID()
var name: String
var country: String
var use: String
var amount: Int

init(name: String, country: String, use: String, amount: Int) {


self.name = name
self.country = country
self.use = use
self.amount = amount
}

extension Loan: Codable {


enum CodingKeys: String, CodingKey {
case name
case country = "location"
case use

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 494


case amount = "loan_amount"
}

enum LocationKeys: String, CodingKey {


case country
}

init(from decoder: Decoder) throws {


let values = try decoder.container(keyedBy: CodingKeys.self)

name = try values.decode(String.self, forKey: .name)

let location = try values.nestedContainer(keyedBy: LocationKeys.self, forK


ey: .country)
country = try location.decode(String.self, forKey: .country)

use = try values.decode(String.self, forKey: .use)


amount = try values.decode(Int.self, forKey: .amount)

}
}

The code is almost the same as we discussed in the previous section. We just use an
extension to adopt the Codable protocol. Other than Codable , this structure also adopts
the Identifiable protocol and has an id property defaulting to UUID() . Later, we will
use SwiftUI's List control to present the loans. This is why we make this structure adopt
the Identifiable protocol.

Next, create another file using the Swift File template and name it LoanStore.swift . This
class is responsible for connecting to Kiva's web API, decoding the JSON data, and
storing them locally.

Let's write the LoanStore class step by step, so you can better understand how I came up
with the implementation. Insert the following code in LoanStore.swift :

class LoanStore: Decodable {


var loans: [Loan] = []
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 495


Later, the decoder will decode the loans JSON objects and store them into the loans

array of LoanStore . This is why we create the LoanStore like above. The code looks very
similar to the LoanStore structure we created before. However, it adopts the Decodable

protocol instead of Codable .

If you look into the documentation of Codable , it is just a type alias of a protocol
composition:

typealias Codable = Decodable & Encodable

Decodable and Encodable are the two actual protocols you need to work with. Since
LoanStore is only responsible for handling the JSON decoding, we adopt the Decodable

protocol.

As mentioned earlier, we will display the loans using a List view. So, other than
Decodable , we have to adopt the ObservableObject protocol and mark the loans variable
with the @Published property wrapper like this:

class LoanStore: Decodable, ObservableObject {


@Published var loans: [Loan] = []
}

By doing so, SwiftUI will manage the UI update automatically whenever there is any
change to the loans variable. If you have forgotten what ObservableObject is, please read
chapter 14 again.

Once you add the @Published property wrapper, Xcode shows you an error. The
Decodable (or Codable ) protocol doesn't play well with @Published .

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 496


Figure 6. Xcode error saying that LoanStore doesn't conform to Decodable

To fix the error, it requires some extra work. When the @Published property wrapper is
used, we need to manually implement the required method of Decodable . If you look into
the documentation (link), here is the method to adopt:

init(from decoder: Decoder) throws

Actually, we've implemented the method before when decoding the nested JSON objects.
Now, update the class like this:

class LoanStore: Decodable, ObservableObject {


@Published var loans: [Loan] = []

enum CodingKeys: CodingKey {


case loans
}

required init(from decoder: Decoder) throws {


let values = try decoder.container(keyedBy: CodingKeys.self)
loans = try values.decode([Loan].self, forKey: .loans)
}

init() {

}
}

We added the CodingKeys enum that explicitly specifies the key to decode. And then, we
implemented the custom initializer to handle the decoding.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 497


Okay, the error is now fixed. What's next?

Calling the Web API


So far, we just set up everything for JSON decoding but we haven't consumed the web
API. Declare a new variable in the LoanStore class to store the URL of the Kiva's API:

private static var kivaLoanURL = "https://api.kivaws.org/v1/loans/newest.json"

Next, insert the following methods in the class:

func fetchLatestLoans() {
guard let loanUrl = URL(string: Self.kivaLoanURL) else {
return
}

let request = URLRequest(url: loanUrl)


let task = URLSession.shared.dataTask(with: request, completionHandler: { (dat
a, response, error) -> Void in

if let error = error {


print(error)
return
}

// Parse JSON data


if let data = data {
DispatchQueue.main.async {
self.loans = self.parseJsonData(data: data)
}

}
})

task.resume()
}

func parseJsonData(data: Data) -> [Loan] {

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 498


let decoder = JSONDecoder()

do {

let loanStore = try decoder.decode(LoanStore.self, from: data)


self.loans = loanStore.loans

} catch {
print(error)
}

return loans
}

The fetchLatestLoans() method connects to the web API using URLSession . Once it
receives the data returned by the API, it passes the data to the parseJsonData method to
decode the JSON and convert the loan data into an array of Loan .

You may wonder why we need to wrap the following line of code with
DispatchQueue.main.async :

DispatchQueue.main.async {
self.loans = self.parseJsonData(data: data)
}

When calling the web API, the operation is performed in a background queue. Here, the
loans variable is marked as @Published . That means, for any modification of the
variable, SwiftUI will trigger an update of the user interface. UI updates are required to
run in the main queue. This is why we wrap it using DispatchQueue.main.async .

Implementing the User Interface


Now that we have created the classes ready for retrieving the loan data, let's move onto
the implementation of the user interface. To help you remember what the UI looks like,
look at the following figure. This is the UI we are going to build.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 499


Figure 7. The user interface of our demo app

And, instead of coding the UI in one file, we will break it down into three views:

ContentView.swift - this is the main view presenting the list of loans


LoanCellView.swift - this is the cell view
LoanFilterView.swift - this is the view showing the filtering option

Let's begin with the cell view. In the project navigator, right click SwiftUIKivaLoan and
choose New file.... Select the SwiftUI View template and name the file
LoanCellView.swift .

Update the LoanCellView like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 500


struct LoanCellView: View {

var loan: Loan

var body: some View {


HStack(alignment: .top) {
VStack(alignment: .leading) {
Text(loan.name)
.font(.system(.headline, design: .rounded))
.bold()
Text(loan.country)
.font(.system(.subheadline, design: .rounded))
Text(loan.use)
.font(.system(.body, design: .rounded))
}

Spacer()

VStack {
Text("$\(loan.amount)")
.font(.system(.title, design: .rounded))
.bold()
}
}
.frame(minWidth: 0, maxWidth: .infinity)

}
}

This view takes in a Loan and renders the cell view. The code is self-explanatory but if
you want to preview the cell view, you will need to modify the #Preview code as shown
below:

#Preview {
LoanCellView(loan: Loan(name: "Ivan", country: "Uganda", use: "to buy a plot o
f land", amount: 575))
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 501


We instantiate a dummy loan and pass it to the cell view for rendering. Your preview
pane should be similar to that shown in figure 8.

Figure 8. The loan cell view

Now go back to ContentView.swift to implement the list view. First, declare a variable
named loanStore :

@ObservedObject var loanStore = LoanStore()

Since we want to observe the change of loan store and update the UI, the loanStore is
marked with the @ObservedObject property wrapper.

Next, update the body variable like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 502


var body: some View {
NavigationStack {

List(loanStore.loans) { loan in

LoanCellView(loan: loan)
.padding(.vertical, 5)
}

.navigationTitle("Kiva Loan")

}
.task {
self.loanStore.fetchLatestLoans()
}
}

If you've read chapters 10 and 11, you should understand how to present a list view and
embed it in a navigation view. That's what the code above does. The code in the task

closure will be invoked when the view appears. We call the fetchLatestLoans() method to
retrieve the latest loans from Kiva.

If this is the first time you're using .task , it's very similar to .onAppear . Both allow you
to run asynchronous tasks when the view appears. The main difference is that .task will
automatically cancel the task when the view is destroyed. It's more appropriate in this
case.

Now test the app in the preview or on a simulator. You should be able to see the loan
records.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 503


Figure 9. Presenting the loans in a list view

Creating the Filter View with a Slider


Before we finish this chapter, I want to show you how to implement a filter feature. This
filter function allows users to define a maximum loan amount and only display the
records below that value. Figure 7 shows a sample filter view. Users can use a slider to
configure the maximum amount.

Again, we want our code to be better organized. So, create a new file for the filter view
and name it LoanFilterView.swift .

Next update the LoanFilterView struct like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 504


struct LoanFilterView: View {

@Binding var amount: Double

var minAmount = 0.0


var maxAmount = 10000.0

var body: some View {


VStack(alignment: .leading) {

Text("Show loan amount below $\(Int(amount))")


.font(.system(.headline, design: .rounded))

HStack {

Slider(value: $amount, in: minAmount...maxAmount, step: 100)


.accentColor(.purple)

HStack {
Text("\(Int(minAmount))")
.font(.system(.footnote, design: .rounded))

Spacer()

Text("\(Int(maxAmount))")
.font(.system(.footnote, design: .rounded))
}

}
.padding(.horizontal)
.padding(.bottom, 10)
}
}

I assume you fully understand stack views. Therefore, I'm not going to discuss how they
are used to create the layout. But let's talk a bit more about the Slider control. It's a
standard component provided by SwiftUI. You can instantiate the slider by passing it the

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 505


binding, range, and step of the slider. The binding holds the current value of the slider.
Here is sample code for creating a slider:

Slider(value: $amount, in: minAmount...maxAmount, step: 100)

The step controls the amount of change when the user drags the slider. If you let the user
have finer control, set the step to a smaller number. For the code above, we set it to 100.

In order to preview the filter view, update the FilterView_Previews like this:

#Preview {
LoanFilterView(amount: .constant(10000))
}

Now your preview should look like figure 10.

Figure 10. The filter view for setting the display criteria

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 506


Okay, we've implemented the filter view. However, we haven't implemented the actual
logic for filtering the records. Let's enhance the LoanStore.swift to power it with the filter
function.

First, declare the following variable which is used to store a copy of the loan records for
the filter operation:

private var cachedLoans: [Loan] = []

To save the copy, insert the following line of code after self.loans =

self.parseJsonData(data: data) :

self.cachedLoans = self.loans

Lastly, create a new function for the filtering:

func filterLoans(maxAmount: Int) {


self.loans = self.cachedLoans.filter { $0.amount < maxAmount }
}

This function takes in the value of maximum amount and filter those loan items that are
below this limit.

Cool! We are almost done.

Let's go back to ContentView.swift to present the filter view. What we are going to do is
add a navigation bar button at the top-right corner. When a user taps this button, the app
presents the filter view.

Let's first declare two state variables:

@State private var filterEnabled = false


@State private var maximumLoanAmount = 10000.0

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 507


The filterEnabled variable stores the current state of the filter view. It's set to false by
default indicating that the filter view is not shown. The maximumLoanAmount stores the
maximum loan amount for display. Any loan records with an amount larger than this
limit will be hidden.

Next, update the code of NavigationView like this:

NavigationStack {
VStack {
if filterEnabled {
LoanFilterView(amount: self.$maximumLoanAmount)
.transition(.opacity)
}

List(loanStore.loans) { loan in

LoanCellView(loan: loan)
.padding(.vertical, 5)
}
}

.navigationTitle("Kiva Loan")
}

We added the LoanFilterView and embed it in a VStack. The appearance of


LoanFilterView is controlled by the filterEnabled variable. When filterEnabled is set to
true , the app will insert the loan filter view on top of the list view. What's left is the
navigation bar button. Insert the following code and place it after .navigationTitle("Kiva

Loan") :

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 508


.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
withAnimation(.linear) {
self.filterEnabled.toggle()
self.loanStore.filterLoans(maxAmount: Int(self.maximumLoanAmount))
}
} label: {
Text("Filter")
.font(.subheadline)
.foregroundColor(.primary)
}
}
}

This adds a navigation bar button at the top-right corner. When the button is tapped, we
toggle the value of filterEnabled to show/hide the filter view. Additionally, we call the
filterLoans function to filter the loan item.

Now test the app in the preview. You should see a filter button on the navigation bar. Tap
it once to bring up the filter view. You can then set a new limit (e.g. $500). Tap the
button again and the app will only show you the loan records that are below $500.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 509


Figure 11. Presenting the filter view

Summary
We covered quite a lot in this chapter. You should know how to consume web APIs, parse
the JSON content, and present the data in a list view. We also briefly covered the usage of
the Slider control.

If you've developed an app using UIKit before, you will be amazed by the simplicity of
SwiftUI. Take a look at the code of ContentView again. It only takes around 40 lines of
code to create the list view. Most importantly, you don't need to handle UI updates
manually or pass data around. Everything just works behind the scenes.

For reference, you can download the complete loan project here:

Demo project (https://www.appcoda.com/resources/swiftui5/SwiftUIKivaLoan.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 510


Chapter 22
Building a ToDo App with Swift Data
One common question in iOS app development is how to work with Core Data and
SwiftUI to save data permanently in the built-in database. In this chapter, we will answer
this question by building a ToDo app.

Since the ToDo demo app makes use of List and Combine to handle data presentation
and sharing, I'll assume that you've read the following chapters:

Chapter 7 - Understanding State and Binding


Chapter 10 - Understanding Dynamic List, ForEach and Identifiable
Chapter 14 - Data Sharing with Combine and Environment Objects

If you haven't done so or have forgotten what Combine and Environment Objects are,
please go back and read the chapters again.

What are we going to do in this chapter to understand SwiftData? Instead of building the
ToDo app from scratch, I've already built the core parts of the app. However, it can't save
data permanently. To be more specific, it can only save the to-do items in an array.
Whenever the user closes the app and starts it again, all the data is lost. We will modify
the app and convert it to use SwiftData for saving the data permanently to the local
database. Figure 1 shows some sample screenshots of the ToDo app.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 511


Figure 1. The ToDo demo app

Before we perform the modification, I will guide you through the starter project so that
you can fully understand how the code works. In addition to learning about Swift Data,
you will also learn how to customize the style of a toggle. Take a look at the screenshots
above. The checkbox is actually a toggle view in SwiftUI. I will show you how to create
these checkboxes by customizing the Toggle's style.

We've got a lot to cover in this chapter, so let's get started!

Understanding SwiftData
Before we check out the starter project of the ToDo app, let me give you a quick
introduction to Swift Data and how you're going to work with it in SwiftUI projects.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 512


First and foremost, it's important to note that the SwiftData framework (as well as Core
Data) should not be confused with a database. SwiftData is actually a framework
designed to help developers manage and interact with data on a persistent store. While
the default persistent store for iOS is typically the SQLite database, it's worth noting that
persistent stores can take other forms as well. For example, Core Data can also be used to
manage data in a local file, such as an XML file.

Regardless of whether you're using Core Data or the SwiftData framework, both tools
serve to shield developers from the complexities of the underlying persistent store.
Consider the SQLite database, for instance. With SwiftData, there's no need to worry
about connecting to the database or understanding SQL in order to retrieve data records.
Instead, developers can focus on working with APIs and Swift Macros, such as @Query

and @Model , to effectively manage data in their applications.

The SwiftData framework is newly introduced in iOS 17 to replace the previous


framework called Core Data. Core Data has long been the data management APIs for iOS
development since the era of Objective-C. Even though developers can integrate the
framework into Swift projects, Core Data is not a native solution for both Swift and
SwiftUI.

In iOS 17, Apple finally introduces a native framework called SwiftData for Swift on
persistent data management and data modeling. It's built on top of Core Data but the
APIs are completely redesigned to make the most out of Swift.

Figure 2. Data model in Core Data

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 513


If you have used Core Data before, you may remember that you have to create a data
model (with a file extension .xcdatamodeld) using a data model editor for data
persistence. With the release of SwiftData, you no longer need to do that. SwiftData
streamlines the whole process with macros, another new Swift feature in iOS 17. Say, for
example, you already define a model class for Song as follows:

class Song {
var title: String
var artist: String
var album: String
var genre: String
var rating: Double
}

To use SwiftData, the new @Model macro is the key for storing persistent data using
SwiftUI. Instead of building the data model with model editor, SwiftData just requires
you to annotate the model class with the @Model macro like this:

@Model class Song {


var title: String
var artist: String
var album: String
var genre: String
var rating: Double
}

This is how you define the schema of the data model in code. With this simple keyword,
SwiftData automatically enables persistence for the data class and offers other data
management functionalities such as iCloud sync. Attributes are inferred from properties
and it supports basic value types such as Int and String.

SwiftData allows you to customize how your schema is built using property metadata.
You can add uniqueness constraints by using the @Attribute annotation, and delete
propagation rules with the @Relationship annotation. If there are certain properties you
do not want included, you can use the @Transient macro to tell SwiftData to exclude
them. Here is an example:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 514


@Model class Album {
@Attribute(.unique) var name: String
var artist: String
var genre: String

// The cascade relationship instructs SwiftData to delete all


// songs when the album is deleted.
@Attribute(.cascade) var songs: [Song]? = []
}

To drive the data persistent operations, there are two key objects of SwiftData that you
should be familiar with: ModelContainer and ModelContext . The ModelContainer serves as
the persistent backend for your model types. To create a ModelContaine r, you simply need
to instantiate an instance of it.

// Basic
let container = try ModelContainer(for: [Song.self, Album.self])

// With configuration
let container = try ModelContainer(for: [Song.self, Album.self],
configurations: ModelConfiguration(url: URL("p
ath"))))

In SwiftUI, you can set up the model container at the root of the application:

import SwiftData
import SwiftUI

@main
struct MusicApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer (for: [Song.self, Album.self]))
}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 515


Once you have set up the model container, you can begin using the model context to fetch
and save data. The context serves as your interface for tracking updates, fetching data,
saving changes, and even undoing those changes. When working with SwiftUI, you can
typically obtain the model context from your view's environment:

struct ContextView: View {


@Environment(\.modelContext) private var modelContext
}

With the context, you are ready to fetch data. The simplest way is to use the @Query

property wrapper. You can easily load and filter anything stored in your database with a
single line of code.

@Query(sort: \.artist, order: .reverse) var songs: [Song]

To insert item in the persistent store, you can call the insert method of the model context
and pass it the model objects to insert.

modelContext.insert(song)

Similarly, you can delete the item via the model context like this:

modelContext.delete(song)

This is a brief introduction of SwiftData. If you're still feeling confused about how to use
SwiftData? No worries. You will understand its usage after building a ToDO app.

Understanding the ToDo App Demo


Now that you have a basic understanding of SwiftData, I'd like to walk you through a
demo of the app. Later on, we'll convert this ToDo demo to allow it to save the to-do
items permanently. For now, as I mentioned before, all of the data is stored in memory
and will vanish when the app is restarted.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 516


First, please download the starter project from
https://www.appcoda.com/resources/swiftui5/SwiftUIToDoListStarter.zip. Unzip the
file and open SwiftUIToDoList.xcodeproj in Xcode. Select the ContentView.swift file and
preview the UI. You should see a screen like that shown in figure 5.

Figure 3. Previewing the demo app

To run the app, simply open it in the preview canvas or a simulator. From there, tap the +
button to add a to-do item. Repeat the procedure to add a few more items. The app will
then list all of the to-do items you've added. To mark an item as complete, simply tap the
checkbox next to it, and it will be crossed out.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 517


Figure 4. Adding a new task

How to present the list of Todo Items


Let's now walk through the code so you can gain a better understanding of how it works.
To begin, let's take a look at the model class. Open ToDoItem.swift in the Model folder.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 518


enum Priority: Int {
case low = 0
case normal = 1
case high = 2
}

@Observable class ToDoItem: Identifiable {


var id: UUID
var name: String
var priority: Priority
var isComplete: Bool

init(id: UUID = UUID(), name: String = "", priority: Priority = .normal, isCom
plete: Bool = false) {
self.id = id
self.name = name
self.priority = priority
self.isComplete = isComplete
}
}

The ToDo app demo is a simplified version of an ordinary ToDo app. Each to-do item (or
task), has three properties: name, priority, and isComplete (i.e. the status of the task).
This class has the @Observable macro, which is a new feature in iOS 17. It adds
observation support to ToDoItem and make it conform to the Observable protocol.

With the macro, the three properties are automatically marked with @Published so that
the subscribers are informed whenever there are any changes of the values. Later, in the
implementation of ContentView , SwiftUI listens for value changes and updates the views
accordingly. For example, when the value of isComplete changes, it toggles the checkbox.

This class also conforms to the Identifiable protocol such that each instance of
ToDoItem has an unique identifier. Later, we will use the ForEach and List to display
the to-do items. This is why we need to adopt the protocol and create the id property.

Now let's move onto the views and begin with the ContentView.swift file. Assuming
you've read chapter 10, you should understand most of the code. The content view has
three main parts, which are embedded in a ZStack :

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 519


1. The list view that presents all the to-do items.
2. The empty view (NoDataView) that is displayed when there are no to-do items .
3. The "Add a new task" view that is shown when a user taps the + button.

Take a look at the first VStack :

VStack {

HStack {
Text("ToDo List")
.font(.system(size: 40, weight: .black, design: .rounded))

Spacer()

Button(action: {
self.showNewTask = true
}) {
Image(systemName: "plus.circle.fill")
.font(.largeTitle)
.foregroundStyle(.purple)
}
}
.padding()

List {
ForEach(todoItems) { todoItem in
ToDoListRow(todoItem: todoItem)
}
}
.listStyle(.plain)
}
.rotation3DEffect(Angle(degrees: showNewTask ? 5 : 0), axis: (x: 1, y: 0, z: 0))
.offset(y: showNewTask ? -50 : 0)
.animation(.easeOut, value: showNewTask)

To hold all of the to-do items, I've declared a state variable named todoItems . It's marked
with @State so that the list will be automatically refreshed whenever changes are made.
In the List view, we use ForEach to loop through the items in the array.

We handle the rows of the list, by a separate view named ToDoListRow :

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 520


struct ToDoListRow: View {

@Bindable var todoItem: ToDoItem

var body: some View {


Toggle(isOn: self.$todoItem.isComplete) {
HStack {
Text(self.todoItem.name)
.strikethrough(self.todoItem.isComplete, color: .black)
.bold()
.animation(.default)

Spacer()

Circle()
.frame(width: 10, height: 10)
.foregroundColor(self.color(for: self.todoItem.priority))
}
}.toggleStyle(CheckboxStyle())
}

private func color(for priority: Priority) -> Color {


switch priority {
case .high: return .red
case .normal: return .orange
case .low: return .green
}
}
}

This view takes in a to-do item, which is a ObservableObject . This means for any changes
of that to-do item, the view that subscribes to the item will be invalidated automatically.
In iOS 17, you can use a @Bindable property wrapper to hold the binding of the
obersvable object.

For each row of the to-do item, consists of three parts:

1. A toggle / checkbox - indicates whether the task is complete or not.


2. A text label - shows the name of the task

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 521


3. A dot / circle - shows the priority of the task

The second and third parts of the code are relatively straightforward. However, the
checkbox control warrants a deeper discussion. In SwiftUI, there's a standard control
called Toggle that's typically used to create a Settings screen. The toggle is presented as
a switch that allows users to flip between "on" and "off". However, in the ToDo app, we
want the toggle to look more like a checkbox.

Customizing the look & feel of a Toggle


Similar to the Button control we discussed in Chapter 6, Toggle also allows developers
to customize its style. To do so, simply implement the ToggleStyle protocol and provide
the desired customizations. If you'd like to see an example of this in action, you can open
the CheckBoxStyle.swift file in the project navigator.

struct CheckboxStyle: ToggleStyle {

func makeBody(configuration: Self.Configuration) -> some View {

return HStack {

Image(systemName: configuration.isOn ? "checkmark.circle.fill" : "circ


le")
.resizable()
.frame(width: 24, height: 24)
.foregroundColor(configuration.isOn ? .purple : .gray)
.font(.system(size: 20, weight: .bold, design: .default))
.onTapGesture {
configuration.isOn.toggle()
}

configuration.label

}
}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 522


In the code, we implement the makeBody method, which is the requirement of the
protocol. We create an image view which displays a checkmark image or a circle image,
depending on the status of the toggle (i.e. configuration.isOn ). This is how you customize
the style of a toggle.

To use the CheckboxStyle , attach the toggleStyle modifier to the Toggle and specify the
checkbox style like this:

.toggleStyle(CheckboxStyle())

Handling the empty list view


When there are no items in the array, we present an image view instead of showing an
empty list view. This is completely optional. However, I think it makes the app look
better and let users know what to do when the app is first started.

// If there is no data, show an empty view


if todoItems.count == 0 {
NoDataView()
}

Since we have a ZStack to embed the views, it's pretty easy to control the appearance of
this empty view, which is only displayed when the array is empty.

Displaying the Add Task view


When a user taps the + button in the top-right corner of the app, the NewToDoView is
displayed. I'll walk you through this view shortly. The NewToDoView overlays on top of the
list view, appearing like a bottom sheet. Additionally, we add a blank view to darken the
list view behind it.

Here is the code for reference:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 523


if showNewTask {
BlankView(bgColor: .black)
.opacity(0.5)
.onTapGesture {
self.showNewTask = false
}

NewToDoView(isShow: $showNewTask, todoItems: $todoItems, name: "", priority: .


normal)
.transition(.move(edge: .bottom))
.animation(.interpolatingSpring(stiffness: 200.0, damping: 25.0, initialVe
locity: 10.0), value: showNewTask)
}

Understanding the Add Task view


Now, let's take a look at the code in NewToDoView.swift , which is designed to allow users
to add a new task or to-do item. You can refer to Figure 6 or simply open the file to
preview what this view looks like.

The NewToDoView takes in two bindings: isShow and todoItems . The isShow parameter
controls whether the "Add New Task" view should appear on screen. The todoItems

variable holds a reference to the array of to-do items. We need the caller to pass us the
binding to todoItems so that we can update the array with the new task.

@Binding var isShow: Bool


@Binding var todoItems: [ToDoItem]

@State var name: String


@State var priority: Priority
@State var isEditing = false

Within the view, users are able to input the name of the task and set its priority
(low/normal/high). The state variable isEditing indicates whether the user is currently
editing the task name. To ensure that the editing view isn't obscured by the software
keyboard, the app will shift the view upward while the user is editing the text field.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 524


TextField("Enter the task description", text: $name, onEditingChanged: { (editingC
hanged) in

self.isEditing = editingChanged

})

...

.offset(y: isEditing ? -320 : 0)

Once the user taps the Save button, we first verify whether the text field is empty. If it's
not empty, we create a new ToDoItem and call the addTask function to append it to the
todoItems array. If the text field is empty, we take no action.

// Save button for adding the todo item


Button(action: {

if self.name.trimmingCharacters(in: .whitespaces) == "" {


return
}

self.isShow = false
self.addTask(name: self.name, priority: self.priority)

}) {
Text("Save")
.font(.system(.headline, design: .rounded))
.frame(minWidth: 0, maxWidth: .infinity)
.padding()
.foregroundColor(.white)
.background(Color.purple)
.cornerRadius(10)
}
.padding(.bottom)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 525


Since the todoItems array is a state variable, the list view will be automatically refreshed
and display the new task once it's added to the array. That's how the code works. If you're
unclear about how the "Add task" view is displayed at the bottom of the screen, please
refer to Chapter 18, which covers building an Expandable Bottom Sheet.

Working with SwiftData


Now that we've gone through the starter project, it's time to convert the app to use
SwiftData for storing the to-do items in the database.

Defining the Schema with Code Using @Model


As discussed earlier, SwiftData makes it very easy to define a scheme with code. All you
need is annotate the model class with the @Model macro. Now open ToDoItem.swift and
add the following statement to import the SwiftData framework:

import SwiftData

Next, change @Observable to @Model for the ToDoItem class:

@Model class ToDoItem: Identifiable {


var id: UUID
var name: String
var priority: Priority
var isComplete: Bool

init(id: UUID = UUID(), name: String = "", priority: Priority = .normal, isCom
plete: Bool = false) {
self.id = id
self.name = name
self.priority = priority
self.isComplete = isComplete
}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 526


Xcode shows a number of compilation errors once you made the changes. The reason is
that Priority , which is an enum, cannot be directly stored in the persistent store. To
save this enum into the database, we will store its raw value which is an integer. Let's
change the ToDoItem class like this:

@Model class ToDoItem: Identifiable {


var id: UUID
var name: String

@Transient var priority: Priority {


get {
return Priority(rawValue: Int(priorityNum)) ?? .normal
}

set {
self.priorityNum = Int(newValue.rawValue)
}
}
@Attribute(originalName: "priority") var priorityNum: Priority.RawValue

var isComplete: Bool

init(id: UUID = UUID(), name: String = "", priority: Priority = .normal, isCom
plete: Bool = false) {
self.id = id
self.name = name
self.priority = priority
self.isComplete = isComplete
}
}

In the code above, we added a new property called priorityNum , which has the same type
as the raw value of the Priority enum (i.e. Int ). This raw value will be directly stored
in the persistent store. For the priorityNum property, we also instructed SwiftData to use
a different field name for the underlying schema.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 527


For the original priority property, we've changed it to a computed property. This
computed property is responsible for transforming the priority number into an Enum,
and vice versa. Since this computed property is not required to store in the persistent
store, we annotate it with the @Transient macro.

After the changes, you should be able to build and run the app in a simulator. Please note
that you can't preview the app due to a known bug in the beta version of Xcode 15.

Configuring the Model Container


Now open SwiftUIToDoListApp.swift and set up the model container for the app. For the
SwiftUIToDoListApp struct, update the code like this:

struct SwiftUIToDoListApp: App {


var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: ToDoItem.self)
}
}

We set a shared model container for storing instances of ToDoItem .

Adding data to the persistent store


With the model container set up, we are ready to use the model context to fetch and save
to do items. Let's begin with NewToDoView.swift . To save a new task in the database, you
need to first obtain the model context from the environment:

@Environment(\.modelContext) private var modelContext

Since we no longer use an array to hold the to-do items, you can remove this line of code:

@Binding var todoItems: [ToDoItem]

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 528


Next, let's update the addTask function like this:

private func addTask(name: String, priority: Priority, isComplete: Bool = false) {

let task = ToDoItem(name: name, priority: priority, isComplete: isComplete)

modelContext.insert(task)
}

To insert a new record into the database, simply call the insert method of the model
context and pass it the to-do item that needs to be saved.

Since we removed the todoItems binding, we need to update the preview code:

#Preview {
NewToDoView(isShow: .constant(true), name: "", priority: .normal)
}

Now let's move back to ContentView.swift . Similarly, you should see an error in the
ContentView (see figure 5).

Figure 5. Xcode shows you an error in ContentView

Change the line of code like this to fix the error:

NewToDoView(isShow: $showNewTask, name: "", priority: .normal)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 529


We simply remove the todoItems parameter. This is how we convert the demo app from
using an in-memory array as storage to a persistent store.

You can now run the app and add a few new tasks. They should be saved permanently
into the database.

Using @Query to fetch records


We've successfully added the data to the database. However, the to-do items are not yet
being displayed on the main screen. To fix this, we'll need to make some changes to
ContentView.swift . First, add an import statement to import the SwiftData framework:

import SwiftData

Next, we had an array variable that held all of the to-do items, which was also marked
with @State :

@State var todoItems: [ToDoItem] = []

Since we are moving to store the items in database, we need to modify this line of code
and fetch the data from it. Apple introduced a new property wrapper called @Query . This
makes it very easy to load data from the database.

Simply replace @State with @Query and remove the default value like this:

@Query var todoItems: [ToDoItem]

This @Query property automatically fetches the required data for you. In the code above,
we specify to fetch the ToDoItem instances. Optionally, you can configure the property
wrapper with the sort option and specify how the results should be ordered. The below
definition will sort the items based on priority:

@Query(sort: \ToDoItem.priorityNum, order: .reverse) var todoItems: [ToDoItem]

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 530


This is how you perform a fetch request and retrieve data from database. And, since the
properties of ToDoItem are kept intact, we DO NOT need to make any code changes for
the list view. We can use the fetch result directly in ForEach :

List {

ForEach(todoItems) { todoItem in
ToDoListRow(todoItem: todoItem)
}

You're now ready to test the app again. Upon launching it, you should be able to see all of
the tasks that you created in the previous section.

Updating an existing item


SwiftData significantly reduces the amount of work required to handle item updates or
modifications in the persistent store. By simply marking your model objects with the
@Model macro, SwiftData automatically modifies the setters for change tracking and
observation. This means that no code changes are needed to update the to-do items.

To test out the update behavior, simply run it on a simulator. You should be able to add
new tasks, which will appear in the list view immediately. The checkbox functionality
should also be working as expected. Most importantly, all of the changes are now saved
permanently in the device's database. Even after restarting the app, all of the items will
still be there.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 531


Figure 6. Your ToDo app now supports Core Data

Deleting an item from database


Now that you know how to perform fetch, update, and insert, how about data deletion?
We will add a feature to the app for removing a to-do item.

In the ContentView struct, declare a modelContext variable:

@Environment(\.modelContext) private var modelContext

Then add a new function called deleteTask like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 532


private func deleteTask(indexSet: IndexSet) {
for index in indexSet {
let itemToDelete = todoItems[index]
modelContext.delete(itemToDelete)
}
}

This function takes an index set that stores the indices of the items to be deleted. To
remove an item from the persistent store, simply call the delete function of the model
context and specify the item to be deleted.

Now that we have prepared the delete function, where should we invoke it? Attach the
onDelete modifier to ForEach of the list view like this:

List {

ForEach(todoItems) { todoItem in
ToDoListRow(todoItem: todoItem)
}
.onDelete(perform: deleteTask)

The onDelete modifier automatically enables the swipe-to-delete feature in the list view.
When a user deletes an item, we call the deleteTask function to remove the item from
the database.

To try this out, simply run the app and swipe to delete an item. This will completely
remove the item from the database.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 533


Figure 7. Deleting an item

Working with SwiftUI Preview


You should be aware that the preview of your app doesn't work since we changed the app
to use SwiftData. This is understandable because we haven't injected the model container
in the #Preview code block. So, how do we fix the issue and make the preview work?

First, we need to create an in-memory data store and populate it with some test data.
Open ContentView.swift and declare a preview container like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 534


@MainActor
let previewContainer: ModelContainer = {
do {
let container = try ModelContainer(for: ToDoItem.self,
ModelConfiguration(inMemory: true))

for index in 0..<10 {


let newItem = ToDoItem(name: "To do item #\(index)")
container.mainContext.insert(newItem)
}

return container
} catch {
fatalError("Failed to create container")
}
}()

In the code above, we create an instance of ModelContainer with the inMemory

configuration set. Then we add 10 sample to-do items and save them to the data store.

Now let's switch over to the ContentView.swift and update the preview code like this:

#Preview {
ContentView()
.modelContainer(previewContainer)
}

By attaching the modelContainer modifier and set it to the preview container, the content
view can now load the sample to-do items and display them in the preview canvas.

Summary
Throughout this chapter, we transformed a Todo list app from storing data in memory to
a persistent store. I hope that you now have a better understanding of how to integrate
SwiftData into a SwiftUI project and how to perform all basic CRUD (create, read, update

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 535


& delete) operations. The addition of the @Query property wrapper, model container, and
model context has made managing data in a persistent store incredibly simple and
efficient.

For reference, you can download the complete ToDoList project here:

Demo project (https://www.appcoda.com/resources/swiftui5/SwiftUIToDoList.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 536


Chapter 23
Integrating UIKit with SwiftUI Using
UIViewRepresentable
There are two common questions that developers often ask about SwiftUI. The first one is
related to data management in SwiftUI projects, that we have discussed in the previous
chapter. The second common question is about working with UIKit views in SwiftUI
projects. In this chapter, you will learn how to work with UIKit views by integrating a
UISearchBar into the Todo app.

If you're new to UIKit, UISearchBar is a built-in component of the framework that allows
developers to present a search bar for data search. Figure 1 showcases the standard
search bar in iOS. However, SwiftUI didn't provide this standard UI component out of
the box when it's first released. To implement a search bar in a SwiftUI project at the
time, one approach is to leverage the UISearchBar component from UIKit.

So, how do we interface with UIKit views or controllers in SwiftUI?

To ensure backward compatibility, Apple introduced a couple of new protocols in the iOS
SDK, namely UIViewRepresentable and UIViewControllerRepresentable . These protocols
enable you to wrap a UIKit view (or view controller) and make it accessible within your
SwiftUI project.

To see how this works, we will enhance our Todo app with a search function. We will add
a search bar just below the app title, allowing users to filter the to-do items by entering a
search term.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 537


Figure 1. Adding a search bar in the ToDo app

To get started, download the ToDo project at


https://www.appcoda.com/resources/swiftui5/SwiftUIToDoList.zip. We will build on
top of the ToDoList project. In case you haven't read chapter 22, I recommend you read it
first. This will help you better understand the topics we are going to discuss below,
especially if you have no experience with Core Data.

Understanding UIViewRepresentable
To use a UIKit view in SwiftUI, you can wrap the view with the UIViewRepresentable

protocol. Essentially, all you need to do is create a struct in SwiftUI that adopts the
protocol to create and manage a UIView object. Below is the basic structure of a custom
wrapper for a UIKit view:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 538


struct CustomView: UIViewRepresentable {

func makeUIView(context: Context) -> some UIView {


// Return the UIView object
}

func updateUIView(_ uiView: some UIView, context: Context) {


// Update the view
}
}

In the actual implementation, you replace some UIView with the UIKit view you want to
wrap. Let's say, we want to use UISearchBar in UIKit. The code can be written like this:

struct SearchBar: UIViewRepresentable {

func makeUIView(context: Context) -> UISearchBar {

return UISearchBar()
}

func updateUIView(_ uiView: UISearchBar, context: Context) {

// Update the view


}
}

In the makeUIView method, we return an instance of UISearchBar . This is how you wrap a
UIKit view and make it available to SwiftUI. To use the SearchBar , you can treat it like
any SwiftUI view and create it like this:

struct ContentView: View {


var body: some View {
SearchBar()
}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 539


Adding a Search Bar
Now back to the ToDoList project to add the search bar to the app. First, we will create a
new file for the search bar. In the project navigator, right click the View folder and
choose New File.... Select the SwiftUI View template and name the file SearchBar.swift .

Replace the content with the following code:

import SwiftUI

struct SearchBar: UIViewRepresentable {

@Binding var text: String

func makeUIView(context: Context) -> UISearchBar {

let searchBar = UISearchBar()

searchBar.searchBarStyle = .minimal
searchBar.autocapitalizationType = .none
searchBar.placeholder = "Search..."

return searchBar
}

func updateUIView(_ uiView: UISearchBar, context: Context) {

uiView.text = text
}
}

#Preview {
SearchBar(text: .constant(""))
}

The code is similar to the code shown in the previous section, but with a few key
differences:

1. Instead of creating a UISearchBar with the default appearance, we initialize it with a

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 540


minimal style, disable auto-capitalization, and update its placeholder value.
2. We've added a binding to hold the search term. While the makeUIView method is
responsible for creating and initializing the view object, the updateUIView method is
responsible for updating the state of the UIKit view. Whenever there is a state
change in SwiftUI, the framework automatically calls the updateUIView method to
update the configuration of the view. In this case, whenever the search term is
updated in SwiftUI, the method will be called, and we will update the text of the
UISearchBar .

Now switch over to ContentView.swift . Declare a state variable to hold the search text:

@State private var searchText = ""

To present the search bar, insert the following code before the List :

SearchBar(text: $searchText)
.padding(.top, -20)

The SearchBar is just like any other SwiftUI views. You can apply modifiers like padding
to adjust the layout. If you run the app in a simulator or simply test it in the preview, you
should see a search bar, though it doesn't function yet.

Figure 2. The ToDo app now has a search bar

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 541


Capturing the Search Text
Presenting a UIKit view in a SwiftUI app is relatively straightforward. However, making
the search bar function properly is a different story. Currently, users can type in the
search field, but the app doesn't perform the query yet. Ideally, the app should search the
to-do items on the fly as the user types in the search term.

So, how do we detect when the user enters a search term?

The search bar has a companion protocol called UISearchBarDelegate . This protocol
provides several methods for managing the search text. In particular, the following
method is called whenever the user changes the search text:

optional func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String)

To make the search bar functional, we have to adopt the UISearchBarDelegate protocol.
This is where things become more complex.

So far, we have only discussed a couple of the methods in the UIViewRepresentable

protocol. If you need to work with a delegate in UIKit and communicate back to SwiftUI,
you must implement the makeCoordinator method and provide a Coordinator instance.
This Coordinator acts as a bridge between the UIView's delegate and SwiftUI. Let's take a
look at the code so that you can better understand what this means.

In the SearchBar struct ( SearchBar.swift file), create a Coordinator class and implement
the makeCoordinator method like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 542


func makeCoordinator() -> Coordinator {
Coordinator($text)
}

class Coordinator: NSObject, UISearchBarDelegate {


@Binding var text: String

init(_ text: Binding<String>) {


self._text = text
}

func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {

searchBar.showsCancelButton = true
text = searchText

print("textDidChange: \(text)")
}
}

The makeCoordinator method simply returns an instance of Coordinator . The


Coordinator adopts the UISearchBarDelegate protocol and implements the
searchBar(_:textDidChange:) method. As mentioned, this method is called every time a
user changes the search text. Therefore, we capture the updated search text and pass it
back to SwiftUI by updating the text binding. I intentionally added a print statement in
the method so that you can see the changes when we test the app later.

Now that we have a Coordinator that adopts the UISearchBarDelegate protocol, we need
to make one more modification. In the makeUIView method, insert the following line of
code to assign the coordinator to the search bar:

searchBar.delegate = context.coordinator

That's it! Test the app again in the preview and type in the search field. You should see
the "textDidChange:" message in the console. If you can't see the message, go up to
Xcode menu and choose View > Debug Area > Activate Console to enable the console.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 543


Figure 3. The console displays the message as you type

Handling the Cancel Button


Have you tried tapping the Cancel button? If so, you probably noticed that it's not
functional. To make it work, we need to implement the following methods in the
Coordinator :

func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {


text = ""
searchBar.resignFirstResponder()
searchBar.showsCancelButton = false
searchBar.endEditing(true)
}

func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool {


searchBar.showsCancelButton = true

return true
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 544


The first method is triggered when the cancel button is clicked. In the code, we call
resignFirstResponder() to dismiss the keyboard and tell the search bar to end editing. The
second method ensures that the Cancel button appears when the user taps the search
field.

You can perform a quick test by running the app in a simulator. Tapping the Cancel
button while editing should dismiss the software keyboard.

Performing the Search


We can now retrieve the search text and handle the cancel button. Unfortunately, the
search bar still isn't functional. In this section, we'll implement the search functionality.

We'll use the filter function to perform the search because todoItems is synchronized
with the to-do items stored in the database. In Swift, you can use the filter function to
loop over a collection and get an array of items that matches the filter criteria. Here is an
example:

todoItems.filter({ $0.name.contains("Buy") })

The filter function takes a closure as an argument that specifies the filter criteria. For
example, the code above will return those items that contain the keyword "Buy" in its
name field.

To implement the search, we can replace the ForEach loop of the List like this:

ForEach(todoItems.filter({ searchText.isEmpty ? true : $0.name.contains(searchText


) })) { todoItem in
ToDoListRow(todoItem: todoItem)
}
.onDelete(perform: deleteTask)

In the closure of the filter function, we first check if the search text has a value. If not,
we simply return true , which means that it returns all items. Otherwise, we check if the
name field contains the search term.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 545


That's it! You can now run the app to test it out or simply test it in the preview canvas.
Type into the search field, and the app will filter the records that match the search term.

Figure 4. Filtering the todo items

Summary
In this chapter, you learned how to use the UIViewRepresentable protocol to integrate
UIKit views with SwiftUI. While SwiftUI is still very new and doesn't come with all the
standard UI components, this backward compatibility allows you to tap into the old
framework and utilize any views you need.

For reference, you can download the complete project here:

Demo project
(https://www.appcoda.com/resources/swiftui5/SwiftUIToDoListUISearchBar.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 546


Chapter 24
Creating a Search Bar View and
Working with Custom Binding
In a previous chapter, we demonstrated how to implement a search bar by reusing the
UISearchBar component from the old UIKit framework. But have you ever considered
building a search bar from scratch? Upon closer inspection, you'll find that it's not too
difficult to implement. So, let's embark on building a SwiftUI version of a search bar in
this chapter.

Not only will you learn how to create the search bar view, but we will also delve into the
realm of custom bindings. We've touched upon bindings before, but we haven't shown
you how to create a custom binding. Custom bindings are particularly useful when you
need to incorporate additional program logic during read and write operations.
Furthermore, we will explore how to dismiss the software keyboard in SwiftUI.

Figure 1 showcases the search bar we're about to build. It will have the same look and feel
as the UISearchBar in UIKit. Additionally, we will implement a Cancel button, which will
only appear when the user begins typing in the search field.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 547


Figure 1. Building a search bar view entirely using SwiftUI

Implementing the Search Bar UI


We will now convert the previous project from using UISearchBar to our custom
implementation of a search bar. To get started, please download the starter project from
https://www.appcoda.com/resources/swiftui5/SwiftUIToDoListUISearchBar.zip. Once
downloaded, compile the project to ensure it functions properly. The app should display
a search bar; however, this bar is currently implemented using UIKit. Our goal is to
convert it into a search bar view built entirely using SwiftUI.

Open the SearchBar.swift file, which is the file we will be focusing on. We will rewrite the
entire code while keeping the struct name intact. We will still refer to it as SearchBar ,
which will still accept a binding of search text as an argument. From the perspective of
the caller (i.e., ContentView), there is no need to make any changes. The usage will
remain the same, as follows:

SearchBar(text: $searchText)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 548


Now, let's begin with the UI implementation. If you want to challenge yourself, stop
reading here and try to implement the search bar UI on your own. This UI is quite
simple. It's composed of a text field, a couple of icons, and the cancel button.

If you have no idea how the UI is built, let's create it together. Replace the SearchBar

struct in SearchBar.swift like this:

struct SearchBar: View {


@Binding var text: String

@State private var isEditing = false

var body: some View {


HStack {

TextField("Search ...", text: $text)


.padding(7)
.padding(.horizontal, 25)
.background(Color(.systemGray6))
.cornerRadius(8)
.overlay(
HStack {
Image(systemName: "magnifyingglass")
.foregroundStyle(.gray)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .l
eading)
.padding(.leading, 8)

if isEditing {
Button(action: {
self.text = ""
}) {
Image(systemName: "multiply.circle.fill")
.foregroundStyle(.gray)
.padding(.trailing, 8)
}
}
}
)
.padding(.horizontal, 10)
.onTapGesture {

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 549


withAnimation {
self.isEditing = true
}
}

if isEditing {
Button(action: {
self.isEditing = false
self.text = ""

}) {
Text("Cancel")
}
.padding(.trailing, 10)
.transition(.move(edge: .trailing))
}
}
}
}

First, we declare two variables: one for the binding of the search text and another for
storing the state of the search field (whether it is being edited or not).

We utilize a HStack to arrange the text field and the Cancel button. The text field is
adorned with an overlay of a magnifying glass icon and the cross icon (represented by
multiply.circle.fill ), which is only displayed when the search field is in editing mode.
Similarly, the Cancel button appears when the user taps the search field.

In order to preview the search bar, we have already added the following preview code:

#Preview {
SearchBar(text: .constant(""))
}

In the preview canvas, you should be able to preview the search field. When you select
the text field, the Cancel button should appear.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 550


Figure 2. Previewing the search bar

What's more is that the search bar already works! Switch over to ContentView.swift and
perform a search in the preview canvas. It should filter the result based on the search
term.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 551


Figure 3. The search bar is already functional

Dismissing the Keyboard


As you can see, it's not hard to create our own search bar entirely using SwiftUI. While
the search bar is working, there is a minor issue that needs to be addressed. Have you
tried tapping the cancel button? It does clear the search field. However, the software
keyboard remains visible.

To resolve the issue, we need to add a line of code in the action block of the Cancel
button in SearchBar.swift :

// Dismiss the keyboard


UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: n
il, from: nil, for: nil)

In the provided code, we invoke the sendAction method to resign the first responder and
dismiss the keyboard. You can now run the app using a simulator. When you tap the
cancel button, the search field should be cleared, and the software keyboard should be
dismissed.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 552


Working with Custom Binding
The SwiftUI version of search bar already functions properly, but I want to take this
opportunity to discuss custom binding with you. In SearchBar.swift , we declare a
binding of the search text like this:

@Binding var text: String

The current implementation works well for our needs. However, let's consider a scenario
where we require additional logic when reading or writing to this binding. For instance,
how would we capitalize each word in the search field?

Swift offers a built-in feature for capitalizing a string. You can utilize the capitalized

property of the text to retrieve the capitalized version of the string. The question now
arises: how do we update the binding of the text property?

In this case, you will need to create a custom binding in SearchBar.swift like this:

private var searchText: Binding<String> {

return Binding<String>(
get: {
self.text.capitalized

}, set: {
self.text = $0
}
)
}

In the provided code snippet, we create a custom binding named searchText using
closures to handle the reading (get) and writing (set) of the binding value. For the get

part, we customize the value of the text binding by accessing the capitalized property.
This allows us to capitalize each word that the user types in the search field. As for the

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 553


set part, we currently leave it unchanged and set it to the original value. However, if
you require additional logic when setting the binding, you can modify the code within the
set closure.

As a side note, you can omit the return keyword and write the binding like this:

private var searchText: Binding<String> {

Binding<String>(
get: {
self.text.capitalized

}, set: {
self.text = $0
}
)
}

This is a notable feature that was introduced in Swift 5.1, in case you were not aware of it.

We still need to pass the text binding to the TextField . However, before the custom
binding change takes effect, we need to make one more modification. Update the
parameter in the TextField and ensure that you pass searchText as the binding.

TextField("Search ...", text: searchText)

Now run the app on a simulator. Type a few words into the search field. The app should
automatically capitalize each word as you type.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 554


Figure 4. The search field automatically capitalizes each word as you type

Summary
In this chapter, we have presented an alternative approach to implementing a search bar.
As you can see, it's not difficult to build one entirely using SwiftUI. You have also
acquired knowledge on how to create a custom binding. This skill is highly valuable when
you require the inclusion of additional program logic during the process of setting or
retrieving the binding value.

For reference, you can download the complete search bar project here:

Demo project
(https://www.appcoda.com/resources/swiftui5/SwiftUIToDoListSearchBarView.zip
)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 555


Chapter 25
Putting Everything Together to Build
a Personal Finance App
By now, you should have a good understanding of SwiftUI and have built some simple
apps using this new framework. In this chapter, you will use what you have learned so far
to develop a personal finance app, enabling users to track their income and expenses.

Figure 1. The Personal Finance App

This app is not too complicated to build but you will learn quite a lot about SwiftUI and
understand how to apply the techniques you learned in developing this real-world app. In
brief, here are some of the topics we will cover:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 556


1. How to build a form and perform validation
2. How to filter records and refresh the list view
3. How to use bottom sheet to display record details
4. How to use MVVM (Model-View-ViewModel) in SwiftUI
5. How to save and manage data in a database using Core Data
6. How to use DatePicker for date selection
7. How to handle keyboard notification and adjust the form position

Let me stress this once again. This app is the result of what you have learned so far.
Therefore, I assume you have already read the book from chapter 1 to chapter 24. You
should understand how a bottom sheet is built (chapter 18), how form validation with
Combine works (chapter 14 & 15), and how to persist data using SwiftData (chapter 22).
If you haven't read these chapters, I suggest you go read them first. In this chapter, we
will primarily focus on techniques that haven't been discussed before.

Downloading the Complete Project


Normally, we build a demo app from scratch. However, this time is a bit different. I have
already built the Personal Finance app for you. You can download the full source code of
the project from https://www.appcoda.com/resources/swiftui5/SwiftUIPFinance.zip to
take a look. Unzip the project and run the app on a simulator to try it out. When the app
is first launched, it will appear different from the one shown in Figure 1 because there are
no records yet. You can tap the "+" button to add a new record. After returning to the
main view, you will see the new record in the Recent Transactions section. Additionally,
the total balance will be automatically calculated.

The app uses SwiftData for data management. The records are persisted locally in the
built-in database, so you should see the records even after restarting the app.

Throughout the rest of this chapter, I will explain how the code works in detail. However,
I encourage you to take a look at the code first to assess your understanding.

Understanding the Model

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 557


As you can see in the project navigator, the app is broken into three main parts: model,
view model and view. Let's begin with the model layer. Open the PaymentActivity.swift

file to take a look:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 558


import SwiftData

enum PaymentCategory: Int {


case income = 0
case expense = 1
}

@Model class PaymentActivity {


@Attribute(.unique) var paymentId: UUID
var date: Date
var name: String
var address: String?
var amount: Double
var memo: String?
@Transient var type: PaymentCategory {
get {
PaymentCategory(rawValue: Int(typeNum)) ?? .expense
}

set {
self.typeNum = Int(newValue.rawValue)
}
}
@Attribute(originalName: "type") var typeNum: PaymentCategory.RawValue

init(paymentId: UUID = UUID(), date: Date, name: String, address: String? = nil
, amount: Double, memo: String? = nil, type: PaymentCategory) {
self.paymentId = paymentId
self.date = date
self.name = name
self.address = address
self.amount = amount
self.memo = memo
self.type = type
}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 559


The PaymentActivity class represents a payment record which can either be an expense or
income. In the code above, we use an enum to differentiate the payment types. Each
payment has the following properties:

paymentId - an unique ID for the payment record


date - the date of the transaction
name - the name of the transaction
address - where you spend / where the income comes from
amount - the amount of the transaction
memo - additional notes for the payment
type - the payment type (income / expense)
typeNum - the raw value of the payment type

Since we use SwiftData to persist the payment activity, this PaymentActivity class is
annotated with the @Model macro. With this keyword, SwiftData automatically enables
persistence for the data class. The @Attribute annotation adds the uniqueness constraint
for the paymentId property. Again, if you don't understand SwiftData, please refer to
chapter 22.

The payment type (i.e. typeNum ), is saved as an integer in the database. Therefore, we
need a conversion between the integer and the actual enumeration. This is one approach
to save an enum in a persistent storage.

The Model Container


When the app starts, we set up the model container using the .modelContainer modifier
for storing the payment activities. Now, switch over to PFinanceApp.swift and check out
the code:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 560


import SwiftUI
import SwiftData

@main
struct PFinanceApp: App {
var body: some Scene {
WindowGroup {
DashboardView()
}
.modelContainer(for: PaymentActivity.self)
}
}

To drive the data management operations with SwiftData, we need to prepare the model
container which serves as the persistent backend. Later, in the SwiftUI views, we can
easily retrieve the model context from the environment for further operations.

Implementing the New Payment View


Now that we have completed the walkthrough of the model layer, let's move on to
implementing each of the views. The New Payment view is specifically designed for users
to create a new payment activity. Open the PaymentFormView.swift file to take a look. You
should be able to preview the input form.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 561


Figure 2. The Payment Form View

The Form Layout


Let me first walk you through how the form is laid out. It is always good practice to
extract common views to create a more generic version. Since most of the form fields are
very similar, we have created a generic text field called FormTextField to render the field
name and the placeholder using a VStack :

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 562


struct FormTextField: View {
let name: String
var placeHolder: String

@Binding var value: String

var body: some View {


VStack(alignment: .leading) {
Text(name.uppercased())
.font(.system(.subheadline, design: .rounded))
.fontWeight(.bold)
.foregroundStyle(.primary)

TextField(placeHolder, text: $value)


.font(.headline)
.foregroundStyle(.primary)
.padding()
.border(Color("Border"), width: 1.0)

}
}
}

Do you notice the two validation errors under the form title? Since these validation
messages have a similar format, we also create a generic view for this kind of message:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 563


struct ValidationErrorText: View {

var iconName = "info.circle"


var iconColor = Color(red: 251/255, green: 128/255, blue: 128/255)

var text = ""

var body: some View {


HStack {
Image(systemName: iconName)
.foregroundStyle(iconColor)
Text(text)
.font(.system(.body, design: .rounded))
.foregroundStyle(.secondary)

Spacer()
}
}
}

With these two common views created, laying out the form becomes straightforward. We
utilize a ScrollView along with a VStack to arrange the form fields. The validation error
messages are displayed only when an error is detected:

Group {
if !paymentFormViewModel.isNameValid {
ValidationErrorText(text: "Please enter the payment name")
}

if !paymentFormViewModel.isAmountValid {
ValidationErrorText(text: "Please enter a valid amount")
}

if !paymentFormViewModel.isMemoValid {
ValidationErrorText(text: "Your memo should not exceed 300 characters")
}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 564


The type field is a bit different because it's not a text field. The user can either select
income or expense. In this case, we created two buttons

VStack(alignment: .leading) {
Text("TYPE")
.font(.system(.subheadline, design: .rounded))
.fontWeight(.bold)
.foregroundStyle(.primary)
.padding(.vertical, 10)

HStack(spacing: 0) {
Button(action: {
self.paymentFormViewModel.type = .income
}) {
Text("Income")
.font(.headline)
.foregroundStyle(self.paymentFormViewModel.type == .income ? Color
.white : Color.primary)
}
.frame(minWidth: 0.0, maxWidth: .infinity)
.padding()
.background(self.paymentFormViewModel.type == .income ? Color("IncomeCard"
) : Color(.systemBackground))

Button(action: {
self.paymentFormViewModel.type = .expense
}) {
Text("Expense")
.font(.headline)
.foregroundStyle(self.paymentFormViewModel.type == .expense ? Color
.white : Color.primary)
}
.frame(minWidth: 0.0, maxWidth: .infinity)
.padding()
.background(self.paymentFormViewModel.type == .expense ? Color("ExpenseCar
d") : Color(.systemBackground))
}
.border(Color("Border"), width: 1.0)
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 565


The background color of the button varies depending on the type of the payment activity.

The date field is implemented using the DatePicker component. Using the DatePicker is
straightforward. You simply need to provide the label, the binding to the date value, and
the display components of the date.

struct FormDateField: View {


let name: String

@Binding var value: Date

var body: some View {


VStack(alignment: .leading) {
Text(name.uppercased())
.font(.system(.subheadline, design: .rounded))
.fontWeight(.bold)
.foregroundStyle(.primary)

DatePicker("", selection: $value, displayedComponents: .date)


.accentColor(.primary)
.padding(10)
.border(Color("Border"), width: 1.0)
.labelsHidden()
}
}
}

Since the release of iOS 14, the built-in DatePicker has been enhanced with improved UI
and additional styles. When you run the view and tap the date field, the app presents a
full calendar view, allowing users to select the desired date. The user interface is
significantly enhanced compared to the older version of the date picker.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 566


Figure 3. Tapping the date field shows you a full calendar

The memo field is not a text field but a text editor. When SwiftUI was first released, it
doesn't come with a multiline text field. To support multiline text editing, you will need
to tap into the UIKit framework and wrap UITextView into a SwiftUI component. Starting
from iOS 14, Swift introduced a new component called TextEditor for displaying and
editing long-form text. In PaymentFormView.swift , you should find the following struct:

The memo field is not a text field but a text editor. When SwiftUI was initially released, it
did not include a multiline text field. To enable multiline text editing, you need to
leverage the UIKit framework and wrap UITextView into a SwiftUI component. However,
starting from iOS 14, Swift introduced a new component called TextEditor specifically
designed for displaying and editing long-form text. In the PaymentFormView.swift file, you
should find the following struct:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 567


struct FormTextEditor: View {
let name: String
var height: CGFloat = 80.0

@Binding var value: String

var body: some View {


VStack(alignment: .leading) {
Text(name.uppercased())
.font(.system(.subheadline, design: .rounded))
.fontWeight(.bold)
.foregroundStyle(.primary)

TextEditor(text: $value)
.frame(minHeight: height)
.font(.headline)
.foregroundStyle(.primary)
.padding()
.border(Color("Border"), width: 1.0)
}
}
}

The usage of TextEditor is very similar to TextField . All you need is to pass it the
binding to a String variable. Like any other SwiftUI view, you can apply view modifiers to
style its appearance. This is how we created the Memo field for users to input long form
text.

At the end of the form is the Save button. By default, this button is disabled and only
becomes enabled when all the required fields are filled. We utilize the disabled modifier
to control the button's state.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 568


Button(action: {
save()
dismiss()
}) {
Text("Save")
.opacity(paymentFormViewModel.isFormInputValid ? 1.0 : 0.5)
.font(.headline)
.foregroundStyle(.white)
.padding()
.frame(minWidth: 0, maxWidth: .infinity)
.background(Color("IncomeCard"))
.cornerRadius(10)

}
.padding()
.disabled(!paymentFormViewModel.isFormInputValid)

When the button is tapped, it calls the save() method to permanently save the payment
activity into the database. Subsequently, it invokes the dismiss() method to dismiss the
view.

Form Validation
That's essentially how we layout the form UI. Now let's discuss how the form validation is
implemented. We followed the approach discussed in chapter 15 to perform form
validation using Combine. Here is what we have done:

1. Created a view model to represent the payment activity form.


2. Implemented form validation in the view model and published the validation results
using Combine.

We created a view model class to hold the values of the form fields. You can switch over
to PaymentFormViewModel.swift to view the code:

class PaymentFormViewModel: ObservableObject {

// Input
@Published var name = ""

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 569


@Published var location = ""
@Published var amount = ""
@Published var type = PaymentCategory.expense
@Published var date = Date.today
@Published var memo = ""

// Output
@Published var isNameValid = false
@Published var isAmountValid = true
@Published var isMemoValid = true
@Published var isFormInputValid = false

private var cancellableSet: Set<AnyCancellable> = []

init(paymentActivity: PaymentActivity?) {

self.name = paymentActivity?.name ?? ""


self.location = paymentActivity?.address ?? ""
self.amount = "\(paymentActivity?.amount ?? 0.0)"
self.memo = paymentActivity?.memo ?? ""
self.type = paymentActivity?.type ?? .expense
self.date = paymentActivity?.date ?? Date.today

$name
.receive(on: RunLoop.main)
.map { name in
return name.count > 0
}
.assign(to: \.isNameValid, on: self)
.store(in: &cancellableSet)

$amount
.receive(on: RunLoop.main)
.map { amount in
guard let validAmount = Double(amount) else {
return false
}
return validAmount > 0
}
.assign(to: \.isAmountValid, on: self)
.store(in: &cancellableSet)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 570


$memo
.receive(on: RunLoop.main)
.map { memo in
return memo.count < 300
}
.assign(to: \.isMemoValid, on: self)
.store(in: &cancellableSet)

Publishers.CombineLatest3($isNameValid, $isAmountValid, $isMemoValid)


.receive(on: RunLoop.main)
.map { (isNameValid, isAmountValid, isMemoValid) in
return isNameValid && isAmountValid && isMemoValid
}
.assign(to: \.isFormInputValid, on: self)
.store(in: &cancellableSet)
}

This class conforms to ObservableObject . All the properties are annotated with
@Published because we want to notify the subscribers whenever there is a value change
and perform the validation accordingly.

Whenever there are any changes to the form's input values, this view model executes the
validation code, updates the results, and notifies the subscribers.

So, who is the subscriber?

If you go back to PaymentFormView.swift , you should notice that we have declared a


variable named paymentFormViewModel with the @ObservedObject wrapper:

@ObservedObject private var paymentFormViewModel: PaymentFormViewModel

The PaymentFormView subscribes to the changes of the view model. When any of the
validation variables (e.g. isNameValid ) are updated, PaymentFormView will be notified and
the view itself will refresh to display the validation error on screen.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 571


if !paymentFormViewModel.isNameValid {
ValidationErrorText(text: "Please enter the payment name")
}

Form Initialization
Do you notice the initialization method? It accepts a PaymentActivity object and
initializes the view model.

var payment: PaymentActivity?

init(payment: PaymentActivity? = nil) {


self.payment = payment
self.paymentFormViewModel = PaymentFormViewModel(paymentActivity: payment)
}

The PaymentFormView allows the user to create a new payment activity or edit an existing
one. This is why the init method takes in an optional payment activity object. If the
object is nil , an empty form is displayed. Otherwise, the form fields are filled with the
values from the given PaymentActivity object.

Implementing the Payment Activity Detail View


Now let's move on to the next view and discuss how the payment activity detail view is
implemented. This view is activated when a user selects one of the payment activities in
the Recent Transactions section. It displays the details of the activity, such as the amount
and location. You can open PaymentDetailView.swift to see what the UI looks like. This
will give you a better understanding of the detail view.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 572


Figure 4. The payment activity detail view

The User Interface


The detail view is quite simple. I believe you know how to layout the components, so I
will not explain the code line by line. One thing I want to highlight is the following lines
of code:

let payment: PaymentActivity

private let viewModel: PaymentDetailViewModel

init(payment: PaymentActivity) {

self.payment = payment
self.viewModel = PaymentDetailViewModel(payment: payment)
}

Since we need to perform some initialization to create the view model, we implement a
custom init method.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 573


The View Model
Instead of putting everything in a single view, we can separate a view into two
components: the view and its view model. The view itself is responsible for the UI
layout, while the view model holds the state and data to be displayed in the view.
The view model also handles the data validation and conversion. For experienced
developers, we are applying a well known design pattern called MVVM (short for
Model-View-ViewModel).

- See Building a Registration Form with Combine and View Model (Chapter 15)

To separate the actual view data from the view UI, we have created a view model named
PaymentDetailViewModel :

private let viewModel: PaymentDetailViewModel

Why do we need to create an additional view model to hold the view's data? Let's take a
look at the icon right next to the title Payment Details. This is a dynamic icon that
changes based on the payment type. Additionally, notice the formatting of the amount. A
requirement for our app is to format the amount with only two decimal places. We could
implement all these logic in the view itself, but as we keep adding more and more logic to
the view, it will become overly complex and difficult to maintain.

One well-known computer programming principle is the Single Responsibility Principle


(SRP). It states that every class or module in a program should have the responsibility for
just a single piece of that program's functionality. Applying SRP is crucial for writing
good code, as it makes your code easier to maintain and read.

This is why we separate the view into two components:

1. PaymentDetailView is only responsible for the UI layout.


2. PaymentDetailViewModel is responsible for converting the view's data into the expected
presentation format.

Open PaymentDetailViewModel and take a look:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 574


struct PaymentDetailViewModel {

var payment: PaymentActivity

var name: String {


return payment.name
}

var date: String {


return payment.date.string()
}

var address: String {


return payment.address ?? ""
}

var typeIcon: String {

let icon: String

switch payment.type {
case .income: icon = "arrowtriangle.up.circle.fill"
case .expense: icon = "arrowtriangle.down.circle.fill"
}

return icon
}

var image: String = "payment-detail"

var amount: String {


let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.minimumFractionDigits = 2

let formattedValue = formatter.string(from: NSNumber(value: payment.amount


)) ?? ""

let formattedAmount = ((payment.type == .income) ? "+" : "-") + "$" + form


attedValue

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 575


return formattedAmount
}

var memo: String {


return payment.memo ?? ""
}

init(payment: PaymentActivity) {
self.payment = payment
}

As you can see, we implement all the conversion logic in this view model. Can we put this
logic back into the view? Yes, of course. However, I believe the code is much cleaner
when breaking the view into two parts.

Walking Through the Dashboard View


Now it's time to walk you through the dashboard view. Among all the views in the
personal finance app, this view is the most complicated one.

Figure 5. The dashboard view

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 576


The Menu Bar
Open Dashboard.swift and let's start with the menu bar:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 577


struct MenuBar<Content>: View where Content: View {
@State private var showPaymentForm = false

let modalContent: () -> Content

var body: some View {


ZStack(alignment: .trailing) {
HStack(alignment: .center) {
Spacer()

VStack(alignment: .center) {
Text(Date.today.string(with: "EEEE, MMM d, yyyy"))
.font(.caption)
.foregroundColor(.gray)
Text("Personal Finance")
.font(.title)
.fontWeight(.black)
}

Spacer()
}

Button(action: {
self.showPaymentForm = true
}) {
Image(systemName: "plus.circle")
.font(.title)
.foregroundColor(.primary)
}

.sheet(isPresented: self.$showPaymentForm, onDismiss: {


self.showPaymentForm = false
}) {
self.modalContent()
}
}

}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 578


The layout of the menu bar is simple. It displays the app's title, today's date, and a plus
button. This menu bar view is designed to accommodate any modal view (referred to as
modalContent ). When the plus button is tapped, the modal view will be presented. If you
are unsure about creating a generic view in SwiftUI, you can refer to chapter 17, which
covers building a generic draggable view.

Income, Expense and Total Balance


Next, we have three card views to show the total balance, income, and expenses. Here is
the code for the income card view:

struct IncomeCard: View {


var income = 0.0

var body: some View {

ZStack {
Rectangle()
.foregroundColor(Color("IncomeCard"))
.cornerRadius(15.0)

VStack {
Text("Income")
.font(.system(.title, design: .rounded))
.fontWeight(.black)
.foregroundColor(.white)
Text(NumberFormatter.currency(from: income))
.font(.system(.title, design: .rounded))
.fontWeight(.bold)
.foregroundColor(.white)
.minimumScaleFactor(0.1)
}
}
.frame(height: 150)

}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 579


We employ a ZStack to overlay the text on a colored rectangle for the layout. We apply a
similar technique to arrange both the TotalBalanceCard and ExpenseCard . Now, how do
we calculate the income, expense, and total balance? At the beginning of DashboardView ,
we declare three computed properties:

private var totalIncome: Double {


let total = paymentActivities
.filter {
$0.type == .income
}.reduce(0) {
$0 + $1.amount
}

return total
}

private var totalExpense: Double {


let total = paymentActivities
.filter {
$0.type == .expense
}.reduce(0) {
$0 + $1.amount
}

return total
}

private var totalBalance: Double {


return totalIncome - totalExpense
}

The paymentActivities variable holds the collection of payment activities. To calculate the
total income, we utilize the filter function to filter the activities with type .income , and
then employ the reduce function to compute the cumulative amount. The same
technique is applied to calculate the total expense. Higher-order functions in Swift are
incredibly valuable. If you are unfamiliar with how to use filter and reduce, you can refer
to this tutorial: https://www.appcoda.com/higher-order-functions-swift/.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 580


Recent Transactions
The last part of the UI is the list of recent transactions. As all the rows share the same
layout (except the icon of the payment type), we create a generic view for the transaction
row like this:

struct TransactionCellView: View {

var transaction: PaymentActivity

var body: some View {

HStack(spacing: 20) {

Image(systemName: transaction.type == .income ? "arrowtriangle.up.circ


le.fill" : "arrowtriangle.down.circle.fill")
.font(.title)
.foregroundColor(Color(transaction.type == .income ? "IncomeCard"
: "ExpenseCard"))

VStack(alignment: .leading) {
Text(transaction.name)
.font(.system(.body, design: .rounded))
Text(transaction.date.string())
.font(.system(.caption, design: .rounded))
.foregroundColor(.gray)
}

Spacer()

Text((transaction.type == .income ? "+" : "-") + NumberFormatter.curre


ncy(from: transaction.amount))
.font(.system(.headline, design: .rounded))

}
.padding(.vertical, 5)

}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 581


This cell view takes in a PaymentActivity object and then presents its content. To list the
transaction, we use ForEach to loop through the payment activities and create a
TransactionCellView for each activity:

ForEach(paymentDataForView) { transaction in
TransactionCellView(transaction: transaction)
.onTapGesture {
self.showPaymentDetails = true
self.selectedPaymentActivity = transaction
}
.contextMenu {
Button(action: {
// Edit payment details
self.editPaymentDetails = true
self.selectedPaymentActivity = transaction

}) {
HStack {
Text("Edit")
Image(systemName: "pencil")
}
}

Button(action: {
// Delete the selected payment
self.delete(payment: transaction)
}) {
HStack {
Text("Delete")
Image(systemName: "trash")
}
}
}
}
.sheet(isPresented: self.$editPaymentDetails) {
PaymentFormView(payment: self.selectedPaymentActivity)
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 582


When a user taps and holds a row, it displays a context menu with both the delete and
edit options. When selecting the edit option, the app will create the PaymentFormView with
the selected payment activity. For the delete operation, the app will completely remove
the activity from the database using SwiftData.

Figure 6. The context menu for the payment activity row

Do you notice the paymentDataForView variable? Instead of using paymentActivities , the


list view presents items stored in paymentDataForView . Why is that?

In the Recent Transactions section, the app provides three options for the user to filter
the payment activities including all, income, and expense. For example, if the expense
option is selected, the app only shows those activities related to expenses.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 583


private var paymentDataForView: [PaymentActivity] {

switch listType {
case .all:
return paymentActivities
.sorted(by: { $0.date.compare($1.date) == .orderedDescending })
case .income:
return paymentActivities
.filter { $0.type == .income }
.sorted(by: { $0.date.compare($1.date) == .orderedDescending })
case .expense:
return paymentActivities
.filter { $0.type == .expense }
.sorted(by: { $0.date.compare($1.date) == .orderedDescending })
}
}

The paymentDataForView is another computed property that returns a collection of


payment activities matching the list type. In the code, we apply the filter function to
filter the payment activities and then use the sort function to arrange the activities in
reverse chronological order.

The Bottom Sheet


The payment activity detail view is presented as an overlay using a BottomSheet . When a
user taps on a payment record, the app displays the bottom sheet and shows the payment
details. This bottom sheet is expandable, allowing the user to drag the detail view
upwards to expand it. Conversely, the user can drag the view downwards to dismiss it.

.sheet(isPresented: $showPaymentDetails) {
PaymentDetailView(payment: selectedPaymentActivity!)
.presentationDetents([.medium, .large])
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 584


We have previously implemented a similar bottom sheet in chapter 18 utilizing the
.presentationDetents modifier. If you wish to delve deeper into the construction of the
BottomSheet , I recommend revisiting the chapter. Here, we define the sheet as an
expandable bottom sheet that begins with a medium size. However, the user can drag the
sheet upwards to expand it to a large size.

Managing Payment Activities with SwiftData


As mentioned before, all the payment activities are saved in the local database and
managed using SwiftData. In the code, we use the @Query property wrapper to fetch the
payment activities like this:

@Query var paymentActivities: [PaymentActivity]

SwiftData makes it very easy to perform a fetch request. Once the payment activities are
retrieved, SwiftUI will automatically update the list views or any other views.

Deleting an activity from the database is also very straightforward. We call the delete

function of the context and pass it with the activity object to remove:

private func delete(payment: PaymentActivity) {


self.modelContext.delete(payment)
}

Adding a new activity happens in the PaymentFormView . If you look at the


PaymentFormView.swift file again, you should find the save() function:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 585


private func save() {
let newPayment = PaymentActivity(
date: paymentFormViewModel.date,
name: paymentFormViewModel.name,
address: paymentFormViewModel.location,
amount: Double(paymentFormViewModel.amount)!,
memo: paymentFormViewModel.memo,
type: paymentFormViewModel.type)

modelContext.insert(newPayment)
}

To add a payment activity to the database, we simply call the insert function of the
model context.

Exploring the Extensions


For the sake of convenience, we have created two extensions for formatting dates and
numbers. In the project navigator, you will find two files located under the Extension
folder. Let's begin by examining the Date+Ext.swift file:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 586


extension Date {
static var today: Date {
return Date()
}

static var yesterday: Date {


return Calendar.current.date(byAdding: .day, value: -1, to: Date())!
}

static var tomorrow: Date {


return Calendar.current.date(byAdding: .day, value: 1, to: Date())!
}

var month: Int {


return Calendar.current.component(.month, from: self)
}

static func fromString(string: String, with format: String = "yyyy-MM-dd") ->


Date? {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = format
return dateFormatter.date(from: string)
}

func string(with format: String = "dd MMM yyyy") -> String {


let dateFormatter = DateFormatter()
dateFormatter.dateFormat = format
return dateFormatter.string(from: self)
}
}

In the code above, we extend Date to provide additional functionality including:

Get today's date


Get tomorrow's date
Get yesterday's date
Get the month of the date
Convert the current date to a string or vice versa

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 587


For formatting the amount, we extend NumberFormatter to provide an additional function:

extension NumberFormatter {
static func currency(from value: Double) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal

let formattedValue = formatter.string(from: NSNumber(value: value)) ?? ""

return "$" + formattedValue


}
}

This function takes in a value, converts it to a string and prepends it with the dollar sign
($).

Handling the Software Keyboard


In the PaymentFormView.swift file, we added the following modifier:

.keyboardAdaptive()

This is a custom view modifier, developed for handling the software keyboard. For iOS
14, this modifier is no longer required but I intentionally added it because you may need
it if your app supports iOS 13.

On iOS 13, the software keyboard blocks parts of the form when it's brought up without
applying the modifier. For example, if you try to tap the memo field, it's completely
hidden behind the keyboard. Conversely, if you attach the modifier to the scroll view, the
form will move up automatically when the keyboard appears. On iOS 14, the mobile
operating system itself automatically handles the appearance of the software keyboard,
preventing it from blocking the input field.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 588


Figure 7. Without using keyboardAdaptive (left), Using keyboardAdaptive

Now let's check out the code ( KeyboardAdaptive.swift ) and see how we handle keyboard
events:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 589


struct KeyboardAdaptive: ViewModifier {

@State var currentHeight: CGFloat = 0

func body(content: Content) -> some View {


content
.padding(.bottom, currentHeight)
.onAppear(perform: handleKeyboardEvents)
}

private func handleKeyboardEvents() {

NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNoti
fication
).compactMap { (notification) in
notification.userInfo?["UIKeyboardFrameEndUserInfoKey"] as? CGRect
}.map { rect in
rect.height
}.subscribe(Subscribers.Assign(object: self, keyPath: \.currentHeight))

NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNoti
fication
).compactMap { _ in
CGFloat.zero
}.subscribe(Subscribers.Assign(object: self, keyPath: \.currentHeight))

}
}

extension View {
func keyboardAdaptive() -> some View {
ModifiedContent(content: self, modifier: KeyboardAdaptive())
}
}

Whenever the keyboard appears (or disappears), iOS sends a notification to the app:

keyboardWillShowNotification - this notification is sent when the keyboard is about


to appear
keyboardWillHideNotification - this notification is sent when the keyboard is going

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 590


to disappear

So, how do we utilize these notifications to scroll up the form? When the app receives the
keyboardWillShowNotification, it adds padding to the form to move it up. Conversely,
we set the padding to zero when the keyboardWillHideNotification is received.

In the provided code, there is a state variable that stores the height of the keyboard. By
utilizing the Combine framework, we have a publisher that captures the
keyboardWillShowNotification and emits the current height of the keyboard.
Additionally, we have another publisher that listens to the keyboardWillHideNotification
and emits a value of zero. For both publishers, we employ the built-in assign subscriber
to assign the emitted value to the currentHeight variable.

This is how we detect the appearance of the keyboard, capture its height, and add the
bottom padding. However, why do we need to have the View extension?

The code works without the extension. You write the code like this to detect the keyboard
events:

.modifier(KeyboardAdaptive())

To make the code cleaner, we create the extension and add the keyboardAdaptive()

function. After that, we can attach the modifier to any view like this:

.keyboardAdaptive()

Since this view modifier is only applicable to iOS 13, we use the #available check to
verify the OS version in the keyboardAdaptive() function:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 591


extension View {
func keyboardAdaptive() -> some View {
if #available(iOS 14.0, *) {
return AnyView(self)
} else {
return AnyView(ModifiedContent(content: self, modifier: KeyboardAdapti
ve()))
}
}
}

Summary
This is how we built the personal finance app from scratch. Most of the techniques we
employed should not be unfamiliar to you. By combining what you have learned in the
earlier chapters, you were able to develop this app.

SwiftUI is an incredibly powerful and promising framework that enables you to build the
same app with less code compared to UIKit. If you have prior programming experience
with UIKit, you are aware that it would require more time and lines of code to create the
personal finance app. I genuinely hope you enjoy learning SwiftUI and constructing user
interfaces with this innovative framework.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 592


Chapter 26
Creating an App Store like Animated
View Transition
You have likely used Apple's built-in App Store app. In the Today section, it presents
users with headlines, various articles, and app recommendations. What interests many of
us is the animated view transition. As shown in Figure 1, the articles are listed in a card-
like format. When tapped, the card pops out to reveal the full content. To dismiss the
article view and return to the list view, simply tap the close button. If you are unsure
about what I mean, the best way to understand these views is to open the App Store app
on your iPhone and try it out.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 593


Figure 1. Apple's App Store app

In this chapter, we will construct a list view and implement an animated transition using
SwiftUI that is similar to the one we discussed earlier. Specifically, you will learn the
following techniques:

How to use GeometryReader to detect screen sizes


How to create a variable-sized card view
How to implement an animated view transition similar to the one found in the App
Store

Let's get started!

Introducing the Demo App

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 594


As usual, we will build a demo app together. The app looks similar to the App Store app,
but without the tab bar. It only features a list view that displays all the articles in card
format. When a user taps on any of the articles, the card expands to full screen and
displays the article details. To return to the list view, the user can either tap the close
button or drag down the article view to collapse it.

Figure 2. The demo app

We will build the app from scratch. But to save you time from typing some of the code, I
have prepared a starter project for you. You can download it from
https://www.appcoda.com/resources/swiftui5/SwiftUIAppStoreStarter.zip. After
downloading the project, unzip it and open SwiftUIAppStore.xcodeproj to take a look.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 595


Figure 3. The starter project

The starter projects comes with the following implementation:

1. It already bundles the required images in the asset catalog.


2. The ContentView.swift file is the default SwiftUI view generated by Xcode.
3. The Article.swift file contains the Article struct, which represents an article in
the app. For testing purposes, this file also creates the sampleArticles array which
includes some test data. You may modify its content if you want to change the article
data.

Understanding the Card View


You have learned how to create a card-like UI before. This card view is similar to the one
implemented in chapter 5, but it will be more flexible to support scrollable content. In
other words, it has two modes: excerpt and full content. In excerpt mode, it only displays
the image, category, headline, and sub-headline of the article. As its name suggests, the
full content mode will display the article details as shown in Figure 2.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 596


Figure 4. The sample card views

If you look a bit closer into the card views shown in figure 4, you will find that the size of
card views varies according to the height of the image. However, the height of the card
will not exceed 500 points.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 597


Figure 5. The components of a card view in excerpt mode

Let's also examine how the card view looks in full content mode. As shown in the figure
below, the card view expands to full screen and displays the content. Additionally, the
image is slightly larger, and the sub-headline is hidden. Furthermore, the close button
appears on the screen for users to dismiss the view. Please note that this is a scrollable
view.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 598


Figure 6. The components of a card view in full content mode

Implementing the Card View


Now that you understand the requirements of this card view, let's see how to create it. We
will use a separate file for implementing the card view. In the project navigator, right
click the View folder and choose New file.... Select the SwiftUI View template and name
the file ArticleCardView.swift .

First, let's begin with the excerpt view, which is the view overlayed on top of the image
(see figure 5). Insert the following code in the file:

struct ArticleExcerptView: View {

let category: String

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 599


let headline: String
let subHeadline: String

@Binding var isShowContent: Bool

var body: some View {


VStack(alignment: .leading) {
Spacer()

Rectangle()
.frame(minHeight: 100, maxHeight: 150)
.overlay(
HStack {
VStack(alignment: .leading) {
Text(self.category.uppercased())
.font(.subheadline)
.fontWeight(.bold)
.foregroundColor(Color.secondary)

Text(self.headline)
.font(.title)
.fontWeight(.bold)
.foregroundStyle(Color.primary)
.minimumScaleFactor(0.1)
.lineLimit(2)
.padding(.bottom, 5)

if !self.isShowContent {
Text(self.subHeadline)
.font(.subheadline)
.foregroundStyle(Color.secondary)
.minimumScaleFactor(0.1)
.lineLimit(3)

}
}
.padding()

Spacer()
}
)
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 600


.foregroundStyle(.white)

}
}

The ArticleExcerptView should be flexible enough to support different content.


Therefore, we define the variables above. As previously mentioned, the card view should
be able to switch between excerpt and full content modes. This binding variable is
declared to control the display of the content. When its value is set to false, the view is in
excerpt mode. Conversely, it's in full content mode when true. The sub-headline is
displayed only when the value of isShowContent is set to true .

There are various ways to layout the excerpt view. In the code above, we create a
Rectangle view and overlay it with the headline and sub-headline. You should be familiar
with most of the modifiers attached to the Text view, but the minimumScaleFactor

modifier is worth mentioning. By applying the modifier, the system automatically shrinks
the font size of the text to fit the available space. For example, if the headline contains too
much text, iOS will scale it down to 10% of its original size before truncating it.

Previewing the UI
To preview the excerpt view, you can modify the preview code like this:

#Preview("Article Excerpt View with subheadline", traits: .fixedLayout(width: 380,


height: 500)) {
ArticleExcerptView(category: sampleArticles[0].category, headline: sampleArtic
les[0].headline, subHeadline: sampleArticles[0].subHeadline, isShowContent: .const
ant(false))
}

#Preview("Article Excerpt View", traits: .fixedLayout(width: 380, height: 500)) {


ArticleExcerptView(category: sampleArticles[0].category, headline: sampleArtic
les[0].headline, subHeadline: sampleArticles[0].subHeadline, isShowContent: .const
ant(true))
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 601


Here, we instantiate two excerpt views, one with the isShowContent binding set to false

and the other set to true . The sampleArticles array is the test data that comes with the
starter project.

Instead of previewing using a device, we preview the UI in a fixed-size rectangle. If


everything works perfectly, you should see the excerpt view in the preview canvas. Please
ensure that you change to the Selectable mode to preview the fixed layout.

Figure 7. Previewing the excerpt view

With the excerpt view ready, let's implement the article card view. Update the
ArticleCardView struct like this:

struct ArticleCardView: View {

let category: String


let headline: String
let subHeadline: String
let image: UIImage
var content: String = ""

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 602


@Binding var isShowContent: Bool

var body: some View {


ScrollView {
VStack(alignment: .leading) {
Image(uiImage: self.image)
.resizable()
.scaledToFill()
.frame(height: min(self.image.size.height/3, 500))
.border(Color(.sRGB, red: 150/255, green: 150/255, blue: 150/2
55, opacity: 0.1), width: self.isShowContent ? 0 : 1)
.cornerRadius(15)
.overlay(
ArticleExcerptView(category: self.category, headline: self
.headline, subHeadline: self.subHeadline, isShowContent: self.$isShowContent)
.cornerRadius(self.isShowContent ? 0 : 15)
)

// Content
if self.isShowContent {
Text(self.content)
.foregroundStyle(Color(.darkGray))
.font(.system(.body, design: .rounded))
.padding(.horizontal)
.padding(.bottom, 50)
.transition(.move(edge: .bottom))
}
}
}
.shadow(color: Color(.sRGB, red: 64/255, green: 64/255, blue: 64/255, opac
ity: 0.3), radius: self.isShowContent ? 0 : 15)
}
}

To arrange the layout of the card view, we overlay the ArticleExcerptView on top of an
Image view. The image view is set to .scaledToFill , with the height not exceeding 500
points. The content is only displayed when the isShowContent binding is set to true .

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 603


To make the view scrollable, we embed the VStack in a vertical scroll view. The shadow

modifier is used to add a shadow to the card view.

To preview the article card view, you can insert the following code:

ArticleCardView(category: sampleArticles[0].category, headline: sampleArticles[0].


headline, subHeadline: sampleArticles[0].subHeadline, image: sampleArticles[0].ima
ge, content: sampleArticles[0].content, isShowContent: .constant(false))
.previewDisplayName("Card View (no Content)")

ArticleCardView(category: sampleArticles[0].category, headline: sampleArticles[0].


headline, subHeadline: sampleArticles[0].subHeadline, image: sampleArticles[0].ima
ge, content: sampleArticles[0].content, isShowContent: .constant(true))
.previewDisplayName("Card View (with Content)")

Once you have made the changes, you should be able to see the card UI in the preview
canvas. Additionally, you should see two simulators such that one displays the excerpt
view and the other displays the full content.

Figure 8. Previewing the article card view

Using GeometryReader

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 604


It seems everything works great. But if you try to preview the card view with another
sample article (say, sampleArticles[1] ), the UI doesn't look good. Both the featured
image and the content go beyond the screen edge.

Figure 9. The card view doesn't fit the content

Let's look at our code again. For the Image view, we only limited the height of the image,
we don't have any limits on its width:

.frame(height: min(self.image.size.height/3, 500))

To resolve the issue, we need to set the frame's width and ensure it doesn't exceed the
screen's width. The question is, how do we determine the screen width? SwiftUI provides
a container view called GeometryReader that allows you to access the size of its parent
view. Therefore, we need to embed the ScrollView within a GeometryReader , like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 605


var body: some View {
GeometryReader { geometry in
ScrollView {
VStack(alignment: .leading) {
.
.
.
}
}
.shadow(color: Color(.sRGB, red: 64/255, green: 64/255, blue: 64/255, opac
ity: 0.3), radius: self.isShowContent ? 0 : 15)
}
}

Within the closure of GeometryReader , it has a parameter that provides you with extra
information about the view such as size and position. So, to limit the width of the frame
to the size of the screen, you can modify the .frame modifier like this:

.frame(width: geometry.size.width, height: min(self.image.size.height/3, 500))

In the code, we set the width equal to that of the screen. Once you complete the change,
the card view should look great.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 606


Figure 10. The width of the image is now equal to that of the screen

Adding the close button


The card view is almost complete, but there is still one thing left to implement: the close
button. To overlay the button on top of the image, we will embed the scroll view in a
ZStack . You can modify the code directly to add the ZStack , or another way to do it is to
hold the control key and click on ScrollView . You should then see a context menu.
Choose Embed in ZStack to embed the scroll view in a ZStack .

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 607


Figure 11. Embed the scroll view in a ZStack

Xcode will automatically indent the code and embed the scroll view in the ZStack . Now
change ZStack to set its alignment to .topTrailing because we want to place the close
button near the top-right corner. Your code should look like this:

var body: some View {


GeometryReader { geometry in
ZStack(alignment: .topTrailing) {
ScrollView {
VStack(alignment: .leading) {
.
.
.
}
}
.shadow(color: Color(.sRGB, red: 64/255, green: 64/255, blue: 64/255,
opacity: 0.3), radius: self.isShowContent ? 0 : 15)
}
}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 608


Next, insert the following code right below the .shadow modifier to add the close button:

if self.isShowContent {
HStack {
Spacer()

Button {
withAnimation(.easeInOut) {
self.isShowContent = false
}
} label: {
Image(systemName: "xmark.circle.fill")
.font(.system(size: 26))
.foregroundStyle(Color.white)
.opacity(0.7)
}
}
.padding(.top, 50)
.padding(.trailing)
}

After the modification, the preview should display the close button when the value of
isShowContent is set to true .

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 609


Figure 12. Adding the close button

Building the List View


Now that we've implemented the layout of the card view, let's switch over to
ContentView.swift and create the list view. At the very top of the list view, is the top bar
with a heading and a profile photo.

Figure 13. The top bar

I believe you should know how to create the layout by using VStack and HStack . To
better organize the code, I will create the top bar and the avatar in two separate structs.
Insert the following code in ContentView.swift :

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 610


struct TopBarView : View {

var body: some View {


HStack(alignment: .lastTextBaseline) {
VStack(alignment: .leading) {
Text(getCurrentDate().uppercased())
.font(.caption)
.foregroundStyle(Color.secondary)
Text("Today")
.font(.largeTitle)
.fontWeight(.heavy)
}

Spacer()

AvatarView(image: "profile", width: 40, height: 40)

}
}

func getCurrentDate(with format: String = "EEEE, MMM d") -> String {


let dateFormatter = DateFormatter()
dateFormatter.dateFormat = format
return dateFormatter.string(from: Date())
}
}

struct AvatarView: View {


let image: String
let width: CGFloat
let height: CGFloat

var body: some View {


Image(image)
.resizable()
.frame(width: width, height: height)
.clipShape(Circle())
.overlay(Circle().stroke(Color.gray, lineWidth: 1))
}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 611


Next, update the code of ContentView like this:

struct ContentView: View {

var body: some View {


ScrollView {
VStack(spacing: 40) {

TopBarView()
.padding(.horizontal, 20)

ForEach(sampleArticles.indices, id: \.self) { index in

ArticleCardView(category: sampleArticles[index].category, head


line: sampleArticles[index].headline, subHeadline: sampleArticles[index].subHeadli
ne, image: sampleArticles[index].image, content: sampleArticles[index].content, is
ShowContent: .constant(false))

.padding(.horizontal, 20)
.frame(height: min(sampleArticles[index].image.size.height/
3, 500))
}
}
}
}
}

We embed a VStack in a ScrollView to create the vertical scroll view. In the code block,
we loop through all the sampleArticles using ForEach and create an ArticleCardView for
each article. If your code works properly, the preview canvas should show you a list of
articles.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 612


Figure 14. The list view showing a list of card views

Expanding the Card View to Full Screen Using


MatchedGeometryEffect
Now comes the challenging part: how do we switch the card view from excerpt mode to
full content mode? Right now, we set the isShowContent parameter to .constant(false) .
To switch between these two modes, we need to have a variable to keep track of the view's
state.

Therefore, declare the following state variable in ContentView :

@State private var showContent = false

By default, all card views are in the excerpt state. Thus, the value of the showContents

variable is set to false . Later, when a card is tapped, we will change the state from
false to true .

We also need a variable to keep track of the index of the selected card. Declare one more
state variable:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 613


@State private var selectedArticleIndex: Int?

It's defined as an optional because no card view is initially selected.

Now, modify the initialization of ArticleCardView . Instead of using .constant(false) ,


pass it the binding of the state variable (i.e. $showContent ):

ArticleCardView(category: sampleArticles[index].category, headline: sampleArticles


[index].headline, subHeadline: sampleArticles[index].subHeadline, image: sampleArt
icles[index].image, content: sampleArticles[index].content, isShowContent: $showCo
ntent)

Handling the Tap Gesture


When the user taps one of the card views, the selected card should be changed to full
screen mode. To capture the tap gesture, attach the .onTapGesture modifier to
ArticleCardView and place it below .padding(.horizontal, 20) :

.onTapGesture {
withAnimation(.interactiveSpring(response: 0.35, dampingFraction: 0.65, blendD
uration: 0.1)) {
self.selectedArticleIndex = index
self.showContent.toggle()
}
}

When a tap gesture is detected, we change the showContent variable from false to
true . At the same time, we save the index of the selected card view.

Let's perform a quick test to see how the app functions after making the changes. When
you run the app in the preview canvas, tap any of the card views to see the result.
Although it may not work as expected, the card view should display the content of the
article and hide the sub-headline. Additionally, you should be able to tap the close button
to return to excerpt mode. If you can't see the content, drag up the card view to reveal it.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 614


Figure 15. Testing the tap gesture

Animating the Transition using


MatchedGeometryEffect
How can we expand the selected card view to a full-screen view and animate the
transition? In Chapter 33, I introduced a modifier called matchedGeometryEffect . With this
powerful modifier, you can describe the appearance of the initial view and the final view.
matchedGeometryEffect then computes the difference between these two views and
automatically animates the size and position changes.

Note: If you haven't read chapter 33, please go through the chapter first.

In this demo, the initial view is the card view in excerpt mode, while the final view is the
card view showing the full content. What we are going to do is embed the current scroll
view in a ZStack view. Initially, the app displays the list of card views. When the user
taps any of the card views, we overlay the full content view on top of the existing scroll
view.

Now hold the command key and click ScrollView . Choose Embed in ZStack.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 615


Figure 16. Embedding the scroll view in a ZStack view

Set the alignment parameter of the ZStack view to .top like this:

ZStack(alignment: .top) {

ScrollView {
.
.
.
}

Next, insert the following code after the closing bracket of the scroll view:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 616


if showContent,
let selectedArticleIndex {
ArticleCardView(category: sampleArticles[selectedArticleIndex].category, headl
ine: sampleArticles[selectedArticleIndex].headline, subHeadline: sampleArticles[se
lectedArticleIndex].subHeadline, image: sampleArticles[selectedArticleIndex].image
, content: sampleArticles[selectedArticleIndex].content, isShowContent: $showConte
nt)
.ignoresSafeArea()
}

When a user taps one of the card views, the value of showContent is changed to true and
selectedArticleIndex is set to the index of the selected card view. In this case, we display
the card view in full content mode by setting the isShowContent parameter to true .

If you test the app in the preview pane, tapping a card view will expand its content to full
screen.

Figure 17. Expanding the card view into full screen

It works but the result doesn't look good. The list behind the full-content card view is still
visible. We need to hide it away when any of the card views is selected. To fix the issue,
attach a opacity modifier to ScrollView :

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 617


.opacity(showContent ? 0 : 1)

When the app is showing full content of a card view, we set the opacity of the scroll view
to 0 . Test the app again. The card view should display full content properly.

Figure 18. The app hides the list view when a card is selected

The last thing we need to do is to animate the transition. As explained at the beginning of
this section, we can make use of the matchedGeometryEffect modifier to let SwiftUI render
the transition animation.

To use the modifier, we first have to define a namespace variable:

@Namespace var nsArticle

Next, attach the matchedGeometryEffect modifier to the ArticleCardView in the ForEach

loop:

.matchedGeometryEffect(id: index, in: nsArticle)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 618


You can place the line of code above before the onTapGesture modifier. For the
ArticleCardView , attach another matchedGeometryEffect modifier and use the same
namespace (insert it above the ignoresSafeArea modifier):

.matchedGeometryEffect(id: selectedArticleIndex, in: nsArticle)

The matchedGeometryEffect modifier works in pairs. By doing the implementation above,


SwiftUI automatically computes the view transition animation.

Enlarging the Image


We haven't finished the implementation yet. Next up is the featured image. In full
content mode, I want to make the image a bit larger. This is an easy fix. Just switch over
to ArticleCardView.swift and change the .frame modifier of the Image view like this:

.frame(width: geometry.size.width, height: self.isShowContent ? geometry.size.heig


ht * 0.7 : min(self.image.size.height/3, 500))

When the card view is displaying the article content, the height of the image is now
adjusted to 70% of the screen height. You may alter the value to suit your preference.
Now go back to ContentView.swift and test the change. The featured image becomes
larger in full content mode.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 619


Figure 19. The featured image becomes larger in full content mode

Run the app in the simulator or in the preview canvas. You will see a slick animation
when the card view expands to full screen.

Summary
Congratulations! You have built an App Store-like animation using SwiftUI. After
implementing this demo project, I hope you understand how to create complex view
animations.

Animation is an essential part of UI design these days. As you can see, SwiftUI has made
it very easy for developers to build beautiful animations and screen transitions. In your
next app project, don't forget to apply the techniques you learned in this chapter to
improve the user experience of your app.

For reference, you can download the complete project here:

Demo project (https://www.appcoda.com/resources/swiftui5/SwiftUIAppStore.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 620


Chapter 27
Building an Image Carousel
The carousel is a common UI pattern found in most mobile and web applications. It is
also referred to as an image slider or rotator. Regardless of its name, the carousel is
intended to display a set of data within a finite screen space. For instance, an image
carousel may showcase a single image from its collection, accompanied by a navigation
control that suggests additional content. Users can swipe the screen to navigate through
the image set, as is the case with Instagram's presentation of multiple images. Similar
carousels can also be found in many other iOS apps, such as Apple's Music and App
Store.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 621


Figure 1. Sample carousel in the Music, App Store, and Instagram app

This chapter will teach you how to build an image carousel entirely in SwiftUI. There are
several methods to implement a carousel, such as integrating with the UIKit component
UIPageViewController to create it. However, we will explore an alternative approach and
construct the carousel entirely in SwiftUI.

Let's get started.

Introducing the Travel Demo App


As with the other chapters, I will guide you through the implementation by building a
demo app. This app showcases a collection of travel destinations in the form of a
carousel. To browse through the trips, users can swipe right to view the subsequent
destination or swipe left to check out the previous trip. To make this demo app more
engaging, users can tap on a destination to view its details. In addition to implementing a

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 622


carousel, you will also learn animation techniques that can be applied to your own apps.
Figure 2 shows sample screenshots of the demo app. To see it in action, you can check
out the video at https://link.appcoda.com/carousel-demo.

Figure 2. The demo app

To save you time from building the app from scratch and to focus on developing the
carousel, I've created a starter project for you. Please download it from
https://www.appcoda.com/resources/swiftui5/SwiftUICarouselStarter.zip and unzip the
package.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 623


Figure 3. The starter project

The starter project comes with the following features:

1. It bundles the required images in the asset catalog.


2. The ContentView.swift file is the default SwiftUI view generated by Xcode.
3. The Trip.swift file contains the Trip struct, which represents a travel destination
in the app. For testing purposes, this file also creates the sampleTrips array which
includes some test data. You may modify its content.
4. The TripCardView.swift file implements the UI of a card view. Each card view is
designed to display the destination's image. The isShowDetails binding controls the
appearance of the text label. When isShowDetails is set to true, the label will be
hidden.

The ScrollView Problem


So, how would you implement the carousel in SwiftUI? Your initial thought may be to
create it using a scroll view. Probably you will write the code in ContentView.swift like
this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 624


struct ContentView: View {

@State private var isCardTapped = false

var body: some View {


GeometryReader { outerView in
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .center) {
ForEach(sampleTrips.indices, id: \.self) { index in
GeometryReader { innerView in
TripCardView(destination: sampleTrips[index].destinati
on, imageName: sampleTrips[index].image, isShowDetails: self.$isCardTapped)
}
.padding(.horizontal, 20)
.frame(width: outerView.size.width, height: 450)
}
}
}
.frame(width: outerView.size.width, height: outerView.size.height, ali
gnment: .leading)
}
}
}

The code above embeds an HStack with a horizontal ScrollView to create the image
slider. Within the HStack , we loop through the sampleTrips array and create a
TripCardView for each trip. To have better control over the card size, we use two
GeometryReader s: outerView and innerView. The outer view represents the size of the
device's screen, while the inner view wraps around the card view to control its size. If you
haven't read the previous chapter and do not understand what GeometryReader is, please
refer to Chapter 26.

This looks simple, right? If you run the code in the preview canvas, it should result in a
horizontal scroll view. You can swipe the screen to scroll through all the card views.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 625


Figure 4. Testing the horizontal scroll view

Have we completed the carousel? Not quite. There are a couple of major issues to
address:

1. The current implementation does not support paging. In other words, users can
swipe the screen to continuously scroll through the content, and the scroll view can
stop at any location. For instance, take a look at Figure 4. The scroll stops between
two card views, which is not the desired behavior. We expect the scrolling to stop on
paging boundaries of the content view.
2. When a card view is tapped, we need to determine its index and display its details in
a separate view. The problem is that it is not easy to determine which card view the
user has tapped with the current implementation.

Both issues are related to the built-in ScrollView . The UIKit version of the scroll view
supports paging, but Apple did not include that feature in the SwiftUI framework until
iOS 17. To address this issue, we need to build our own horizontal scroll view with paging
support.

At first, you may think that developing our own scroll view is challenging. However, in
reality, it is not that difficult. If you understand the usage of HStack and DragGesture ,
you can build a horizontal scroll view with paging support.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 626


Building a Carousel with HStack and DragGesture
The idea is to lay out all the card views (i.e., trips) in a horizontal stack ( HStack ). The
HStack should be long enough to accommodate all the card views but only display a
single card view at a time. By default, the horizontal stack is non-scrollable. Therefore, we
need to attach a drag gesture recognizer to the stack view and handle the drag ourselves.
Figure 5 illustrates our implementation of the horizontal scroll view.

Figure 5. Understanding how to create a horizontal scroll view using HStack and
DragGesture

Implementing the horizontal stack


Now let's see how we turn this idea into code. Please bear with me in that you will need to
update the code several times. I want to show you the implementation step by step. Open
Content.swift and update the body like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 627


var body: some View {
HStack {
ForEach(sampleTrips.indices, id: \.self) { index in
TripCardView(destination: sampleTrips[index].destination, imageName: s
ampleTrips[index].image, isShowDetails: self.$isCardTapped)
}
}
}

In the code above, we start by laying out all card views within an HStack . By default, the
horizontal stack tries to fit all the card views within the available screen space. You
should see something similar to Figure 6 in the preview canvas.

Figure 6. Squeezing all card views to fit the screen

Obviously, this isn't the horizontal stack we want to build. We expect each card view to
takes up the width of the screen. To do so, we have to wrap the HStack in a
GeometryReader to retrieve the screen size. Update the code in the body like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 628


var body: some View {
GeometryReader { outerView in
HStack {
ForEach(sampleTrips.indices, id: \.self) { index in
GeometryReader { innerView in
TripCardView(destination: sampleTrips[index].destination, imag
eName: sampleTrips[index].image, isShowDetails: self.$isCardTapped)
}
.frame(width: outerView.size.width, height: 500)
}
}
.frame(width: outerView.size.width, height: outerView.size.height)
}
}

The outerView parameter provides us with the screen width and height, while the
innerView parameter allows us to have better control over the size and position of the
card view.

In the code above, we attach the .frame modifier to the card view and set its width to the
screen width (i.e., outerView.size.width ). This ensures that each card view takes up the
entire screen width. For the height of the card view, we set it to 500 points to make it
slightly smaller. After making the changes, you should see the card view displaying the
"London" image.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 629


Figure 7. The horizontal stack now shows only a single card view

Why the "London" card view? If you switch to the Selectable mode, the preview canvas
should display something similar to what is shown in Figure 8. We have 13 items in the
sampleTrips array. Since each card view has a width equal to the screen width, the
horizontal stack view has to expand beyond the screen. It happens that the "London"
card view is the center (7th) item of the array. This is why you see the "London" card
view.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 630


Figure 8. The horizontal stack view has 13 card views

Changing the alignment


So, how can we display the first item of the array instead of the center (7th) item? The
trick is to attach a .frame modifier to the HStack with the alignment set to .leading like
this:

.frame(width: outerView.size.width, height: outerView.size.height, alignment: .lea


ding)

The default alignment is set to .center . This is why the 7th element of the horizontal
view is shown on screen. Once you change the alignment to .leading , you should see the
first element.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 631


Figure 9. The horizontal stack view shows the first card view

If you want to understand how the alignment affects the horizontal stack view, you can
change its value to .center or .trailing to see its effect. Figure 10 shows what the stack
view looks likes with different alignment settings.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 632


Figure 10. The horizontal stack view with different alignment settings

Did you notice the gap between each of the card views? This is also related to the default
setting of HStack . To eliminate the spacing, you can update the HStack and set its
spacing to zero, like this:

HStack(spacing: 0)

Adding padding
Optionally, you can add horizontal padding to the image. I think this will make the card
view look better. Insert the following line of code and attach it to the GeometryReader that
wraps the card view (before .frame(width: outerView.size.width, height: 500) ):

.padding(.horizontal, self.isCardTapped ? 0 : 20)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 633


While it's too early for us to talk about the implementation of the detailed view, we added
a condition for the padding. The horizontal padding will be removed when the user taps
the card view.

Figure 11. Adding the horizontal padding

Moving the HStack Card by Card


Now that we have created a horizontal stack that defaults to showing the first card view,
the next question is, how do we move the stack to display a particular card?

It's just simple math! The card view's width is equal to the width of the screen. Suppose
the screen width is 300 points, and we want to display the third card view. In that case,
we can shift the horizontal stack to the left by 600 points (300 x 2). Figure 12 shows the
result.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 634


Figure 12. Moving the stack view to the left

To translate the description above into code, we first declare a state variable to keep track
of the index of the visible card view:

@State private var currentTripIndex = 2

By default, I want to display the third card view. This is why I set the currentTripIndex

variable to 2. You can change it to other values.

To move the horizontal stack to the left, we can attach the .offset modifier to the
HStack like this:

.offset(x: -CGFloat(self.currentTripIndex) * outerView.size.width)

The outerView 's width is actually the width of the screen. To display the third card view,
as explained before, we need to move the stack by 2x the screen width. This is why we
multiply the currentTripIndex with the outerView 's width. A negative value for the
horizontal offset will shift the stack view to the left.

Once you have made the change, you should see the "Amsterdam" card view in your
preview canvas.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 635


Figure 13. The stack now shows the Amsterdam card view

Adding the Drag Gesture


With the current implementation, we can change the visible card view by altering the
value of currentTripIndex . Remember, the horizontal stack does not allow users to drag
the view. This is what we are going to implement in this section. I assume you already
understand how gestures work in SwiftUI. If you do not understand gestures or
@GestureState , please read Chapter 17 first.

The drag gesture of the horizontal stack is expected to work like this:

1. The user can tap the image and start dragging.


2. The drag can be in both directions.
3. When the drag's distance exceeds a certain threshold, the horizontal stack will move
to the next or previous card view depending on the drag direction.
4. Otherwise, the stack view returns to the original position and displays the current
card view.

To translate the description above into code, we first declare a variable to hold the drag
offset:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 636


@GestureState private var dragOffset: CGFloat = 0

Next, we attach the .gesture modifier to the HStack and initialize a DragGesture like
this:

.gesture(
!self.isCardTapped ?

DragGesture()
.updating(self.$dragOffset, body: { (value, state, transaction) in
state = value.translation.width
})
.onEnded({ (value) in
let threshold = outerView.size.width * 0.65
var newIndex = Int(-value.translation.width / threshold) + self.curren
tTripIndex
newIndex = min(max(newIndex, 0), sampleTrips.count - 1)

self.currentTripIndex = newIndex
})

: nil
)

As you drag the horizontal stack, the updating function is called. We save the horizontal
drag distance to the dragOffset variable. When the drag ends, we check if the drag
distance exceeds the threshold, which is set to 65% of the screen width, and compute the
new index. Once we have the newIndex computed, we verify if it is within the range of the
sampleTrips array. Lastly, we assign the value of newIndex to currentTripIndex . SwiftUI
will then update the UI and display the corresponding card view automatically.

Please take note that we have a condition for enabling the drag gesture. When the card
view is tapped, there is no gesture recognizer.

To move the stack view during the drag, we have to make one more change. Attach an
additional .offset modifier to the HStack (right after the previous .offset) like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 637


.offset(x: self.dragOffset)

Here, we update the horizontal offset of the stack view to the drag offset. Now you are
ready to test the changes. Run the app in a simulator or in the preview canvas. You
should be able to drag the stack view. When your drag exceeds the threshold, the stack
view shows you the next trip.

Figure 14. Dragging the horizontal stack view

Animating the Card Transition


To improve the user experience, I want to add a nice animation when the app moves from
one card view to another. First, modify the following line of code from:

.frame(width: outerView.size.width, height: 500)

To:

.frame(width: outerView.size.width, height: self.currentTripIndex == index ? (self


.isCardTapped ? outerView.size.height : 450) : 400)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 638


By updating the code, we make the visible card view a little bit larger than the rest. On
top of that, attach the .opacity modifier to the card view like this:

.opacity(self.currentTripIndex == index ? 1.0 : 0.7)

Other than the card view's height, we also want to set a different opacity value for the
visible and invisible card views. All these changes are not animated yet. Now insert the
following line of code to the outer view's GeometryReader:

.animation(.interpolatingSpring(.bouncy), value: dragOffset)

SwiftUI will then animate the size and opacity changes of the card views automatically.
Run the app in the preview canvas to test out the changes. This is how we implement a
scroll view with HStack and add paging support.

Figure 15. Adding the card transition animation

Adding the Title


Now that we have built the image carousel, wouldn't it be great if we implement the detail
view to make the demo app more complete? Let's start by adding a title for the app.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 639


Command-click the GeometryReader of the outer view and choose embed in ZStack.

Figure 16. Embed the outer view in a VStack

Next, insert the following code at the beginning of ZStack :

VStack(alignment: .leading) {
Text("Discover")
.font(.system(.largeTitle, design: .rounded))
.fontWeight(.black)

Text("Explore your next destination")


.font(.system(.headline, design: .rounded))
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, align
ment: .topLeading)
.padding(.top, 25)
.padding(.leading, 20)
.opacity(self.isCardTapped ? 0.1 : 1.0)
.offset(y: self.isCardTapped ? -100 : 0)

The code above is self-explanatory, but I would like to highlight two lines of code. Both
.opacity and .offset are optional. The purpose of the .opacity modifier is to hide the
title when the card is tapped. The change to the vertical offset will add a nice touch to the
user experience.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 640


Figure 17. Adding the title bar

Exercise: Working on the Detail View


Let's begin the implementation of the detail view with an exercise. I assume you have
some experience with SwiftUI and should be able to create the detail view shown in
figure 18. You can create a separate file named TripDetailView.swift and write the code
there.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 641


Figure 18. The detail view of a trip

To keep things simple, the rating and description are just dummy data. The same goes for
the Book Now button, which is not functional. This detail view only takes in a destination
like this:

struct TripDetailView: View {


let destination: String

var body: some View {


.
.
.
}
}

Please take some time to create the detail view. I will walk you through my solution in a
later section.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 642


Implementing the Trip Detail View
Were you able to develop the detail view? I hope you tried to complete the exercise. Let
me show you my solution. First, create a new file named TripDetailView.swift using the
SwiftUI View template.

Next, replace the TripDetailView struct like this:

struct TripDetailView: View {


let destination: String

var body: some View {


GeometryReader { geometry in
ScrollView {
ZStack {
VStack(alignment: .leading, spacing: 5) {

VStack(alignment: .leading, spacing: 5) {


Text(self.destination)
.font(.system(.title, design: .rounded))
.fontWeight(.heavy)

HStack(spacing: 3) {
ForEach(1...5, id: \.self) { _ in
Image(systemName: "star.fill")
.foregroundStyle(.yellow)
.font(.system(size: 15))
}

Text("5.0")
.font(.system(.headline))
.padding(.leading, 10)
}

}
.padding(.bottom, 30)

Text("Description")
.font(.system(.headline))
.fontWeight(.medium)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 643


Text("Growing up in Michigan, I was lucky enough to experi
ence one part of the Great Lakes. And let me assure you, they are great. As a phot
ojournalist, I have had endless opportunities to travel the world and to see a var
iety of lakes as well as each of the major oceans. And let me tell you, you will b
e hard pressed to find water as beautiful as the Great Lakes.")
.padding(.bottom, 40)

Button(action: {
// tap me
}) {
Text("Book Now")
.font(.system(.headline, design: .rounded))
.fontWeight(.heavy)
.foregroundStyle(.white)
.padding()
.frame(minWidth: 0, maxWidth: .infinity)
.background(Color(red: 0.97, green: 0.369, blue: 0
.212))
.cornerRadius(20)
}
}
.padding()
.frame(width: geometry.size.width, height: geometry.size.heigh
t, alignment: .topLeading)
.background(.white)
.cornerRadius(15)

Image(systemName: "bookmark.fill")
.font(.system(size: 40))
.foregroundStyle(Color(red: 0.97, green: 0.369, blue: 0.212
))
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, max
Height: .infinity, alignment: .topTrailing)
.offset(x: -15, y: -5)
}
.offset(y: 15)
}
}
}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 644


Basically, we embed the whole content in a scroll view. Inside the scroll view, we use a
ZStack to layout the content and the bookmark image. Since the TripDetailView requires
a valid destination to work correctly, you need to update the preview code like this:

#Preview {
TripDetailView(destination: "London").background(.black)
}

I also changed the background color to black, so that we can see the rounded corners of
the detail view.

Figure 19. Previewing the detail view

Bringing up the Detail View


Now let's go back to ContentView.swift to bring up the detail view. When a user taps a
card view, we will bring up the detail view with an animated transition. Since the content
view has a ZStack that wraps its core content, it is straightforward for us to integrate with
the detail view.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 645


Insert the following code snippet in the ZStack :

if self.isCardTapped {
TripDetailView(destination: sampleTrips[currentTripIndex].destination)
.offset(y: 200)
.transition(.move(edge: .bottom))

Button(action: {
withAnimation {
self.isCardTapped = false
}
}) {
Image(systemName: "xmark.circle.fill")
.font(.system(size: 30))
.foregroundStyle(.black)
.opacity(0.7)
.contentShape(Rectangle())
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, a
lignment: .topTrailing)
.padding(.trailing)

The TripDetailView is only brought up when the card view is tapped. It is expected that
the detail view will appear from the bottom of the screen and move upward with an
animation. This is why we attach the .transition modifiers to the detail view. To let
users dismiss the detail view, we also add a close button, which appears at the top-right
corner of the screen. In case you are not sure where to insert the code above, please refer
to Figure 20.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 646


Figure 20. The code snippet for bringing up the detail view

The code won't work yet because we haven't captured the tap gesture. Thus, attach the
.onTapGesture function to the card view like this:

.onTapGesture {
withAnimation(.interpolatingSpring(.bouncy, initialVelocity: 0.3)) {
self.isCardTapped = true
}
}

When someone taps the card view, we simply change the isCardTapped state variable to
true . Run the app and tap any of the card views. The app should bring up the detail
view.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 647


Figure 21. The code snippet for bringing up the detail view

The detail view works! However, the animation doesn't work well. When the detail view
is brought up, the card view grows a little bit bigger, which is achieved by the following
line of code:

.frame(width: outerView.size.width, height: self.currentTripIndex == index ? (self


.isCardTapped ? outerView.size.height : 450) : 400)

To make the animation look better, let's move the image upward when the detail view
appears. Attach the .offset modifier to TripCardView :

.offset(y: self.isCardTapped ? -innerView.size.height * 0.3 : 0)

I set the vertical offset to 30% of the card view's height. You are free to change the value.
Now run the app again and you should see a more slick animation.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 648


Figure 22. Adding an offset modifier to TripCardView

Summary
Great! You have built a custom scroll view with paging support and learned how to bring
up a detail view with an animated transition. This technique is not limited to an image
carousel. In fact, you can modify the code to create a set of onboarding screens. I hope
you have enjoyed what you learned in this chapter and will apply it to your next app
project.

For reference, you can download the complete carousel project here:

Demo project (https://www.appcoda.com/resources/swiftui5/SwiftUICarousel.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 649


Chapter 28
Building an Expandable List View
Using OutlineGroup
The SwiftUI list is very similar to UITableView in UIKit. In the first release of SwiftUI,
Apple's engineers made list view construction a breeze. You do not need to create a
prototype cell, and there is no delegate/data source protocol. With just a few lines of
code, you can build a list view with custom cells. Starting from iOS 14, Apple continued to
improve the List view and introduced several new features. In this chapter, we will
show you how to build an expandable list/outline view and explore the inset grouped list
style.

The Demo App


First, let's take a look at the final deliverable. I'm a big fan of La Marzocco, so I used the
navigation menu on its website as an example. The list view below shows an outline of
the menu. Users can tap the disclosure button to expand the list.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 650


Figure 1. The expandable list view

Of course, you can build this outline view using your own implementation. Starting from
iOS 14, Apple made it simpler for developers to build this kind of outline view, which
automatically works on iOS, iPadOS, and macOS.

Creating the Expandable List


To follow this chapter, please download these image assets from
https://www.appcoda.com/resources/swiftui/expandablelist-images.zip. Then, create a
new SwiftUI project using the App template. I named the project
SwiftUIExpandableList, but you are free to set the name to whatever you want.

Once the project is created, unzip the image archive and add the images to the asset
catalog.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 651


In the project navigator, right click SwiftUIExpandableList and choose to create a new
file. Select the Swift File template and name it MenuItem.swift.

Setting up the data model


To make the list view expandable, all you need to do is create a data model like this.
Insert the following code in the file:

struct MenuItem: Identifiable {


var id = UUID()
var name: String
var image: String
var subMenuItems: [MenuItem]?
}

In the code above, we have a struct that models a menu item. The key to making a nested
list is to include a property that contains an optional array of child menu items (i.e.
subMenuItems ). Note that the children are of the same type (MenuItem) as their parent.

For the top level menu items, we create an array of MenuItem in the same file like this:

// Main menu items


let sampleMenuItems = [ MenuItem(name: "Espresso Machines", image: "linea-mini", s
ubMenuItems: espressoMachineMenuItems),
MenuItem(name: "Grinders", image: "swift-mini", subMenuIte
ms: grinderMenuItems),
MenuItem(name: "Other Equipment", image: "espresso-ep", su
bMenuItems: otherMenuItems)
]

For each of the menu item, we specify the array of the sub-menu items. If there are no
sub-menu items, you can omit the subMenuItems parameter or pass it a nil value. We
define the sub-menu items like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 652


// Sub-menu items for Espressco Machines
let espressoMachineMenuItems = [ MenuItem(name: "Leva", image: "leva-x", subMenuIt
ems: [ MenuItem(name: "Leva X", image: "leva-x"), MenuItem(name: "Leva S", image:
"leva-s") ]),
MenuItem(name: "Strada", image: "strada-ep", subM
enuItems: [ MenuItem(name: "Strada EP", image: "strada-ep"), MenuItem(name: "Strad
a AV", image: "strada-av"), MenuItem(name: "Strada MP", image: "strada-mp"), MenuI
tem(name: "Strada EE", image: "strada-ee") ]),
MenuItem(name: "KB90", image: "kb90"),
MenuItem(name: "Linea", image: "linea-pb-x", subM
enuItems: [ MenuItem(name: "Linea PB X", image: "linea-pb-x"), MenuItem(name: "Lin
ea PB", image: "linea-pb"), MenuItem(name: "Linea Classic", image: "linea-classic"
) ]),
MenuItem(name: "GB5", image: "gb5"),
MenuItem(name: "Home", image: "gs3", subMenuItems
: [ MenuItem(name: "GS3", image: "gs3"), MenuItem(name: "Linea Mini", image: "line
a-mini") ])
]

// Sub-menu items for Grinder


let grinderMenuItems = [ MenuItem(name: "Swift", image: "swift"),
MenuItem(name: "Vulcano", image: "vulcano"),
MenuItem(name: "Swift Mini", image: "swift-mini"),
MenuItem(name: "Lux D", image: "lux-d")
]

// Sub-menu items for other equipment


let otherMenuItems = [ MenuItem(name: "Espresso AV", image: "espresso-av"),
MenuItem(name: "Espresso EP", image: "espresso-ep"),
MenuItem(name: "Pour Over", image: "pourover"),
MenuItem(name: "Steam", image: "steam")
]

Presenting the List


With the data model prepared, we can now create the list view. The List view has an
optional children parameter. If you have any sub items, you can provide their key path.
SwiftUI will then look up the sub menu items recursively and present them in outline
form. Open ContentView.swift and insert the following code in body :

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 653


List(sampleMenuItems, children: \.subMenuItems) { item in
HStack {
Image(item.image)
.resizable()
.scaledToFit()
.frame(width: 50, height: 50)

Text(item.name)
.font(.system(.title3, design: .rounded))
.bold()
}
}

In the closure of the List view, you describe how each row looks. In the code above, we
layout an image and a text description using HStack . If you've added the code in
ContentView correctly, SwiftUI should render the outline view as shown in figure 2.

Figure 2. The expandable list view

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 654


To test the app, run it in a simulator or the preview canvas. You can tap the disclosure
indicator to access the submenu.

Using the Plain List Style


Apple sets the default style of the list view to Inset Grouped, where the grouped sections
are inset with rounded corners. If you want to switch it back to the plain list style, you
can attach the .listStyle modifier to the List view and set its value to .plain like this:

List {
...
}
.listStyle(.plain)

If you've followed me, the list view should now change to the plain style.

Figure 3. Using inset grouped list style

Using OutlineGroup to Customize the Expandable List

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 655


As you can see in the earlier example, it is quite easy to create an outline view using the
List view. However, if you want to have better control over the appearance of the
outline view (e.g., adding a section header), you will need to use OutlineGroup . This view
is for presenting a hierarchy of data.

If you understand how to build an expandable list view, the usage of OutlineGroup is very
similar. For example, the following code allows you to build the same expandable list
view as shown in Figure 1:

List {
OutlineGroup(sampleMenuItems, children: \.subMenuItems) { item in
HStack {
Image(item.image)
.resizable()
.scaledToFit()
.frame(width: 50, height: 50)

Text(item.name)
.font(.system(.title3, design: .rounded))
.bold()
}
}
}

Similar to the List view, you just need to pass OutlineGroup the array of items and
specify the key path for the sub menu items (or children).

With OutlineGroup , you have better control on the appearance of the outline view. For
example, we want to display the top-level menu items as the section header. You can
write the code like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 656


List {
ForEach(sampleMenuItems) { menuItem in

Section(header:
HStack {

Text(menuItem.name)
.font(.title3)
.fontWeight(.heavy)

Image(menuItem.image)
.resizable()
.scaledToFit()
.frame(width: 30, height: 30)

}
.padding(.vertical)

) {
OutlineGroup(menuItem.subMenuItems ?? [MenuItem](), children: \.subMen
uItems) { item in
HStack {
Image(item.image)
.resizable()
.scaledToFit()
.frame(width: 50, height: 50)

Text(item.name)
.font(.system(.title3, design: .rounded))
.bold()
}
}
}
}
}

In the code above, we use ForEach to loop through the menu items. We present the top-
level items as section headers. For the rest of the submenu items, we rely on
OutlineGroup to create the hierarchy of data. If you have made the change in
ContentView.swift , you should see an outline view like the one shown in Figure 4.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 657


Figure 4. Building the outline view using OutlineGroup

Similarly, if you prefer to use the plain list style, you can attach the listStyle modifier to
the List view:

.listStyle(.plain)

Your preview should display an outline view like figure 5.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 658


Figure 5. Applying the inset grouped list style

Understanding DisclosureGroup
In the outline view, you can show/hide the submenu items by tapping the disclosure
indicator. Whether you use List or OutlineGroup to implement the expandable list, this
"expand & collapse" feature is supported by a new view called DisclosureGroup .

The disclosure group view is designed to show or hide another content view. While
DisclosureGroup is automatically embedded in OutlineGroup , you can use this view
independently. For example, you can use the following code to show & hide a question
and an answer:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 659


DisclosureGroup(
content: {
Text("Absolutely! You are allowed to reuse the source code in your own pro
jects (personal/commercial). However, you're not allowed to distribute or sell the
source code without prior authorization.")
.font(.body)
.fontWeight(.light)
},
label: {
Text("1. Can I reuse the source code?")
.font(.body)
.bold()
.foregroundColor(.black)
}
)

The disclosure group view takes in two parameters: label and content. In the code above,
we specify the question in the label parameter and the answer in the content

parameter. Figure 6 shows you the result.

Figure 6. Using DisclosureGroup for showing and hiding content

By default, the disclosure group view is in hidden mode. To reveal the content view, you
tap the disclosure indicator to switch the disclosure group view to the "expand" state.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 660


Optionally, you can control the state of DisclosureGroup by passing it a binding which
specifies the state of the disclosure indicator (expanded or collapsed) like this:

struct FaqView: View {


@State var showContent = true

var body: some View {


DisclosureGroup(
isExpanded: $showContent,
content: {
...
},
label: {
...
}
)
.padding()
}
}

Exercise
The DisclosureGroup view allows you to have finer control over the state of the disclosure
indicator. Your exercise is to create a FAQ screen similar to the one shown in figure 7.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 661


Figure 7. Your exercise

Users can tap the disclosure indicator to show or hide an individual question.
Additionally, the app provides a "Show All" button to expand all questions and reveal the
answers at once.

Summary
In this chapter, I have introduced a couple of new features of SwiftUI. As you can see in
the demo, it is effortless to build an outline view or expandable list view. All you need to
do is define a correct data model. The List view handles the rest, traverses the data
structure, and renders the outline view. On top of that, the new update provides
OutlineGroup and DisclosureGroup for you to further customize the outline view.

For reference, you can download the complete expandable list project here:

Demo project
(https://www.appcoda.com/resources/swiftui5/SwiftUIExpandableList.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 662


Please note that you can refer to FaqView.swift for the solution to the exercise.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 663


Chapter 29
Building Grid Layouts Using
LazyVGrid and LazyHGrid
The initial release of SwiftUI did not include a native collection view. You could either
create your own solution or use third-party libraries. In WWDC 2020, Apple introduced
several new features for the SwiftUI framework, including two new UI components called
LazyVGrid and LazyHGrid that address the need for grid views. LazyVGrid is for
creating vertical grids, while LazyHGrid is for horizontal grids. As Apple mentions, the
word 'Lazy' refers to the grid view not creating items until they are needed, which
optimizes the performance of these grid views by default.

In this chapter, I will guide you on how to create both horizontal and vertical views using
LazyVGrid and LazyHGrid. Both components are designed to be flexible, allowing
developers to create various types of grid layouts with ease. We will also explore how to
vary the size of grid items to achieve different layouts. Once we have covered the basics,
we will delve into creating more complex layouts, such as the one shown in Figure 1.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 664


Figure 1. Sample grid layouts

The Essential of Grid Layout in SwiftUI


To create a grid layout, whether it's horizontal or vertical, here are the steps you follow:

1. First, you need to prepare the raw data for presentation in the grid. For example,
here is an array of SF symbols that we are going to present in the demo app:

private var symbols = ["keyboard", "hifispeaker.fill", "printer.fill", "tv.fill",


"desktopcomputer", "headphones", "tv.music.note", "mic", "plus.bubble", "video"]

2. Create an array of type GridItem that describes what the grid will look like.
Including, how many columns the grid should have. Here is a code snippet for
describing a 3-column grid:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 665


private var threeColumnGrid = [GridItem(.flexible()), GridItem(.flexible()), GridI
tem(.flexible())]

3. Next, you layout the grid by using LazyVGrid and ScrollView . Here is an example:

ScrollView {
LazyVGrid(columns: threeColumnGrid) {
// Display the item
}
}

4. Alternatively, if you want to build a horizontal grid, you use LazyHGrid like this:

ScrollView(.horizontal) {
LazyHGrid(rows: threeColumnGrid) {
// Display the item
}
}

Using LazyVGrid to Create Vertical Grids


With a basic understanding of the grid layout, let's put the code to work. We will start
with something simple by building a 3-column grid. Open Xcode and create a new project
with the App template. Please make sure you select SwiftUI for the Interface option.
Name the project SwiftUIGridLayout or whatever name you prefer.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 666


Figure 2. Creating a new project using the App template

Once the project is created, choose ContentView.swift . In ContentView , declare the


following variables:

private var symbols = ["keyboard", "hifispeaker.fill", "printer.fill", "tv.fill",


"desktopcomputer", "headphones", "tv.music.note", "mic", "plus.bubble", "video"]

private var colors: [Color] = [.yellow, .purple, .green]

private var gridItemLayout = [GridItem(.flexible()), GridItem(.flexible()), GridIt


em(.flexible())]

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 667


We are going to display a set of SF symbols in a 3-column grid. To present the grid,
update the body variable like this:

var body: some View {


ScrollView {
LazyVGrid(columns: gridItemLayout, spacing: 20) {
ForEach((0...9999), id: \.self) {
Image(systemName: symbols[$0 % symbols.count])
.font(.system(size: 30))
.frame(width: 50, height: 50)
.background(colors[$0 % colors.count])
.cornerRadius(10)
}
}
}
}

We use LazyVGrid to create a vertical grid layout with three columns, and specify a 20-
point space between rows. In the code block, we use a ForEach loop to present a total of
10,000 image views. If you've made the changes correctly, you should see a three-column
grid in the preview.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 668


Figure 3. Displaying a 3-column grid

This is how we create a vertical grid with three columns. The size of each image is fixed at
50 by 50 points using the .frame modifier. If you want to make a grid item wider, you
can modify the frame modifier as follows:

.frame(minWidth: 0, maxWidth: .infinity, minHeight: 50)

The image's width will expand to take up the column's width like that shown in figure 4.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 669


Figure 4. Changing the frame size of the grid items

Note that there is a space between the columns and rows in the current grid layout.
Sometimes you may want to create a grid without any spaces. How can you achieve that?
The space between rows is controlled by the spacing parameter of LazyVGrid , which we
have set to 20 points. To remove the space between rows, simply change the value of the
spacing parameter to 0 .

The spacing between grid items is controlled by the instances of GridItem initialized in
gridItemLayout . You can set the spacing between items by passing a value to the spacing

parameter. Therefore, to remove the spacing between rows in our grid layout, initialize
the gridLayout variable as follows:

private var gridItemLayout = [GridItem(.flexible(), spacing: 0), GridItem(.flexibl


e(), spacing: 0), GridItem(.flexible(), spacing: 0)]

For each GridItem , we specify to use a spacing of zero. For simplicity, the code above can
be rewritten like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 670


private var gridItemLayout = Array(repeating: GridItem(.flexible(), spacing: 0), c
ount: 3)

If you've made the changes, your preview canvas should show you a grid view without
any spacing.

Figure 5. Removing the spacing between columns and rows

Using GridItem to Vary the Grid Layout


(Flexible/Fixed/Adaptive)
Let's take a closer look at GridItem . You use GridItem instances to configure the layout
of items in LazyHGrid and LazyVGrid views. Earlier, we defined an array of three
GridItem instances, each of which uses the .flexible() size type. This size type enables
you to create three columns with equal sizes. If you want to create a 6-column grid,
initialize the GridItem array as follows:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 671


private var sixColumnGrid: [GridItem] = Array(repeating: .init(.flexible()), count
: 6)

.flexible() is just one of the size types for controlling the grid layout. If you want to
place as many items as possible in a row, you can use the adaptive size type:

private var gridItemLayout = [GridItem(.adaptive(minimum: 50))]

The adaptive size type requires you to specify the minimum size for a grid item. In the
code above, each grid item has a minimum size of 50 points. If you modify the
gridItemLayout variable as shown above and set the spacing of LazyVGrid back to 20 ,
you should be able to achieve a grid layout similar to the one shown in figure 6.

Figure 6. Using adaptive size to create the grid

By using .adaptive(minimum: 50) , you instruct LazyVGrid to fill as many images as


possible in a row such that each item has a minimum size of 50 points.

Note: I used iPhone 13 Pro as the simulator. If you use other iOS simulators with
different screen sizes, you may achieve a different result.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 672


In addition to .flexible and .adaptive , you can also use .fixed if you want to create
fixed width columns. For example, you want to layout the image in two columns such
that the first column has a width of 100 points and the second one has a width of 150
points. You write the code like this:

In addition to using .flexible and .adaptive , you can also use .fixed to create fixed-
width columns. For example, if you want to create a grid layout with two columns where
the first column has a width of 100 points and the second one has a width of 150 points,
you can use the following code:

private var gridItemLayout = [GridItem(.fixed(100)), GridItem(.fixed(150))]

Update the gridItemLayout variable as shown above, this will result in a two-column grid
with different sizes.

Figure 7. A grid with fixed-size items

You are allowed to mix different size types to create more complex grid layouts. For
example, you can define a fixed size GridItem , followed by a GridItem with an adaptive
size like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 673


private var gridItemLayout = [GridItem(.fixed(150)), GridItem(.adaptive(minimum: 50
))]

In this case, LazyVGrid creates a fixed size column of 100 point width. And then, it tries
to fill as many items as possible within the remaining space.

Figure 8. Mixing a fixed-size item with adaptive size items

Using LazyHGrid to Create Horizontal Grids


Now that you've created a vertical grid, it's easy to use LazyHGrid to convert it into a
horizontal one. The usage of LazyHGrid is similar to that of LazyVGrid , except that you
need to embed it in a horizontal scroll view. Additionally, LazyHGrid takes a parameter
named rows instead of columns .

Therefore, to transform a grid view from vertical to horizontal orientation, you can
simply modify a few lines of code as follows:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 674


ScrollView(.horizontal) {
LazyHGrid(rows: gridItemLayout, spacing: 20) {
ForEach((0...9999), id: \.self) {
Image(systemName: symbols[$0 % symbols.count])
.font(.system(size: 30))
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 50, maxHeight:
.infinity)
.background(colors[$0 % colors.count])
.cornerRadius(10)
}
}
}

Run the demo in the preview or test it on a simulator. You should see a horizontal grid.

Figure 9. Creating a horizontal grid with LazyHGrid

Switching Between Different Grid Layouts


Now that you have some experience with LazyVGrid and LazyHGrid , let's create
something more complex. Imagine you are building a photo app that displays a collection
of coffee photos. The app provides a feature for users to change the layout. By default, it

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 675


shows the list of photos in a single column. However, the user can tap a Grid button to
switch to a grid view with 2 columns. Tapping the same button again will change the
layout to 3 columns, and then 4 columns.

Figure 10. Creating a horizontal grid with LazyHGrid

Create a new project for this demo app. Again, choose the App template and name the
project SwiftUIPhotoGrid. Next, download the image pack at
https://www.appcoda.com/resources/swiftui/coffeeimages.zip. Unzip the images and
add them to the asset catalog.

Before creating the grid view, we will create the data model for the collection of photos.
In the project navigator, right click SwiftUIPhotoGrid and choose New file... to create a
new file. Select the Swift File template and name the file Photo.swift.

Insert the following code in the Photo.swift file to create the Photo struct:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 676


struct Photo: Identifiable {
var id = UUID()
var name: String
}

let samplePhotos = (1...20).map { Photo(name: "coffee-\($0)") }

We have 20 coffee photos in the image pack, so we initialize an array of 20 Photo

instances. With the data model ready, let's switch over to ContentView.swift to build the
grid.

First, declare a gridLayout variable to define our preferred grid layout:

@State var gridLayout: [GridItem] = [ GridItem() ]

By default, we want to display the photos in a list view. Instead of using List , you can
use LazyVGrid to build a list view by defining the gridLayout with a single grid item.
When you tell LazyVGrid to use a single-column grid layout, it will arrange the items in a
list view format. Insert the following code in body to create the grid view:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 677


NavigationStack {
ScrollView {
LazyVGrid(columns: gridLayout, alignment: .center, spacing: 10) {

ForEach(samplePhotos.indices, id: \.self) { index in

Image(samplePhotos[index].name)
.resizable()
.scaledToFill()
.frame(minWidth: 0, maxWidth: .infinity)
.frame(height: 200)
.cornerRadius(10)
.shadow(color: Color.primary.opacity(0.3), radius: 1)

}
}
.padding(.all, 10)
}

.navigationTitle("Coffee Feed")
}

We use LazyVGrid to create a vertical grid with a spacing of 10 points between rows. The
grid is used to display coffee photos, so we use ForEach to loop through the samplePhotos

array. We embed the grid in a scroll view to make it scrollable and wrap it with a
navigation view. Once you have made the change, you should see a list of photos in the
preview canvas.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 678


Figure 11. Creating a list view with LazyVGrid

Now we need to a button for users to switch between different layouts. We will add the
button to the navigation bar. The SwiftUI framework has a modifier called .toolbar for
you to populate items within the navigation bar. Right after .navigationTitle , insert the
following code to create the bar button:

.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
self.gridLayout = Array(repeating: .init(.flexible()), count: self.gri
dLayout.count % 4 + 1)
} label: {
Image(systemName: "square.grid.2x2")
.font(.title)
}
.tint(.primary)
}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 679


In the code above, we update the gridLayout variable and initialize the array of
GridItem . Let's say the current item count is one, we will create an array of two
GridItem s to change to a 2-column grid. Since we've marked gridLayout as a state
variable, SwiftUI will render the grid view every time we update the variable.

Figure 12. Adding a bar button for switching the grid layout

You can run the app to have a quick test. Tapping the grid button will switch to another
grid layout.

There are a couple of things we want to improve. First, the height of the grid item should
be adjusted to 100 points for grids with two or more columns. Update the .frame

modifier with the height parameter like this:

.frame(height: gridLayout.count == 1 ? 200 : 100)

Second, when you switch from one grid layout to another, SwiftUI simply redraws the
grid view without any animation. Wouldn't it be great if we added a nice transition
between layout changes? To do that, you just add a single line of code. Insert the
following code after .padding(.all, 10) :

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 680


.animation(.interactiveSpring(), value: gridLayout.count)

This is the power of SwiftUI. By telling SwiftUI that you want to animate changes, the
framework handles the rest and you will see a nice transition between the layout changes.

Figure 13. SwiftUI automatically animates the transition

Building Grid Layout with Multiple Grids


You are not limited to using a single LazyVGrid or LazyHGrid in your app. By combining
multiple LazyVGrid and LazyHGrid views, you can create interesting layouts. Figure 14
shows an example of such a layout, which displays a list of cafe photos along with a list of
coffee photos under each cafe photo. When the device is in landscape orientation, the
cafe photo and the list of coffee photos are arranged side by side.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 681


Figure 14. Building complex grid layout with two grids

Let's go back to our Xcode project and create the data model first. The image pack you
downloaded earlier comes a set of cafe photos. So, create a new Swift file and name it
Cafe.swift. In the file, insert the following code:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 682


struct Cafe: Identifiable {
var id = UUID()
var image: String
var coffeePhotos: [Photo] = []
}

let sampleCafes: [Cafe] = {

var cafes = (1...18).map { Cafe(image: "cafe-\($0)") }

for index in cafes.indices {


let randomNumber = Int.random(in: (2...12))
cafes[index].coffeePhotos = (1...randomNumber).map { Photo(name: "coffee-\
($0)") }
}

return cafes
}()

The Cafe struct is self-explanatory, with an image property for storing the cafe photo
and a coffeePhotos property for storing a list of coffee photos. In the code above, we also
create an array of Cafe for demonstration purposes, randomly picking some coffee
photos for each cafe. Feel free to modify the code if you have other images you prefer.

Rather than modifying the ContentView.swift file, let's create a new file to implement this
grid view. Right-click on SwiftUIPhotoGrid and select New File.... Then, choose the
SwiftUI View template and name the file MultiGridView.

Similar to the earlier implementation, let's declare a gridLayout variable to store the
current grid layout:

@State var gridLayout = [ GridItem() ]

By default, our grid is initialized with one GridItem . Next, insert the following code in
body to create a vertical grid with a single column:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 683


NavigationStack {
ScrollView {
LazyVGrid(columns: gridLayout, alignment: .center, spacing: 10) {

ForEach(sampleCafes) { cafe in
Image(cafe.image)
.resizable()
.scaledToFill()
.frame(minWidth: 0, maxWidth: .infinity)
.frame(maxHeight: 150)
.cornerRadius(10)
.shadow(color: .primary.opacity(0.3), radius: 1)
}

}
.padding(.all, 10)
.animation(.interactiveSpring(), value: gridLayout.count)
}

.navigationTitle("Coffee Feed")
}

I don't think we need to go through the code again because it's almost the same as the
code we wrote earlier. If your code works properly, you should see a list view that shows
the collection of cafe photos.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 684


Figure 15. A list of cafe photos

Adding an Additional Grid


How do we display another grid under each of the cafe photos? All you need to do is add
another LazyVGrid inside the ForEach loop. Insert the following code after the Image

view in the loop:

LazyVGrid(columns: [GridItem(.adaptive(minimum: 50))]) {


ForEach(cafe.coffeePhotos) { photo in
Image(photo.name)
.resizable()
.scaledToFill()
.frame(minWidth: 0, maxWidth: .infinity)
.frame(height: 50)
.cornerRadius(10)
}
}
.frame(minHeight: 0, maxHeight: .infinity, alignment: .top)
.animation(.easeIn, value: gridLayout.count)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 685


Here we create another vertical grid for the coffee photos. By using the adaptive size
type, this grid will fill as many photos as possible in a row. Once you make the code
change, the app UI will look like that shown in figure 16.

Figure 16. Adding another grid for the coffee photos

If you prefer to arrange the cafe and coffee photos side by side, you can modify the
gridLayout variable like this:

@State var gridLayout = [ GridItem(.adaptive(minimum: 100)), GridItem(.flexible())


]

As soon as you change the gridLayout variable, your preview will be updated to display
the cafe and coffee photos side by side.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 686


Figure 17. Arrange the cafe and coffee photos side by side

Handling Landscape Orientation


To test the app in landscape orientation in Xcode preview, you can choose the Device
Settings option and enable the Orientation option. Set it to Landscape (Left) or
Landscape (Right).

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 687


Figure 18. Switch to the landscape orientation in Xcode preview

Alternatively, you can run the app on a simulator and test the landscape mode. But
before you run the app, you will need to perform a simple modification in
SwiftUIPhotoGridApp.swift . Since we have created a new file for implementing this multi-
grid, modify the view in WindowGroup from ContentView() to MultiGridView() like below:

struct SwiftUIPhotoGridApp: App {


var body: some Scene {
WindowGroup {
MultiGridView()
}
}
}

Now you're ready to test the app on an iPhone simulator. You rotate the simulator
sideways by pressing command-left (or right)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 688


Do you find the UI in landscape mode less appealing? The app works great in the portrait
orientation. However, the grid layout doesn't look as expected on landscape orientation.
What we expect is that the UI should look pretty much the same as that in portrait mode.

To fix the issue, we can adjust the minimum width of the adaptive grid item and make it a
bit wider when the device is in landscape orientation. The question is how can you detect
the orientation changes?

In SwiftUI, every view comes with a set of environment variables. You can find out the
current device orientation by accessing both the horizontal and vertical size class
variables like this:

@Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass


?
@Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass?

The @Environment property wrapper allows you to access the environment values. In the
code above, we tell SwiftUI that we want to read both the horizontal and vertical size
classes, and subscribe to their changes. In other words, we will be notified whenever the
device's orientation changes.

If you haven't done so, please make sure you insert the code above in MultiGridView .

The next question is how do we capture the notification and respond to the changes? You
use a modifier called .onChange() . You can attach this modifier to any views to monitor
any state changes. In this case, we can attach the modifier to NavigationStack like this:

.onChange(of: verticalSizeClass) {
self.gridLayout = [ GridItem(.adaptive(minimum: verticalSizeClass == .compact
? 100 : 250)), GridItem(.flexible()) ]
}

We monitor the change of both horizontalSizeClass and verticalSizeClass variables.


Whenever there is a change, we will update the gridLayout variable with a new grid
configuration. The iPhone has a compact height in landscape orientation. Therefore, if

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 689


the value of verticalSizeClass equals .compact , we alter the minimum size of the grid
item to 250 points.

Now run the app on an iPhone simulator again. When you turn the device sideways, it
now shows the cafe photo and coffee photos side by side.

Figure 19. The app UI in landscape mode now looks better

Exercise
I have a couple of exercises for you. First, the app UI doesn't look good on iPad. Modify
the code and fix the issue such that it only shows two columns: one for the cafe photo and
the other for the coffee photos.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 690


Figure 21. App UI on iPad

The next exercise is more complicated with a number of requirements:

1. Different default grid layout for iPhone and iPad - When the app is first
loaded up, it displays a single column grid for iPhone in portrait mode. For iPad and
iPhone landscape, the app shows the cafe photos in a 2-column grid.
2. Show/hide button for the coffee photos - Add a new button in the navigation
bar for toggling the display of coffee photos. By default, the app only shows the list of
cafe photos. When this button is tapped, it shows the coffee photo grid.
3. Another button for switching grid layout - Add another bar button for toggling
the grid layout between one and two columns.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 691


Figure 22. Enhancing the app to support both iPhone and iPad

To help you better understand what the final deliverable looks like, please check out this
video demo at https://link.appcoda.com/multigrid-demo.

Summary
The missing collection view in the first release of SwiftUI is finally available. The
introduction of LazyVGrid and LazyHGrid in SwiftUI allows developers to create various
types of grid layouts with just a few lines of code. This chapter provides a quick overview
of these two new UI components. I encourage you to experiment with different
configurations of GridItem to see what grid layouts you can achieve.

For reference, you can download the complete grid project and the solution to the
exercise at:

https://www.appcoda.com/resources/swiftui5/SwiftUIGridLayout.zip
https://www.appcoda.com/resources/swiftui5/SwiftUIPhotoGrid.zip

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 692


Chapter 30
Creating an Animated Activity Ring
with Shape and Animatable
The built-in Activity app uses three circular progress bars to show your progress in Move,
Exercise, and Stand. This type of progress bar is known as an activity ring. If you haven't
used the Activity app or are not familiar with activity rings, take a look at Figure 1. The
Apple Watch has played a significant role in popularizing this round progress bar as a UI
pattern.

Figure 1. A sample activity ring

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 693


In this chapter, we will explore its implementation and build a similar activity ring in
SwiftUI. Our goal is not just to create a static activity ring, but an animated one that
shows progress changes, like the one shown in Figure 2. You can also check out the demo
video at https://link.appcoda.com/progressring.

Figure 2. Animated progress ring

Creating a New Project


Let's create a new project to build the progress indicator. As usual, you use the App
template for the project. Name it SwiftUIProgressRing or whatever name you like.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 694


Figure 3. Creating a new project with the App template

To organize our code better, create a new file using the SwiftUI view template and name
it ProgressRingView.swift . Once created, Xcode should generate the file with the following
code:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 695


import SwiftUI

struct ProgressRingView: View {


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

#Preview {
ProgressRingView()
}

Dissecting the Activity Ring


Before we dive into the implementation, take another look at Figures 1 and 2. You should
notice that an activity ring is actually composed of two or more circular progress bars.
Thus, what we need to build is a circular progress bar view that is flexible enough to
display a certain percentage value and allow the user to adjust the bar width and color.

For example, if you tell the bar view to display 60% progress in red and set its width to
250 points, the circular progress view should show something like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 696


Figure 4. A sample circular progress bar

By building a flexible circular progress bar view, it becomes very easy to create an activity
ring. For example, we can overlay another circular progress bar with a bigger size and
different color on top of the one shown in Figure 4 to create an activity ring.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 697


Figure 5. A sample circular progress bar

That's how we are going to build the activity ring. Now let's begin to implement the
circular progress bar.

Preparing the Color Extension


As mentioned, the circular progress bar that we are going to implement can support
multiple colors and gradients. For demonstration and convenience purposes, we will
prepare a set of default colors using a Color extension. In the project navigator, right-
click SwiftUIProgressRing and choose New file.... Select the Swift file template and
name the file Color+Ext.swift . Replace the file's content with the following code:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 698


import SwiftUI

extension Color {

public init(red: Int, green: Int, blue: Int, opacity: Double = 1.0) {
let redValue = Double(red) / 255.0
let greenValue = Double(green) / 255.0
let blueValue = Double(blue) / 255.0

self.init(red: redValue, green: greenValue, blue: blueValue, opacity: opac


ity)
}

public static let lightRed = Color(red: 231, green: 76, blue: 60)
public static let darkRed = Color(red: 192, green: 57, blue: 43)
public static let lightGreen = Color(red: 46, green: 204, blue: 113)
public static let darkGreen = Color(red: 39, green: 174, blue: 96)
public static let lightPurple = Color(red: 155, green: 89, blue: 182)
public static let darkPurple = Color(red: 142, green: 68, blue: 173)
public static let lightBlue = Color(red: 52, green: 152, blue: 219)
public static let darkBlue = Color(red: 41, green: 128, blue: 185)
public static let lightYellow = Color(red: 241, green: 196, blue: 15)
public static let darkYellow = Color(red: 243, green: 156, blue: 18)
public static let lightOrange = Color(red: 230, green: 126, blue: 34)
public static let darkOrange = Color(red: 211, green: 84, blue: 0)
public static let purpleBg = Color(red: 69, green: 51, blue: 201)
}

In the code above, we create an init method which takes in the values of red , green ,
and blue . This makes it easier to initialize an instance of Color with an RGB color code.
All the colors are derived from the flat color palette
(https://flatuicolors.com/palette/defo). If you prefer to use other colors, you can simply
modify the color values or create your own color constants.

Implementing the Circular Progress Bar

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 699


Referring to Figure 4, a circular progress bar consists of two circles: a full circle in gray
underneath and another partial (or full) circle in a gradient color on top. Therefore, to
implement the progress bar, we need a ZStack to overlay two views:

1. A circle view in gray


2. A ring shape in a gradient color sitting on top of #1

Now, open ProgressRingView.swift and declare the following variables:

var thickness: CGFloat = 30.0


var width: CGFloat = 250.0

Since this circular progress bar should support various sizes, we declare the variables
above with default values. As the name suggests, the thickness variable controls the
thickness of the progress bar, while the width variable stores the diameter of the circle.

You can create the circle view using the built-in Circle shape like this:

Figure 6. Drawing the Circle view

We use the stroke modifier to draw the outline of the circle in gray. As shown in the
figure, the thickness property controls the width of the outline, while the width

property represents the diameter of the circle. I intentionally highlight the frame so that
you can see the thickness and width.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 700


Next, we will implement the ring shape. One way to create this shape is by using Circle .
We discussed drawing circles in Chapter 8. This time, let me show you an alternative
implementation. We will use the Shape protocol to create a custom Ring shape.

In the same file, insert the following code:

struct RingShape: Shape {


var progress: Double = 0.0
var thickness: CGFloat = 30.0

func path(in rect: CGRect) -> Path {

var path = Path()

path.addArc(center: CGPoint(x: rect.width / 2.0, y: rect.height / 2.0),


radius: min(rect.width, rect.height) / 2.0,
startAngle: .degrees(0),
endAngle: .degrees(360 * progress), clockwise: false)

return path.strokedPath(.init(lineWidth: thickness, lineCap: .round))


}
}

We create a RingShape struct by adopting the Shape protocol. We declare two properties
in the struct: the progress property, which allows the user to specify the percentage of
progress, and the thickness property, which, similar to that in ProgressRingView , lets you
control the width of the ring.

To draw the ring, we use the addArc method, followed by strokedPath . The radius of the
arc can be calculated by dividing the frame's width (or height) by 2. The starting angle is
currently set to zero degrees. We calculate the ending angle by multiplying 360 with the
progress value. For example, if we set the progress to 0.5, we draw a half ring (from 0 to
180 degrees).

To use the RingShape , you can update the body variable like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 701


ZStack {
Circle()
.stroke(Color(.systemGray6), lineWidth: thickness)

RingShape(progress: 0.5, thickness: thickness)


}
.frame(width: width, height: width, alignment: .center)

Once you make the changes, you should see a partial ring overlay on top of the gray
circle. Note that it has round cap at both ends since we set the lineCap parameter of
strokedPath to .round .

Figure 7. Displaying the RingShape

Other than the ring's color, you may also notice something that we need to tweak. The
start point of the arc is not the same as that in figure 4. To fix the issue, you need change
the startAngle from zero to -90.

Declare the following property in RingShape :

var startAngle: Double = -90.0

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 702


Then update the addArc method like this:

path.addArc(center: CGPoint(x: rect.width / 2.0, y: rect.height / 2.0),


radius: min(rect.width, rect.height) / 2.0,
startAngle: .degrees(startAngle),
endAngle: .degrees(360 * progress + startAngle), clockwise: false)

We change the startAngle parameter to -90 degree. we also need to alter the endAngle

parameter, because the starting angle is changed. With the modification, the arc now
rotates by 90 degrees anticlockwise.

Figure 8. The partial ring after changing the start angle

Adding a Gradient
Now that we have a ring shape that we can adjust by passing different progress values to
it, wouldn't it be great if we could add a gradient color to the bar? SwiftUI provides three
types of gradients, including linear gradient, angular gradient, and radial gradient. Apple
uses the angular gradient to fill the progress bar.

Here is an example using AngularGradient :

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 703


AngularGradient(gradient: Gradient(colors: [.darkPurple, .lightYellow]), center: .
center, startAngle: .degrees(0), endAngle: .degrees(180))

The angular gradient applies the gradient color as the angle changes. In the code above,
we render the gradient from 0 degrees to 180 degrees. Figure 9 shows you the result of
two different angular gradients.

Figure 9. Angular gradient with different start and end angles

Since the starting angle of the ring shape is set to -90 degrees, we will apply the angular
gradient like this (assuming the progress is set to 0.5):

AngularGradient(gradient: Gradient(colors: [.darkPurple, .lightYellow]), center: .


center, startAngle: .degrees(startAngle), endAngle: .degrees(360 * 0.5 + startAngl
e))

Now let's modify the code to apply the gradient to the RingShape . First, declare the
following properties in ProgressRingView :

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 704


var gradient = Gradient(colors: [.darkPurple, .lightYellow])
var startAngle = -90.0

Then fill the RingShape with the angular gradient by attaching the .fill modifier like
below:

RingShape(progress: 0.5, thickness: thickness)


.fill(AngularGradient(gradient: gradient, center: .center, startAngle: .degree
s(startAngle), endAngle: .degrees(360 * 0.5 + startAngle)))

As soon as you complete the modification, the circular progress bar should be filled with
the specified gradient.

Figure 10. A circular progress bar with gradient

Varying Progress
The percentage of progress is now fixed at 0.5. Obviously, we need to create a variable for
that to make it adjustable. In ProgressRingView , declare a variable named progress like
this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 705


@Binding var progress: Double

We are developing a flexible ProgressRingView and want to let the caller control the
percentage of progress. Therefore, the source of truth (i.e. progress) should be provided
by the caller. This is the reason why progress is marked as a binding variable.

With the variable, we can update the following line of code accordingly:

RingShape(progress: progress, thickness: thickness)


.fill(AngularGradient(gradient: gradient, center: .center, startAngle: .degree
s(startAngle), endAngle: .degrees(360 * progress + startAngle)))

Xcode should now indicate an error in #Preview because we have to pass


ProgressRingView the progress parameter. Therefore, update the #Preview code block
like this:

#Preview("ProgressRingView (50%)") {
ProgressRingView(progress: .constant(0.5))
}

#Preview("ProgressRingView (90%)") {
ProgressRingView(progress: .constant(0.9))
}

I want to see the end result of two different values of progress, so we create two instances
of ProgressRingView in the preview. Now you should be able to see two previews in the
preview pane.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 706


Figure 11. A circular progress bar with gradient

Animating the Ring Shape with Animatable


The circular progress bar looks pretty good. Let's put it into practice and create a simple
demo like Figure 12. This demo has three buttons for adjusting the progress. We expect
the progress bar to gradually increase (or decrease) to the chosen percentage when any of
the buttons is tapped. For example, the current progress is set to 0. When the "50%"
button is tapped, the progress bar will gradually increase from 0% to 50%.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 707


Figure 12. A quick demo

Now let's switch over to ContentView.swift to create this demo. First, declare a state
variable to keep track of the progress like this:

@State var progress = 0.0

Then insert the following code in the body variable to create the UI:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 708


VStack {
ProgressRingView(progress: $progress)

HStack {
Group {
Text("0%")
.font(.system(.headline, design: .rounded))
.onTapGesture {
self.progress = 0.0
}

Text("50%")
.font(.system(.headline, design: .rounded))
.onTapGesture {
self.progress = 0.5
}

Text("100%")
.font(.system(.headline, design: .rounded))
.onTapGesture {
self.progress = 1.0
}
}
.padding()
.background(Color(.systemGray6))
.clipShape(RoundedRectangle(cornerRadius: 15.0, style: .continuous))
.padding()
}
.padding()
}

In your preview canvas, you should have something like below. The progress bar only
shows the gray circle underneath because the progress is defaulted to zero. Click the Play
button to run the demo. Try tapping different buttons to see how the progress bar
responds.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 709


Figure 13. The demo UI

Does it work up to your expections? I think not. When you tap the 50% button, the
progress bar instantly fills half of the ring without any animation. This isn't what we
expect.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 710


Figure 14. The progress bar doesn't animate its change

I guess you may know why the view is not animated. We haven't attached an .animation

modifier to the ring shape. Switch back to ProgressRingView.swift and attach the
.animation modifier to the ZStack of ProgressRingView . You can insert the code after the
.frame modifier:

.animation(.easeInOut(duration: 1.0), value: progress)

Okay, it seems like we've figured out the solution. Let's go back to ContentView.swift and
test the demo again. Run the demo and tap any of the buttons to try it out.

What's your result? Does the fix work?

Unfortunately, the ring still doesn't animate the progress change, but it does animate the
gradient change.

What's the root cause?

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 711


Before solving the issue, let me further explain how the .animation modifier works. In
the official documentation) for the .animation modifier, it mentions that the modifier
applies the given animation to all animatable values within the view. The keyword here
is animatable. When you use the .animation modifier on a view, SwiftUI automatically
animates any changes to animatable properties of the view.

SwiftUI comes with a protocol called Animatable . For a view that supports animation,
you can adopt the protocol and provide the animatableData property. This property tells
SwiftUI what data the view can animate.

In Chapter 9, I introduced you the basics of SwiftUI animation. You can easily animate
the size change of a view using .scaleEffect or the position change by using .offset . It
may seem to you that all these animations work automatically. Behind the scenes, Apple's
engineers actually adopted the protocol and provided the animatable data for CGSize

and CGPoint .

So, why can't RingShape animate its progress change?

The RingShape struct conforms to the Shape protocol. If you look at its documentation,
Shape adopts the Animatable protocol and provides the default implementation.
However, the default implementation of the animatableData property returns an instance
of EmptyAnimatableData , which means no animatable data. This is why ProgressRingView

cannot animate the progress change.

To fix the issue and make the progress animatable, all you need to do is to override the
default implementation and provide the animatable values. In the RingShape struct,
insert the following code before the path function:

var animatableData: Double {


get { progress }
set { progress = newValue }
}

The implementation is very simple. We just tell SwiftUI to animate the progress value.
That's it!

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 712


Now go back to ContentView.swift and play the demo app to have another test. This time
the progress bar should animate the progress change.

Figure 15. The progress bar doesn't animate its change

The 100% Problem


With the added animation, this circular progress bar is even better. However, there is a
minor issue that you may notice. When the percentage is set to 100%, the arc becomes a
full circle, hiding the round caps. To highlight where the arc ends, it's better to add a
round cap with a drop shadow, like the activity ring in Figure 1.

To resolve the issue, my idea is to overlay a small circle, the size of which is based on the
thickness of the ring, at the end of the arc. Additionally, we will add a drop shadow to
that small circle. Figure 16 illustrates this solution. Please note that, for the final
solution, the circle should have the same color as the arc's end. I have highlighted it in
red for illustrative purposes only.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 713


Figure 16. Overlaying a little circle

The question is how do you calculate the position of this little circle or the end position of
the arc? This requires some mathematical knowledge. Figure 17 shows you how we
calculate the position of the little circle.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 714


Figure 17. Overlaying a little circle

Now, let's dive into the implementation and create the little circle. Let's call it RingTip

and implement it in the ProgressRingView.swift file like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 715


struct RingTip: Shape {
var progress: Double = 0.0
var startAngle: Double = -90.0
var ringRadius: Double

private var position: CGPoint {


let angle = 360 * progress + startAngle
let angleInRadian = angle * .pi / 180

return CGPoint(x: ringRadius * cos(angleInRadian), y: ringRadius * sin(ang


leInRadian))
}

var animatableData: Double {


get { progress }
set { progress = newValue }
}

func path(in rect: CGRect) -> Path {


var path = Path()

guard progress > 0.0 else {


return path
}

let frame = CGRect(x: position.x, y: position.y, width: rect.size.width, h


eight: rect.size.height)

path.addRoundedRect(in: frame, cornerSize: frame.size)

return path
}

The RingTip struct takes in three parameters: progress , startAngle , and ringRadius for
the calculation of the circle's position. Once we figure out the position, we can draw the
path of the circle by using addRoundedRect .

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 716


Now go back to ProgressRingView and declare the following computed property to
calculate the ring's radius:

private var radius: Double {


Double(width / 2)
}

Next, create RingTip by inserting the following code after RingShape in the ZStack :

RingTip(progress: progress, startAngle: startAngle, ringRadius: radius)


.frame(width: thickness, height: thickness)
.foregroundColor(progress > 0.96 ? gradient.stops[1].color : Color.clear)

We instantiate RingTip by passing the current progress, start angle, and the radius of the
ring. The foreground color is set to the ending gradient color. You may wonder why we
only display the gradient color when the progress is greater than 0.96. Take a look at
Figure 18, and you will understand why I came up with this decision.

Figure 18. Need to overlay the circle only when the progress is greater than 0.96

After adding the instance of RingTip in the ZStack , run the program in the preview.
Click the 100% button. The progress bar should now have a round cap.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 717


Figure 19. Overlaying a little circle at the ring end

You've already built a pretty nice circular progress bar, but we can make it even better by
adding a drop shadow at the end of the arc. In SwiftUI, you can simply attach the
.shadow modifier to add a drop shadow. In this case, we can attach the modifier to
RingTip . The hard part is figuring out where to add the drop shadow.

The calculation of the shadow position is very similar to that of the ring tip. So, in
ProgressRingView.swift , let's insert a function for computing the position of the ring tip:

private func ringTipPosition(progress: Double) -> CGPoint {


let angle = 360 * progress + startAngle
let angleInRadian = angle * .pi / 180

return CGPoint(x: radius * cos(angleInRadian), y: radius * sin(angleInRadian))


}

Then add a new computed property for calculating the shadow offset of the ring tip like
this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 718


private var ringTipShadowOffset: CGPoint {
let shadowPosition = ringTipPosition(progress: progress + 0.01)
let circlePosition = ringTipPosition(progress: progress)

return CGPoint(x: shadowPosition.x - circlePosition.x, y: shadowPosition.y - c


irclePosition.y)
}

By adding 0.01 to the current progress, we can compute the shadow position. This is my
solution for finding the shadow position. You may come up with an alternative solution.

With the shadow offset, we can attach the .shadow modifier to RingTip :

.shadow(color: progress > 0.96 ? Color.black.opacity(0.15) : Color.clear, radius: 2


, x: ringTipShadowOffset.x, y: ringTipShadowOffset.y)

I just want to add a light shadow, so the opacity is set to 0.15. If you prefer a darker
shadow, increase the opacity value (say, 1.0). After making the code change, you should
see a drop shadow at the end of the ring, provided that the progress is greater than 0.96.
You can also try setting the progress value to a value larger than 1.0 and see how the
progress bar looks.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 719


Figure 20. The ring end now has a drop shadow

Exercise
Now that you've created a flexible circular progress bar, it's time to have an exercise. Your
task is to make use of what you've built and create an activity ring. The app should also
provide four buttons for adjusting the activity ring, as seen in Figure 21.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 720


Figure 21. A sample activity ring

Summary
By building an activity ring, we have covered a number of SwiftUI features in this
chapter. You should now have a better idea of how to implement your custom shape and
how to animate a shape using the Animatable protocol.

For reference, you can download the complete project here:

Demo project
(https://www.appcoda.com/resources/swiftui5/SwiftUIProgressRing.zip)
Solution
(https://www.appcoda.com/resources/swiftui5/SwiftUIProgressRingExercise.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 721


Chapter 31
Working with AnimatableModifier
and LibraryContentProvider
Earlier, you learned how to animate a shape using Animatable and AnimatableData . In
this chapter, we will take it a step further and show you how to animate a view using
another protocol called AnimatableModifier . Additionally, I will guide you through a new
feature of SwiftUI that makes it easier for developers to share custom views in the View
library, enhancing reusability. Later, I will demonstrate how to integrate the progress
ring view into the View library for seamless reuse. As a sneak peek, you can refer to
Figure 1 or watch this demo video (https://link.appcoda.com/librarycontentprovider) to
see it in action.

Figure 1. Using a custom view in the View library

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 722


Understanding AnimatableModifier
Let's begin by examining the AnimatableModifier protocol. As the name suggests,
AnimatableModifier is a view modifier that conforms to the Animatable protocol. This
allows for powerful animation of value changes for various types of views.

protocol AnimatableModifier : Animatable, ViewModifier

So, what are we going to animate? We will build upon what we implemented in the
previous chapter and add a text label at the center of the progress ring. This label will
display the current percentage of progress. As the progress bar moves, the label will
update accordingly. Figure 2 provides a visual representation of the label's appearance.

Figure 2. Animating the progress label

Animating Text using AnimatableModifer

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 723


I highly recommend reading chapter 30 first, as this demo project is built on top of the
previous one. If you haven't worked on the project yet, you can download it at
https://www.appcoda.com/resources/swiftui5/SwiftUIProgressRingExercise.zip to get
started.

Before we delve into the AnimatableModifier protocol, let me ask you: How do you plan to
layout the progress label and animate its changes? In fact, we built a similar progress
indicator in chapter 9. Based on what you've learned, you can layout the progress label
(in the ProgressRingView.swift file) as follows:

ZStack {
Circle()
.stroke(Color(.systemGray6), lineWidth: thickness)

Text(progressText)
.font(.system(.largeTitle, design: .rounded))
.fontWeight(.bold)
.foregroundStyle(.black)

...
}

You add a Text view in the ZStack and display the current progress in a formatted text
using the below conversion:

private var progressText: String {


let formatter = NumberFormatter()
formatter.numberStyle = .percent
formatter.percentSymbol = "%"

return formatter.string(from: NSNumber(value: progress)) ?? ""


}

Since the progress variable is a state variable, the progressText will be automatically
updated whenever the value of progress changes. This implementation is
straightforward. However, there is an issue with the solution: the text animation doesn't
work well.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 724


If you have made the above changes in ProgressRingView.swift , you can return to
ContentView.swift to see the result. The app does display the progress label, but when
you change the progress from one value to another, the progress label immediately shows
the new value using a fade animation.

This is not what we expect. The progress label shouldn't jump directly from one value
(e.g., 100%) to another value (e.g., 50%). Instead, we expect the progress label to follow
the animation of the progress bar and update its value step by step, as shown in the
following example:

100 -> 99 -> 98 -> 97 -> 96 ... ... ... ... ... ... ... ... ... ... 53 -> 52 -> 51 -> 50

The current implementation doesn't allow you to animate the text change. This is why I
have to introduce you the AnimatableModifier protocol.

Note: Please remove the Text view you just added.

To animate the progress text, we will create a new struct called ProgressTextModifier ,
which adopts AnimatableModifier , in ProgressRingView.swift :

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 725


struct ProgressTextModifier: AnimatableModifier {

var progress: Double = 0.0


var textColor: Color = .primary

private var progressText: String {


let formatter = NumberFormatter()
formatter.numberStyle = .percent
formatter.percentSymbol = "%"

return formatter.string(from: NSNumber(value: progress)) ?? ""


}

var animatableData: Double {


get { progress }
set { progress = newValue }
}

func body(content: Content) -> some View {


content
.overlay(
Text(progressText)
.font(.system(.largeTitle, design: .rounded))
.fontWeight(.bold)
.foregroundStyle(textColor)
.animation(nil)
)
}
}

Does the code look familiar to you? As mentioned earlier, the AnimatableModifier

protocol conforms to both Animatable and ViewModifier . Therefore, we specify in the


animatableData property which values to animate. In this case, it's progress . To meet the
requirements of ViewModifier , we implement the body function and add the Text view.

This is how we animate the text using AnimatableModifier . For convenience, insert the
following code at the end of ProgressRingView to create an extension for applying the
ProgressTextModifier :

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 726


extension View {
func animatableProgressText(progress: Double, textColor: Color = Color.primary)
-> some View {
self.modifier(ProgressTextModifier(progress: progress, textColor: textColo
r))
}
}

Now you can attach the animatableProgressText modifier to RingShape like this:

RingShape(progress: progress, thickness: thickness)


.fill(AngularGradient(gradient: gradient, center: .center, startAngle: .degree
s(startAngle), endAngle: .degrees(360 * progress + startAngle)))
.animatableProgressText(progress: progress)

Once you have made the change, you should see the progress label in the preview canvas.
To test the animation, run the app on an iPhone simulator or test it in the preview of
ContentView.swift . When you change the progress, the progress text now animates.

Figure 3. Displaying the progress label by applying the custom modifier

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 727


Using LibraryContentProvider
Xcode has a powerful feature that enables developers to include custom views in the View
library. In case you're unfamiliar with the View library, simply press Command-Shift-L to
bring it up. The library provides easy access to all the available UI controls in SwiftUI.
You can simply drag a control from the library and add it directly to the user interface.

Figure 4. Displaying the progress label by applying the custom modifier

Xcode enables developers to include custom views in the library by utilizing a protocol
called LibraryContentProvider . To add a custom view to the View library, you simply need
to create a new struct that conforms to the LibraryContentProvider protocol.

For example, to share the progress ring view to the View library, we can create a struct
called ProgressBar_Library in ProgressRingView.swift like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 728


struct ProgressBar_Library: LibraryContentProvider {
@LibraryContentBuilder var views: [LibraryItem] {
LibraryItem(ProgressRingView(progress: .constant(1.0), thickness: 12.0, wi
dth: 130.0, gradient: Gradient(colors: [.darkYellow, .lightYellow])), title: "Prog
ress Ring", category: .control)
}
}

The way to add a view to the View library is very simple. You need to create a struct that
conforms to the LibraryContentProvider protocol and override the views property to
return an array of custom views. In the provided code, we return the progress ring view
with default values, name it "Progress Ring," and categorize it under the control category.

Optionally, if you want to add more than one library item, you can write the code like
this:

struct ProgressBar_Library: LibraryContentProvider {


@LibraryContentBuilder var views: [LibraryItem] {
LibraryItem(ProgressRingView(progress: .constant(1.0), thickness: 12.0, wi
dth: 130.0, gradient: Gradient(colors: [.darkYellow, .lightYellow])), title: "Prog
ress Ring", category: .control)

LibraryItem(ProgressRingView(progress: .constant(1.0), thickness: 30.0, wi


dth: 250.0, gradient: Gradient(colors: [.darkPurple, .lightYellow])), title: "Prog
ress Ring - Bigger", category: .control)
}
}

As a side note, there are four possible values that can be given to item's category,
depending on what the library item is supposed to represent:

control

effect

layout

other

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 729


You may also wonder what the @LibraryContentBuilder property wrapper is. It just saves
you from writing the code for creating the array of LibraryItem instances. The code above
can be rewritten like this:

struct ProgressBar_Library: LibraryContentProvider {


var views: [LibraryItem] {
return [LibraryItem(ProgressRingView(progress: .constant(1.0), thickness:
12.0, width: 130.0, gradient: Gradient(colors: [.darkYellow, .lightYellow])), titl
e: "Progress Ring", category: .control),

LibraryItem(ProgressRingView(progress: .constant(1.0), thickness:


30.0, width: 250.0, gradient: Gradient(colors: [.darkPurple, .lightYellow])), titl
e: "Progress Ring - Bigger", category: .control)]
}
}

Once you create the struct, Xcode automatically detects the implementation of the
LibraryContentProvider protocol in your project and includes the progress ring view in the
View library. You can now easily add the progress ring view to your user interface using
drag and drop. Please note that as of the time of this writing, it is not possible to add
documentation for custom controls.

Figure 5. The progress ring view is added to the View library

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 730


Not only can you add a custom view to the Xcode library, you can also add your own
modifiers by implementing the modifiers method and return the array of library items.
You can add the animatableProgressText modifier to the View library by implementing the
method like this:

struct ProgressBar_Library: LibraryContentProvider {


.
.
.

@LibraryContentBuilder
func modifiers(base: Circle) -> [LibraryItem] {
LibraryItem(base.animatableProgressText(progress: 1.0), title: "Progress I
ndicator", category: .control)
}
}

The base parameter lets you specify the type of control that can be modified by the
modifier. In the code above, it's the Circle view. Again, once you insert the code
in ProgressBar_Library , Xcode will scan the library item and add it to the Modifier library.

Figure 6. The progress indicator is added to the Modifier library

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 731


Exercise
The progress ring is now included in the View library. Let's utilize it to build an app like
the one described below. The app consists of four sliders that allow you to adjust the
progress of different tasks. The overall progress is calculated by averaging the progress
values of all tasks.

Figure 7. The progress ring view is added to the View library

Summary
The AnimatableModifier protocol is a powerful tool for animating changes in any view. In
this chapter, we demonstrated how to animate text changes in a label. This technique can
be applied to animate other values such as color and size.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 732


The introduction of LibraryContentProvider greatly simplifies the sharing of custom views
and promotes code reuse. Imagine building a library of custom components and adding
them to the View/Modifier library. Every member of your team can easily access these
controls and use them through simple drag and drop. Currently, you can only use these
controls within the same Xcode project. In the next chapter, we will discuss how you can
make this functionality possible by utilizing Swift Packages.

For reference, you can download the complete project here:

Demo project
(https://www.appcoda.com/resources/swiftui5/SwiftUIProgressRingLibrary.zip)
Exercise
(https://www.appcoda.com/resources/swiftui5/SwiftUITaskAnimation.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 733


Chapter 32
Working with TextEditor and
Multiline Text Fields
The first version of SwiftUI, released along with iOS 13, didn't come with a native UI
component for a multiline text field. To support multiline input, developers had to wrap a
UITextView from the UIKit framework and make it available to their SwiftUI project by
adopting the UIViewRepresentable protocol. In iOS 14, Apple introduced a new component
called TextEditor for the SwiftUI framework. This TextEditor enables developers to
display and edit multiline text in your apps. Additionally, in iOS 16, Apple further
improved the built-in TextField to support multiline input.

In this chapter, we will demonstrate how to utilize both TextEditor and TextField for
handling multiline input in your SwiftUI apps.

Using TextEditor
It is very easy to use TextEditor . You simply need to have a state variable to hold the
input text. Then, create a TextEditor instance in the body of your view, like this:

struct ContentView: View {


@State private var inputText = ""

var body: some View {


TextEditor(text: $inputText)
}
}

To instantiate the text editor, you pass the binding of inputText so that the state variable
can store the user input.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 734


You can customize the editor like any other SwiftUI view. For example, the code below
changes the font type and adjusts the line spacing of the text editor:

TextEditor(text: $inputText)
.font(.title)
.lineSpacing(20)
.autocapitalization(.words)
.disableAutocorrection(true)
.padding()

Optionally, you can enable/disable the auto-capitalization and auto-correction features.

Figure 1. Using TextEditor

Using the onChange() Modifier to Detect Text Input


Change
In UIKit, UITextView works with the UITextViewDelegate protocol to handle editing
changes. So, how about TextEditor in SwiftUI? How do we detect the change of user
input and perform further processing?

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 735


The SwiftUI framework provides an onChange() modifier, which can be attached to
TextEditor or any other views to detect state changes. Let's say you are building a note
application using TextEditor and need to display a word count in real time. You can
attach the onChange() modifier to TextEditor like this:

struct ContentView: View {


@State private var inputText = ""
@State private var wordCount: Int = 0

var body: some View {


ZStack(alignment: .topTrailing) {
TextEditor(text: $inputText)
.font(.body)
.padding()
.padding(.top, 20)
.onChange(of: inputText) {
let words = inputText.split { $0 == " " || $0.isNewline }
self.wordCount = words.count
}

Text("\(wordCount) words")
.font(.headline)
.foregroundColor(.secondary)
.padding(.trailing)
}
}
}

In the code above, we declare a state property to store the word count. And, we specify in
the onChange() modifier to monitor the change of inputText . Whenever a user types a
character, the code inside the onChange() modifier wilsl be invoked. In the closure, we
compute the total number of words in inputText and update the wordCount variable
accordingly.

If you test the code in Xcode preview or a simulator, you should see a plain text editor
that also displays the word count in real time.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 736


Figure 2. Using onChange() to detect text input and display the word count

Expandable Text Fields with Multiline Support


Prior to iOS 16, the built-in TextField could only support a single line of text. Now,
Apple has greatly improved the TextField view, allowing users to input multiple lines.
Even better, you can now use a new parameter named axis to tell iOS whether the text
field should be expanded.

For example, the text field initially displays a single-line input. When the user keys in the
text, the text field expands automatically to support multiline input. Here is the sample
code snippet for the implementation:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 737


struct TextFieldDemo: View {
@State private var comment = ""

var body: some View {


TextField("Comment", text: $comment, prompt: Text("Please input your comme
nt"), axis: .vertical)
.padding()
.background(.green.opacity(0.2))
.cornerRadius(5.0)
.padding()

}
}

The axis parameter can either have a value of .vertical or .horizontal . In the code
above, we set the value to .vertical . In this case, the text field expands vertically to
support multiline input. If it's set to .horizontal , the text field will expand horizontally
and keep itself as a single line text field.

Figure 3. The improved TextField view can support multiline input

By pairing the lineLimit modifier, you can change the initial size of the text field. Let's
say, if you want to display a three-line text field, you can attach the lineLimit modifier
and set the value to 3 :

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 738


TextField("Comment", text: $comment, prompt: Text("Please input your comment"), ax
is: .vertical)
.lineLimit(3)

You can also provide a range for the line limit. Here is an example:

TextField("Comment", text: $comment, prompt: Text("Please input your comment"), ax


is: .vertical)
.lineLimit(3...5)

In this case, the text field will not expand more than 5 lines.

Summary
Since the initial release of SwiftUI, TextEditor has been one of the most anticipated UI
components. You can now use this native component to handle multiline input. With the
release of iOS 16, you can also use TextField to get user input that may need more space
to type. The auto-expand feature allows you to easily create a text field that is flexible
enough to take a single line or multiline input.

For reference, you can download the complete text editor project here:

Demo project
(https://www.appcoda.com/resources/swiftui5/SwiftUITextEditor.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 739


Chapter 33
Using matchedGeometryEffect to
Create View Animations
When iOS 14 was released, Apple introduced many new additions to the SwiftUI
framework, such as LazyVGrid and LazyHGrid. However, matchedGeometryEffect caught
my attention because it allows developers to create amazing view animations with just a
few lines of code. In earlier chapters, you learned how to create view animations.
matchedGeometryEffect takes the implementation of view animations to the next level.

For any mobile app, it's common to move from one view to another. Creating a delightful
transition between views will enhance the user experience. With the
matchedGeometryEffect modifier, you describe the appearance of two views. The modifier
then computes the difference between those views and automatically animates the size
and position changes.

Feeling confused? No worries. You will understand what I mean after going through the
demo apps.

Revisiting SwiftUI Animation


Before I walk you through the usage of matchedGeometryEffect , let's take a look at how we
implement animation using SwiftUI. Figure 1 shows the initial and final states of a view.
When you tap the circle view on the left, it grows bigger and moves upward. Conversely,
if you tap the one on the right, it returns to its original size and position.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 740


Figure 1. The Circle view at the start state (left), The Circle view at the end state (right)

The implementation of this tappable circle is very straightforward. Assuming you've


created a new SwiftUI project, you can update the ContentView struct like this:

struct ContentView: View {

@State private var expand = false

var body: some View {


Circle()
.fill(.green)
.frame(width: expand ? 300 : 150, height: expand ? 300 : 150)
.offset(y: expand ? -200 : 0)
.animation(.default, value: expand)
.onTapGesture {
self.expand.toggle()
}
}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 741


We have a state variable expand to keep track of the current state of the Circle view. In
both the .frame and .offset modifiers, we vary the frame size and offset when the state
changes. If you run the app in the preview canvas, you should see the animation when
you tap the circle.

Figure 2. The Circle view animation

Understanding the matchedGeometryEffect Modifier


What is matchedGeometryEffect , and how does it simplify the implementation of view
animation? Take another look at Figure 1 and the code for the circle animation. We have
to determine the exact value changes between the start and final states, such as the frame
size and offset.

However, with the matchedGeometryEffect modifier, you no longer need to figure out these
differences. All you need to do is describe two views: one that represents the start state
and the other for the final state. matchedGeometryEffect automatically interpolates the size
and position between the views.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 742


To create the same animation as shown in figure 2 with matchedGeometryEffect , you first
declare a namespace variable:

@Namespace private var shapeTransition

And then, rewrite the body part like this:

var body: some View {


if expand {

// Final State
Circle()
.fill(.green)
.matchedGeometryEffect(id: "circle", in: shapeTransition)
.frame(width: 300, height: 300)
.offset(y: -200)
.onTapGesture {
withAnimation(.easeIn) {
expand.toggle()
}
}

} else {

// Start State
Circle()
.fill(.green)
.matchedGeometryEffect(id: "circle", in: shapeTransition)
.frame(width: 150, height: 150)
.offset(y: 0)
.onTapGesture {
withAnimation(.easeIn) {
expand.toggle()
}
}
}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 743


In the code, we created two Circle views: one for the start state and the other for the
final state. When the app initializes, we present a Circle view that is centered and has a
width of 150 points. When the expand state variable changes from false to true , the
app displays another Circle view that is positioned 200 points from the center of the
screen and has a width of 300 points.

For both Circle views, we attach the matchedGeometryEffect modifier and specify the
same ID and namespace. By doing so, SwiftUI computes the size and position difference
between the views and interpolates the transition. Along with the withAnimation

function, the framework animates the transition automatically.

The ID and namespace are used to identify which views are part of the same transition.
That's why both Circle views use the same ID and namespace.

This is how you use matchedGeometryEffect to animate transitions between two views. If
you've used Magic Move in Keynote before, this new modifier is very much like Magic
Move. To test the animation, simply tap the circle in the preview canvas.

Morphing From a Circle to a Rounded Rectangle


Let's try implementing another animated view transition. This time, we will morph a
circle into a rounded rectangle. The circle is positioned at the top of the screen, while the
rounded rectangle is close to the bottom part of the screen.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 744


Figure 3. The Circle view at the start state (left), The Rounded Rectangle view at the end
state (right)

Using the same technique you just learned, you need to prepare two views: the circle view
and the rounded rectangle view. The matchedGeometryEffect modifier will handle the
transformation. Replace the body variable of the ContentView struct with the following
code:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 745


VStack {
if expand {

// Rounded Rectangle
Spacer()

RoundedRectangle(cornerRadius: 50.0)
.matchedGeometryEffect(id: "circle", in: shapeTransition)
.frame(minWidth: 0, maxWidth: .infinity, maxHeight: 300)
.padding()
.foregroundColor(Color(.systemGreen))
.onTapGesture {
withAnimation {
expand.toggle()
}
}

} else {

// Circle
RoundedRectangle(cornerRadius: 50.0)
.matchedGeometryEffect(id: "circle", in: shapeTransition)
.frame(width: 100, height: 100)
.foregroundColor(Color(.systemOrange))
.onTapGesture {
withAnimation {
expand.toggle()
}
}

Spacer()
}
}

We still use the expand state variable to toggle between the circle view and the rounded
rectangle view. The code is very similar to the previous example, except that we use a
VStack and a Spacer to position the views. You may wonder why we used
RoundedRectangle to create the circle. The main reason is that it gives a smoother
transition.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 746


For both views, we attach the matchedGeometryEffect modifier and specify the same ID
and namespace. That's all we need to do. The modifier compares the difference between
these two views and animates the changes. If you run the app in the preview canvas or on
an iPhone simulator, you will see a nice transition between the circle and rounded
rectangle views. This is the magic of matchedGeometryEffect .

Figure 4. The Circle view animation

However, you may notice that the modifier doesn't animate the color change. This is
because matchedGeometryEffect only handles position and size changes.

Exercise #1
Let's have a simple exercise to test your understanding of matchedGeometryEffect . Your
task is to create the animated transition shown in Figure 5. It starts with an orange circle
view. When the circle is tapped, it transforms into a full-screen background. You can find

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 747


the solution in the final project.

Figure 5. Transforming a circle button to a full screen background

Swapping Two Views with Animated Transition


Now that you have some basic knowledge of matchedGeometryEffect , let's continue to see
how it can help us create some nice animations. In this example, we will swap the
position of two circle views and apply a modifier to create a smooth transition.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 748


Figure 6. Swapping the position of two circles

We will use a state variable to store the state of the swap and create a namespace variable
for matchedGeometryEffect . Declare the following variable in ContentView :

@State private var swap = false

@Namespace private var dotTransition

By default, the orange circle is on the left side of the screen, while the green circle is
positioned on the right. When the user taps either circle, it triggers the swap. You don't
need to figure out how the swap is done when using matchedGeometryEffect . To create the
transition, all you need to do is:

1. Create the layout of the orange and green circles before the swap
2. Create the layout of the two circles after the swap

To translate the layout into code, you write the body variable like this:

if swap {

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 749


// After swap
// Green dot on the left, Orange dot on the right

HStack {
Circle()
.fill(.green)
.frame(width: 30, height: 30)
.matchedGeometryEffect(id: "greenCircle", in: dotTransition)

Spacer()

Circle()
.fill(.orange)
.frame(width: 30, height: 30)
.matchedGeometryEffect(id: "orangeCircle", in: dotTransition)
}
.frame(width: 100)
.onTapGesture {
withAnimation {
swap.toggle()
}
}

} else {

// Start state
// Orange dot on the left, Green dot on the right

HStack {
Circle()
.fill(.orange)
.frame(width: 30, height: 30)
.matchedGeometryEffect(id: "orangeCircle", in: dotTransition)

Spacer()

Circle()
.fill(.green)
.frame(width: 30, height: 30)
.matchedGeometryEffect(id: "greenCircle", in: dotTransition)
}
.frame(width: 100)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 750


.onTapGesture {
withAnimation {
swap.toggle()
}
}
}

We use a HStack to layout the two circles horizontally and have a Spacer in between
them to create some separation. When the swap variable is set to true , the green circle
is placed to the left of the orange circle. When false , the green circle is positioned to the
right of the orange circle.

As you can see, we just describe the layout of the circle views in difference states and let
matchedGeometryEffect handle the rest. We attach the modifier to each of the Circle

views. However, this time is a bit different. Since we have two different Circle views to
match, we use two distinct IDs for the matchedGeometryEffect modifier. For the orange
circles, we set the identifier to orangeCircle , while the green circles uses the identifier
greenCircle .

Run the app on a simulator or in the preview canvas, you should see the swap animation
when you tap any of the circles.

Exercise #2
Earlier, we used matchedGeometryEffect on two circles and swapped their positions. Your
exercise is to apply the same technique but on two images. Figure 6 shows you the
sample UI. When the swap button is tapped, the app swaps the two photos with a nice
animation.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 751


Figure 7. Swapping the position of two photos

You are free to use your own photos. For my demo, I used these free photos from
Unsplash.com:

https://unsplash.com/photos/pMW4jzELQCw
https://unsplash.com/photos/PM4Vu1B0gxk

Creating a Basic Hero Animation


Other than transforming from one shape to another, you can use the
matchedGeometryEffect modifier to create a basic hero animation. Figure 8 shows you a
sample stack view of an image and text. When the view is tapped, both the image and text
will expand to take up the full screen. This type of animation is usually known as a Hero
Animation.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 752


Figure 8. Expanding a stack view into a full screen

Again, we apply the matchedGeometryEffect technique to create this type of animated


transition. If you refer to figure 8, there are two views in the view transition:

1. One is the view showing a smaller image and an excerpt for the article.
2. The other is the view expanded into full screen showing a featured photo and the full
article.

To begin, first declare a state variable to control the status of the view mode:

@State private var showDetail = false

When showDetail is set to false, the article view with a smaller image is displayed. when
true, a full screen article view will be shown. Again, to use the matchedGeometryEffect

modifier, we have to declare a namespace variable:

@Namespace private var articleTransition

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 753


Next, update the body variable like this:

// Display an article view with smaller image


if !showDetail {
VStack {
Spacer()

VStack {
Image("latte")
.resizable()
.scaledToFill()
.frame(minWidth: 0, maxWidth: .infinity)
.frame(height: 200)
.matchedGeometryEffect(id: "image", in: articleTransition)
.cornerRadius(10)
.padding()
.onTapGesture {
withAnimation(.interactiveSpring(response: 0.5, dampingFractio
n: 0.8, blendDuration: 0.2)) {
showDetail.toggle()
}
}

Text("The Watertower is a full-service restaurant/cafe located in the


Sweet Auburn District of Atlanta.")
.matchedGeometryEffect(id: "text", in: articleTransition)
.padding(.horizontal)
}
}
}

// Display the article view in a full screen


if showDetail {

ScrollView {
VStack {
Image("latte")
.resizable()
.scaledToFill()
.frame(minWidth: 0, maxWidth: .infinity)
.frame(height: 400)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 754


.clipped()
.matchedGeometryEffect(id: "image", in: articleTransition)
.onTapGesture {
withAnimation(.interactiveSpring(response: 0.5, dampingFracti
on: 0.8, blendDuration: 0.4)) {
showDetail.toggle()
}
}

Text("The Watertower is a full-service restaurant/cafe located in the


Sweet Auburn District of Atlanta. The restaurant features a full menu of moderatel
y priced \"comfort\" food influenced by African and French cooking traditions, but
based upon time honored recipes from around the world. The cafe section of The Wa
tertower features a coffeehouse with a dessert bar, magazines, and space for live
performers.\n\nThe Watertower will be owned and operated by The Watertower LLC, a
Georgia limited liability corporation managed by David N. Patton IV, a resident of
the Empowerment Zone. The members of the LLC are David N. Patton IV (80%) and the
Historic District Development Corporation (20%).\n\nThis business plan offers fin
ancial institutions an opportunity to review our vision and strategic focus. It al
so provides a step-by-step plan for the business start-up, establishing favorable
sales numbers, gross margin, and profitability.\n\nThis plan includes chapters on
the company, products and services, market focus, action plans and forecasts, mana
gement team, and financial plan.")
.matchedGeometryEffect(id: "text", in: articleTransition)
.animation(nil, value: showDetail)
.padding(.all, 20)

Spacer()
}

}
.edgesIgnoringSafeArea(.all)
}

In the code above, we layout the views in different states. When showDetail is set to
false , we use a VStack to layout the article image and the excerpt. The height of the
image is set to 200 points to make it smaller.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 755


The layout of the article view is very similar in full-screen mode. The main difference is
that the VStack view is embedded in a ScrollView to make the content scrollable. The
image's height is set to 400 points, so that the image is a little bit bigger. To extend the
image and text views outside of the screen's safe area, we attach the
.edgesIgnoringSafeArea modifier to the scroll view and set its value to .all .

Since we have two different views in the transition, we use two different IDs for the
matchedGeometryEffect modifier. For the image, we set the ID to image :

.matchedGeometryEffect(id: "image", in: articleTransition)

On the other hand, we set the ID of the text view to text :

.matchedGeometryEffect(id: "text", in: articleTransition)

Furthermore, we use two different animations for the text and image views. We apply the
.interactiveSpring animation for the image view, while for the text view, we use the
.easeOut animation.

The implementation is very straightforward, similar to what we have done in the earlier
examples. Run the app on a simulator or in the preview canvas to try it out. When you
tap the image view, the app renders a nice animation and shows the article in full screen.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 756


Figure 9. A basic hero animation

Passing @Namespace between Views


Referring to the previous example, we can better organize the code by breaking the two
different stack views into subviews. But the problem is how we can pass the @Namespace

variable between views. Let's see how it can be done.

First, hold the control key and click on the VStack keyword of the first stack view.
Choose Extract Subview from the context menu and name the subview
ArticleExcerptView .

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 757


Figure 10. Extracting the stack view into a subview

You should see quite a number of errors in the ArticleExcerptView struct, complaining
about the missing of the namespace and the showDetail variable. To fix the error of the
showDetail variable, you can declare a binding in ArticleExcerptView like this:

@Binding var showDetail: Bool

To accept a namespace from another view, the trick is to declare a variable with the type
Namespace.ID like this:

var articleTransition: Namespace.ID

This should now fix all the errors in ArticleExcerptView . Now go back to ContentView and
replace ArticleExcerptView() with:

ArticleExcerptView(showDetail: $showDetail, articleTransition: articleTransition)

We pass the binding to showDetail and the namespace variable to the subview. This is
how you share a namespace across different views. Repeat the same procedure to extract
the ScrollView into another subview. Name the subview ArticleDetailView .

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 758


Again, you need to declare the following variable and binding in ArticleDetailView to
resolve all the errors:

@Binding var showDetail: Bool

var articleTransition: Namespace.ID

You should also update the instantiation of ArticleDetailView() like this:

ArticleDetailView(showDetail: $showDetail, articleTransition: articleTransition)

After all these changes, the ContentView struct is now simplified like this:

struct ContentView: View {

@State private var showDetail = false

@Namespace private var articleTransition

var body: some View {

// Display an article view with smaller image


if !showDetail {
ArticleExcerptView(showDetail: $showDetail, articleTransition: article
Transition)
}

// Display the article view in a full screen


if showDetail {
ArticleDetailView(showDetail: $showDetail, articleTransition: articleT
ransition)
}

}
}

Everything works the same but the code is now more readable and organized.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 759


Summary
The introduction of the matchedGeometryEffect modifier takes the implementation of view
animation to the next level. You can create some nice view transitions with much less
code. Even if you are a beginner to SwiftUI, you can take advantage of this new modifier
to make your app more awesome.

For reference, you can download the complete matched geometry project, with the
solutions to the exercises, here:

Demo project
(https://www.appcoda.com/resources/swiftui5/SwiftUIMatchedGeometry.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 760


Chapter 34
ScrollViewReader and Grid
Animation
Earlier, I introduced you to the new matchedGeometryEffect modifier and demonstrated
how to create some basic view animations. In this chapter, we will explore how to use the
modifier to animate item selection in a grid view. Additionally, you will learn about a new
UI component called ScrollViewReader .

The Demo App


Before we delve into the implementation, let me show you the final product. This should
give you an idea of what you will be building. In real-world apps, you may need to display
a grid of photo items and allow users to select some of the items.

One way to present item selection is to have a dock at the bottom of the screen. When an
item is selected, it is removed from the grid and added to the dock. As you select more
items, the dock will hold more items. You can swipe horizontally to navigate through the
items in the dock. If you tap an item in the dock, that item will be removed and added
back to the grid. Figure 1 illustrates how insertion and removal of an item works.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 761


Figure 1. The demo app

We will implement the grid view and the item selection. We will use the
matchedGeometryEffect modifier to animate the selection. To get started, please first
download the starter project at
https://www.appcoda.com/resources/swiftui5/SwiftUIGridViewAnimationStarter.zip.
This project includes sample data and images.

Building the Photo Grid


First, let's create the photo grid. In the ContentView struct, declare a state variable like
this:

@State private var photoSet = samplePhotos

The samplePhotos constant is predefined in the starter project and stores the array of
photos. The reason why photoSet is declared as a state variable is that we will change its
content for photo selection.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 762


To present the photos in a grid, we use the built-in LazyVGrid component. Insert the
following code in body :

VStack {
ScrollView {

HStack {
Text("Photos")
.font(.system(.title, design: .rounded))
.fontWeight(.heavy)

Spacer()
}

LazyVGrid(columns: [ GridItem(.adaptive(minimum: 50)) ]) {

ForEach(photoSet) { photo in

Image(photo.name)
.resizable()
.scaledToFill()
.frame(minWidth: 0, maxWidth: .infinity)
.frame(height: 60)
.cornerRadius(3.0)
}
}
}
}
.padding()

Assuming you have read the earlier chapter on grid views, the code is self-explanatory.
We use the adaptive layout to arrange the set of photos in a grid.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 763


Figure 2. Presenting the photo set in a grid

Adding the Dock


For photo selection, we will create a dock to hold the selected photos. Insert the following
code inside the VStack :

ScrollView(.horizontal, showsIndicators: false) {

}
.frame(height: 100)
.padding()
.background(Color(.systemGray6))
.cornerRadius(5)

This creates a scrollable rectangle area for holding the selected photos. Right now, it's
just blank.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 764


Figure 3. Adding a gray area

Handling Photo Selection


When a photo is selected, we remove it from the photo grid and add it to the dock. To
handle photo selection, we create a state variable to hold the selected photos. Insert the
following code in ContentView to declare the variable:

@State private var selectedPhotos: [Photo] = []

Each photo in the photoSet has its own ID of the type UUID . To store the current
selected photo, declare another state variable of the type UUID :

@State private var selectedPhotoId: UUID?

To handle the photo selection, attach a onTapGesture function to the Image component
of LazyVGrid like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 765


Image(photo.name)
.resizable()
.scaledToFill()
.frame(minWidth: 0, maxWidth: .infinity)
.frame(height: 60)
.cornerRadius(3.0)
.onTapGesture {
selectedPhotos.append(photo)
selectedPhotoId = photo.id
if let index = photoSet.firstIndex(where: { $0.id == photo.id }) {
photoSet.remove(at: index)
}
}

In the block onTapGesture , we add the selected photo to the selectedPhotos array and
update the selectedPhotoId . Additionally, we remove the selected photo from photoSet .
Since photoSet is a state variable, the selected photo will be removed from the grid once
it's removed from the array.

The selected photo should be added to the dock. So, update the empty ScrollView of the
dock like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 766


ScrollView(.horizontal, showsIndicators: false) {
LazyHGrid(rows: [ GridItem() ]) {
ForEach(selectedPhotos) { photo in
Image(photo.name)
.resizable()
.scaledToFill()
.frame(minWidth: 0, maxWidth: .infinity)
.frame(height: 100)
.cornerRadius(3.0)
.onTapGesture {
photoSet.append(photo)
if let index = selectedPhotos.firstIndex(where: { $0.id == pho
to.id }) {
selectedPhotos.remove(at: index)
}
}
}
}
}

We create a horizontal grid to present the selected photos. For each photo, we attach the
onTapGesture function to it. When someone taps a photo in the dock, it will be added
back to the photo grid and removed from selectedPhotos . In other words, the photo will
be deleted from the dock.

If you test the app in the preview canvas, you should be able to select any of the photos in
the grid. When you tap a photo, it will be automatically added to the dock and that photo
will be removed from the grid. Conversely, you can tap a photo in the dock to move it
back to the photo grid.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 767


Figure 4. The selected photos are added to the dock

Using MatchedGeometryEffect to Animate the


Transition
The photo selection works well, but we can improve it by animating the transition of the
photo selection. Currently, the selected photo immediately appears in the dock. I want to
animate the transition of the photo selection so that, once selected, the photo appears to
fly from the photo grid to the dock.

With the matchedGeometryEffect modifier, it is very easy to implement this type of


animation. First, declare the namespace variable for this transition in ContentView :

@Namespace private var photoTransition

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 768


Next, attach the .matchedGeometryEffect modifier to both Image objects:

.matchedGeometryEffect(id: photo.id, in: photoTransition)

The trick here is to assign each image a distinct ID, so that the app will only animate the
change of the selected photo.

To enable the animation, attach the .animation modifier to the VStack and insert the
following line of code under .padding() :

.animation(.interactiveSpring(), value: selectedPhotoId)

This is the code you need to create the animated transtion. Run the app on a simulator or
in the preview canvas. When you tap a photo in the grid, you can see a beautiful
transition before it's added to the dock.

Figure 5. The selected photos are added to the dock with animation

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 769


Using ScrollViewReader to Move a Scroll View
The animated transition works well. However, did you notice a bug in the app? The dock
doesn't scroll automatically to display the most recently selected photo. If you select more
than four photos, you will need to manually scroll the dock to see the other selected
photos.

How can we fix this bug? SwiftUI offers a component called ScrollViewReader . As its
name suggests, this reader is designed to work with ScrollView . It allows developers to
programmatically move a scroll view to a specific location. To use ScrollViewReader , wrap
it around a ScrollView . Each child view should be given its own identifier. Then you call
the scrollTo function of the ScrollViewProxy with the specific ID to move the scroll view
to that particular location.

Figure 6. Understanding ScrollViewReader

Now let's return to our demo app. To programmatically scroll the dock's ScrollView , we
first need to give each photo an identifier. The scrollTo function requires an identifier of
the view to scroll to. Since each photo already has its unique identifier, we can use the
photo ID as the view's identifier.

To set the identifier of the Image views in the dock, attach the .id modifier to it:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 770


.id(photo.id)

Once we assign each Image view an identifier, wrap the horizontal ScrollView in a
ScrollViewReader like this:

ScrollViewReader { scrollProxy in
ScrollView(.horizontal, showsIndicators: false) {
LazyHGrid(rows: [ GridItem() ]) {
ForEach(selectedPhotos) { photo in
Image(photo.name)
.resizable()
.scaledToFill()
.frame(minWidth: 0, maxWidth: .infinity)
.frame(height: 100)
.cornerRadius(3.0)
.id(photo.id)
.matchedGeometryEffect(id: photo.id, in: photoTransition)
.onTapGesture {
photoSet.append(photo)
if let index = selectedPhotos.firstIndex(where: { $0.id ==
photo.id }) {
selectedPhotos.remove(at: index)
}
}
}
}
}
.frame(height: 100)
.padding()
.background(Color(.systemGray6))
.cornerRadius(5)
}

Finally, attach the .onChange function to the ScrollView of the dock like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 771


.onChange(of: selectedPhotoId) { oldValue, newValue in
withAnimation {
scrollProxy.scrollTo(newValue)
}
}

We use .onChange to listen for updates to the selectedPhotoId . Whenever the selected
photo ID changes, we call scrollTo with that photo ID to scroll the scroll view to that
location. This ensures the dock always shows the most recently selected photo. You can
run the app again to try it out.

Figure 7. Scrolling the dock automatically

Summary
In this chapter, we continue exploring the usage of matchedGeometryEffect and use this
modifier to create an amazing view transition. The modifier opens up many opportunities
for developers to improve the user experience of their iOS apps. Additionally, we
experimented with the new ScrollViewReader to learn how to use it to scroll a scroll view
programmatically.

For reference, you can download the complete project here:

Demo project
(https://www.appcoda.com/resources/swiftui5/SwiftUIGridViewAnimation.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 772


Chapter 35
Working with Tab View and Tab Bar
Customization
The tab bar interface appears in some of the most popular mobile apps such as Facebook,
Instagram, and Twitter. A tab bar appears at the bottom of an app screen and let users
quickly switch between different functions of an app. In UIKit, you use the
UITabBarController to create the tab bar interface. The SwiftUI framework provides a UI
component called TabView for developers to display tabs in the app.

In this chapter, we will show you how to create a tab bar interface using TabView , handle
the tab selection, and customize the appearance of the tab bar.

Using TabView to Create the Tab Bar Interface


Assuming you've created a SwiftUI project using Xcode, let's start with a simple text view
like this:

struct ContentView: View {


var body: some View {
Text("Home Tab")
.font(.system(size: 30, weight: .bold, design: .rounded))
}
}

To embed this text view in a tab bar, all you need to do is wrap it with the TabView

component and set the tab item description by attaching the .tabItem modifier like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 773


struct ContentView: View {
var body: some View {
TabView {
Text("Home Tab")
.font(.system(size: 30, weight: .bold, design: .rounded))
.tabItem {
Image(systemName: "house.fill")
Text("Home")
}
}
}
}

This creates a tab bar with a single tab item. In the sample code, the tab item has both
image and text, but you are free to remove either one of the those.

Figure 1. Tab bar with a single tab item

To display more tabs, you just need to add child views inside the TabView like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 774


TabView {
Text("Home Tab")
.font(.system(size: 30, weight: .bold, design: .rounded))
.tabItem {
Image(systemName: "house.fill")
Text("Home")
}

Text("Bookmark Tab")
.font(.system(size: 30, weight: .bold, design: .rounded))
.tabItem {
Image(systemName: "bookmark.circle.fill")
Text("Bookmark")
}

Text("Video Tab")
.font(.system(size: 30, weight: .bold, design: .rounded))
.tabItem {
Image(systemName: "video.circle.fill")
Text("Video")
}

Text("Profile Tab")
.font(.system(size: 30, weight: .bold, design: .rounded))
.tabItem {
Image(systemName: "person.crop.circle")
Text("Profile")
}
}

This gives you a tab bar interface with 4 tab items.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 775


Figure 2. Adding tab items in the tab bar

Customizing the Tab Bar Color


By default, the color of the tab bar item is set to blue. You can change its color by
attaching the .tint modifier to TabView like this:

TabView {

}
.tint(.red)

If you attach the modifier to TabView , the icon and text of the tab bar should be changed
to red.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 776


Figure 3. Changing the color of the tab bar

Switching Between Tabs Programmatically


Users tap the tab bar items to switch between tabs, which is automatically handled the
TabView . In some use cases, you may want to switch to a specific tab programmatically.
The TabView has another init method for this purpose. The method requires a state
variable which contains the tag value of the tab.

TabView(selection: $selection)

As an example, declare the following state variable in ContentView :

@State private var selection = 0

Here, we initialize the selection variable with a value of 0 , which corresponds to the
tag value of the first tab item. To complete the setup, we need to define the tag values for
each of the tab items. Let's update the code as follows and attach the tag modifier for
each of the tab items:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 777


TabView(selection: $selection) {
Text("Home Tab")
.font(.system(size: 30, weight: .bold, design: .rounded))
.tabItem {
Image(systemName: "house.fill")
Text("Home")
}
.tag(0)

Text("Bookmark Tab")
.font(.system(size: 30, weight: .bold, design: .rounded))
.tabItem {
Image(systemName: "bookmark.circle.fill")
Text("Bookmark")
}
.tag(1)

Text("Video Tab")
.font(.system(size: 30, weight: .bold, design: .rounded))
.tabItem {
Image(systemName: "video.circle.fill")
Text("Video")
}
.tag(2)

Text("Profile Tab")
.font(.system(size: 30, weight: .bold, design: .rounded))
.tabItem {
Image(systemName: "person.crop.circle")
Text("Profile")
}
.tag(3)
}

We assign a unique index to each tab item using the tag modifier. The TabView is also
bound to the selection value. To programmatically switch to your preferred tab, simply
update the value of the selection variable.

You can create a Next button that switches to the next tab like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 778


ZStack(alignment: .topTrailing) {
TabView(selection: $selection) {
.
.
.
}
.tint(.red)

Button {
selection = (selection + 1) % 4
} label: {
Text("Next")
.font(.system(.headline, design: .rounded))
.padding()
.foregroundStyle(.white)
.background(.red)
.cornerRadius(10.0)
.padding()
}
}

After making the changes, test the app in the preview canvas. You should be able step
through the tabs by tapping the Next button.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 779


Figure 4. Using the Next button to switch the tab

Hiding the Tab Bar in a Navigation View


You can embed a tab view in a navigation view by wrapping the TabView component with
NavigationStack like this:

NavigationStack {
TabView(selection: $selection) {
.
.
.
}

.navigationTitle("TabView Demo")
}

In UIKit, there is another option called hidesBottomBarWhenPushed , which allows you to


hide the tab bar when the UI is changed to the detail view in a navigation interface.
SwiftUI also has this feature built-in. You can modify the code like this to see it in action:

NavigationStack {

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 780


TabView(selection: $selection) {
List(1...10, id: \.self) { index in
NavigationLink(
destination: Text("Item #\(index) Details"),
label: {
Text("Item #\(index)")
.font(.system(size: 20, weight: .bold, design: .rounded))
})

}
.listStyle(.plain)
.tabItem {
Image(systemName: "house.fill")
Text("Home")
}
.tag(0)

Text("Bookmark Tab")
.font(.system(size: 30, weight: .bold, design: .rounded))
.tabItem {
Image(systemName: "bookmark.circle.fill")
Text("Bookmark")
}
.tag(1)

Text("Video Tab")
.font(.system(size: 30, weight: .bold, design: .rounded))
.tabItem {
Image(systemName: "video.circle.fill")
Text("Video")
}
.tag(2)

Text("Profile Tab")
.font(.system(size: 30, weight: .bold, design: .rounded))
.tabItem {
Image(systemName: "person.crop.circle")
Text("Profile")
}
.tag(3)
}
.tint(.red)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 781


.navigationTitle("TabView Demo")
}

We have just updated the code of the Home tab to display a list of items. Each list item is
now wrapped with a NavigationLink , allowing it to navigate to the detail view when
tapped. When you run the app in the simulator or preview canvas, you may notice that
the tab bar is hidden when navigating to the detail view.

Figure 5. Hiding the tab bar in the detailed view

For certain scenarios, you may not want the tab bar to be hidden when navigating to a
detail view. In such cases, you can create the navigation interface in the reverse order.
Instead of wrapping the tab view in a navigation view, you embed the navigation view in
a tab view. The updated code would look like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 782


TabView(selection: $selection) {
NavigationStack {
List(1...10, id: \.self) { index in
NavigationLink(
destination: Text("Item #\(index) Details"),
label: {
Text("Item #\(index)")
.font(.system(size: 20, weight: .bold, design: .rounded))
})

.navigationTitle("TabView Demo")
}
.tabItem {
Image(systemName: "house.fill")
Text("Home")
}
.tag(0)

.
.
.
}

Now when you navigate to the detail view of the item, the tab bar is still there.

Summary
In this chapter, we walked you through the basics of TabView , which is the UI component
in SwiftUI for building a tab view interface. We only show you how to work with the
built-in tab bar. However, keep in mind that you have the flexibility to create your own
custom tab bar if you require full customization. We will explore this topic in future
chapters.

For reference, you can download the complete project here:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 783


Demo project (https://www.appcoda.com/resources/swiftui5/SwiftUITabView.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 784


Chapter 36
Using AsyncImage in SwiftUI for
Loading Images Asynchronously
In WWDC 2021, Apple announced tons of new features for the SwiftUI framework to
make developers’ lives easier. AsyncImage was definitely one of the most anticipated
features introduced in iOS 15. If your app needs to retrieve and display images from
remote servers, this view saves you from writing your own code to handle asynchronous
download.

AsyncImage is a built-in view for loading and displaying remote images asynchronously.
All you need is to tell it what the image URL is, and AsyncImage will then do the heavy
lifting to grab the remote image and show it on the screen.

In this chapter, I will show you how to work with AsyncImage in SwiftUI projects.

The Basic Usage of AsyncImage


The simplest way to use AsyncImage is by specifying the image URL like this:

AsyncImage(url: URL(string: imageURL))

AsyncImage then connects to the given URL and downloads the remote image
asynchronously. It also automatically renders a placeholder in gray while the image is not
yet ready for display. Once the image is completely downloaded, AsyncImage displays the
image in its intrinsic size.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 785


Figure 1. Using AsyncImage

Assuming that you've created a new SwiftUI project in Xcode, you can replace the code in
ContentView like this to have a try:

struct ContentView: View {


let imageURL = "https://link.appcoda.com/testimage"

var body: some View {


AsyncImage(url: URL(string: imageURL))
}
}

In the preview canvas, you should immediately see a placeholder in gray, followed by the
image. It takes a few seconds for the image to download. Thus, iOS displays the
placeholder.

If you want to make the image smaller or larger, you can pass a scaling value to the
scale parameter like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 786


AsyncImage(url: URL(string: imageURL), scale: 2.0)

A value greater than 1.0 will scale down the image. Conversely, a value less than 1 will
make the image bigger.

Figure 2. Scaling down the image

Customizing the Image Size and PlaceHolder


AsyncImage provides another constructor for developers if you need further
customization:

init<I, P>(url: URL?, scale: CGFloat, content: (Image) -> I, placeholder: () -> P)

By initializing AsyncImage using the init above, we can resize and scale the downloaded
image to the preferred size. On top of that, we can provide our own implementation for
the placeholder. Here is a sample code snippet:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 787


AsyncImage(url: URL(string: imageURL)) { image in
image
.resizable()
.scaledToFill()
} placeholder: {
Color.purple.opacity(0.1)
}
.frame(width: 300, height: 500)
.cornerRadius(20)

In the code above, AsyncImage provides the resulting image for manipulation. We then
apply the resizable() and scaledToFill() modifier to resize the image. For the
AsyncImage view, we limit its size to 300×500 points.

The placeholder parameter allows us to create our own placeholder instead of using the
default one. Here, we display a placeholder in light purple.

Figure 3. Customizing the placeholder

Handling Different Phases of the Asynchronous


Operation

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 788


The AsyncImage view provides another constructor if you need to provide a better control
for the asynchronous download operation:

init(url: URL?, scale: CGFloat, transaction: Transaction, content: (AsyncImagePhase


) -> Content

AsyncImagePhase is an enum that keeps track of the current phase of the download
operation. You can provide detailed implementation for each of the phases including
empty, failure, and success.

Here is a sample code snippet:

AsyncImage(url: URL(string: imageURL)) { phase in


switch phase {
case .empty:
Color.purple.opacity(0.1)
case .success(let image):
image
.resizable()
.scaledToFill()
case .failure(_):
Image(systemName: "exclamationmark.icloud")
.resizable()
.scaledToFit()
@unknown default:
Image(systemName: "exclamationmark.icloud")
}
}
.frame(width: 300, height: 300)
.cornerRadius(20)

The empty state indicates that the image is not loaded. In this case, we display a
placeholder. For the success state, we apply a couple of modifiers and display it on
screen. The failure state allows you to provide an alternate view if there is any errors. In
the code above, we simply display a system image. If you updated the imageURL to an
invalid URL, the app now displays an exclamation mark image.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 789


Figure 4. Displaying an exclamation mark when the image download fails

Adding Animation with Transaction


The same init lets you specify an optional transaction when the phase changes. For
example, the following code snippet specifies to use a spring animation in the
transaction parameter:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 790


AsyncImage(url: URL(string: imageURL), transaction: Transaction(animation: .spring
())) { phase in
switch phase {
case .empty:
Color.purple.opacity(0.1)

case .success(let image):


image
.resizable()
.scaledToFill()

case .failure(_):
Image(systemName: "exclamationmark.icloud")
.resizable()
.scaledToFit()

@unknown default:
Image(systemName: "exclamationmark.icloud")
}
}
.frame(width: 300, height: 500)
.cornerRadius(20)

By doing so, you will see a fade-in animation when the image is downloaded. If you test
the code in the preview pane, it won’t work. Please make sure you test the code in a
simulator to see the animation.

You can also attach the transition modifier to the image view like this:

case .success(let image):


image
.resizable()
.scaledToFill()
.transition(.slide)

This creates a slide-in animation when displaying the resulting image.

Summary

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 791


In this chapter, we showed you a very useful view called AsyncImage . With this feature, it
is very easy to download and display remote images. All you need is specify the URL of
the image and the AsyncImage view will do all the heavy liftings for you.

For reference, you can download the complete demo project here:

Demo project
(https://www.appcoda.com/resources/swiftui5/SwiftUIAsyncImage.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 792


Chapter 37
Implementing Search Bar Using
Searchable
Prior to iOS 15, SwiftUI didn't come with a built-in modifier for handling search in List

views. Developers had to create their own solutions. In earlier chapters, I showed you
how to implement a search bar in SwiftUI using TextField and display search results.
With the release of iOS 15, the SwiftUI framework introduced a new modifier named
searchable for List views.

In this chapter, we will look into this modifier and see easily it is to implement search for
a list.

Basic Usage of Searchable


To demonstrate the usage of Searchable , please download the demo project from
https://www.appcoda.com/resources/swiftui5/SwiftUISearchableStarter.zip.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 793


Figure 1. The starter project

The starter project already includes a list view that displays a set of articles. What I want
to do is provide an enhancement by adding a search bar for filtering the articles. To add a
search bar to the list view, all you need to do is declare a state variable (e.g., searchText )
to hold the search text and attach a searchable modifier to the NavigationStack , like this:

struct ContentView: View {

@State var articles = sampleArticles


@State private var searchText = ""

var body: some View {


NavigationStack {
.
.
.
}
.searchable(text: $searchText)
}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 794


SwiftUI automatically renders the search bar for you and put it under the navigation bar
title. If you can't find the search bar, try to run the app and drag down the list view. The
search box should appear.

Figure 2. The searchable modifier automatically renders a search field

By default, it displays the word Search as a placeholder. In case if you want to change it,
you can write the .searchable modifier like this and use your own placeholder value:

.searchable(text: $searchText, prompt: "Search articles...")

Search Bar Placement


The .searchable modifier has a placement parameter that allows you to specify where to
place the search bar. By default, it's set to .automatic . On iPhone, the search bar is
placed under the navigation bar title. When you scroll up the list view, the search bar is
hidden.

If you want to permanently display the search field, you can change the .searchable

modifier and specify the placement parameter like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 795


.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .alway
s))

So far, we attach the .searchable modifier to the navigation view. You can actually attach
it to the List view and achieve the same result on iPhone.

Having that said, the placement of the .searchable modifier affects the position of the
search field when using split view on iPadOS. Let's change the code to use
NavigationSplitView :

NavigationSplitView {
List {
ForEach(articles) { article in
ArticleRow(article: article)
}

.listRowSeparator(.hidden)

}
.listStyle(.plain)

.navigationTitle("AppCoda")
} detail: {
Text("Article details")
}
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .alway
s))

As usual, we attach the .searchable modifier to the navigation stack. If you run the app
on iPad, the search bar is displayed on the sidebar of the split view.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 796


Figure 3. Search bar for split view on iPad

What if you want to place the search field in the detail view? You can try to insert the
following code right above the .navigationTitle modifier:

Text("Article details")
.searchable(text: $searchText)

iPadOS will then render an additional search bar at the top right corner of the detail view.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 797


Figure 4. Adding a search bar to the detail view

Again, you can further change the placement of the search bar by adjusting the value of
the placement parameter. Here is an example:

Text("Article details")
.searchable(text: $searchText, placement: .navigationBarDrawer)

By setting the placement parameter to .navigationBarDrawer , iPadOS places the search


field beneath the navigation bar title.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 798


Figure 5. Using .navigationBarDrawer to adjust the position of the search field

Performing Search and Displaying Search Results


There are different ways to filter a list of data. You can create a computed property that
performs real-time data filtering, or you can attach the .onChange modifier to keep track
of changes to the search field. Update the code of NavigationStack as follows:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 799


NavigationSplitView {
List {
ForEach(articles) { article in
ArticleRow(article: article)
}

.listRowSeparator(.hidden)

}
.listStyle(.plain)

.navigationTitle("AppCoda")
} detail: {
Text("Article details")
}
.searchable(text: $searchText)
.onChange(of: searchText) { oldValue, newValue in

if !newValue.isEmpty {
articles = sampleArticles.filter { $0.title.contains(newValue) }
} else {
articles = sampleArticles
}
}

The .onChange modifier is called whenever the user types in the search field. We then
perform the search in real-time by using the filter method. The Xcode preview doesn't
work properly for search, so please test the search feature on simulators.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 800


Figure 6. Performing search

Adding Search Suggestions


The .searchable modifier lets you add a list of search suggestions for displaying some
commonly used search terms or search history. For example, you can create tappable
search suggestion like this:

.searchable(text: $searchText) {
Text("SwiftUI").searchCompletion("SwiftUI")
Text("iOS 15").searchCompletion("iOS 15")
}

This displays a search suggestion with two tappable search terms. Users can either type
the search keyword or tap the search suggestion to perform the search.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 801


Figure 7. Displaying search suggestions

On iOS 16, Apple introduces an independent modifier named .searchSuggestions for


adding search suggestions. The same piece of the code can be written like this:

.searchable(text: $searchText)
.searchSuggestions {
Text("SwiftUI").searchCompletion("SwiftUI")
Text("iOS 15").searchCompletion("iOS 15")
}

Summary
The .searchable modifier simplifies the implementation of a search bar and saves us
time from creating our own solution. The downside is that this feature is only available
on iOS 15 (or later). If you're building an app that needs to support older versions of iOS,
you still need to build your own search bar.

For reference, you can download the complete demo project here:

Demo project
(https://www.appcoda.com/resources/swiftui5/SwiftUISearchable.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 802


Chapter 38
Creating Bar Charts and Line Charts
with the Charts Framework
You no longer need to build your own chart library or rely on third-party libraries to
create charts. The SwiftUI framework now comes with the Charts APIs. With this Charts
framework, available in iOS 16 or later, you can present animated charts with just a few
lines of code.

Building a Simple Bar Chart


The Charts framework is very simple to use. In brief, you build SwiftUI Charts by
defining what it calls Mark. Here is a quick example:

import SwiftUI
import Charts

struct ContentView: View {


var body: some View {
Chart {
BarMark(
x: .value("Day", "Monday"),
y: .value("Steps", 6019)
)

BarMark(
x: .value("Day", "Tuesday"),
y: .value("Steps", 7200)
)
}
}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 803


Whether you want to create a bar chart or a line chart, you start with the Chart view. In
the chart view, we define the bar marks to provide the chart data. The BarMark view is
used for creating a bar chart. Each BarMark view accepts the x and y values. The x

value is used for defining the chart data for the x-axis. In the code above, the label of the
x-axis is set to "Day". The y-axis shows the total number of steps.

If you enter the code in Xcode, the preview automatically displays the bar chart with two
vertical bars.

Figure 1. A simple bar chart

The code above shows you the simplest way to create a bar chart. However, instead of
hardcoding the chart data, you usually use the Charts API with a collection of data. Here
is an example:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 804


struct ContentView: View {
let weekdays = Calendar.current.shortWeekdaySymbols
let steps = [ 10531, 6019, 7200, 8311, 7403, 6503, 9230 ]

var body: some View {


Chart {
ForEach(weekdays.indices, id: \.self) { index in
BarMark(x: .value("Day", weekdays[index]), y: .value("Steps", step
s[index]))
}
}
}
}

We created two arrays ( weekdays and steps ) for the chart data. In the Chart view, we
loop through the weekdays array and present the chart data. If you have entered the code
in your Xcode project, the preview should render the bar chart as shown in Figure 2.

Figure 2. Using an array to represent the chart data

By default, the Charts API renders the bars in the same color. To display a different color
for each of the bars, you can attach the foregroundStyle modifier to the BarMark view:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 805


.foregroundStyle(by: .value("Day", weekdays[index]))

To add an annotation to each bar, you use the annotation modifier like this:

.annotation {
Text("\(steps[index])")
}

By making these changes, the bar chart becomes more visually appealing.

Figure 3. Displaying a bar chart with colors and annotations

To create a horizontal bar chart, you can simply swap the values of x and y parameter
of the BarMark view.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 806


Figure 4. A horizontal bar chart

Creating a Line Chart


Now that you understand how to create a bar chart, let's see how to create a line chart
using the Chart framework. As a demo, we will create a line chart that displays the
average temperature of Hong Kong, Taipei, and London from July 2021 to June 2022.

To store the weather data, we created a WeatherData struct. In your Xcode project, create
a new file named WeatherData using the Swift File template. Insert the following code in
the file:

struct WeatherData: Identifiable {


let id = UUID()
let date: Date
let temperature: Double

init(year: Int, month: Int, day: Int, temperature: Double) {


self.date = Calendar.current.date(from: .init(year: year, month: month, da
y: day)) ?? Date()
self.temperature = temperature
}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 807


let hkWeatherData = [
WeatherData(year: 2021, month: 7, day: 1, temperature: 30.0),
WeatherData(year: 2021, month: 8, day: 1, temperature: 29.0),
WeatherData(year: 2021, month: 9, day: 1, temperature: 30.0),
WeatherData(year: 2021, month: 10, day: 1, temperature: 26.0),
WeatherData(year: 2021, month: 11, day: 1, temperature: 23.0),
WeatherData(year: 2021, month: 12, day: 1, temperature: 19.0),
WeatherData(year: 2022, month: 1, day: 1, temperature: 18.0),
WeatherData(year: 2022, month: 2, day: 1, temperature: 15.0),
WeatherData(year: 2022, month: 3, day: 1, temperature: 22.0),
WeatherData(year: 2022, month: 4, day: 1, temperature: 24.0),
WeatherData(year: 2022, month: 5, day: 1, temperature: 26.0),
WeatherData(year: 2022, month: 6, day: 1, temperature: 29.0)
]

let londonWeatherData = [
WeatherData(year: 2021, month: 7, day: 1, temperature: 19.0),
WeatherData(year: 2021, month: 8, day: 1, temperature: 17.0),
WeatherData(year: 2021, month: 9, day: 1, temperature: 17.0),
WeatherData(year: 2021, month: 10, day: 1, temperature: 13.0),
WeatherData(year: 2021, month: 11, day: 1, temperature: 8.0),
WeatherData(year: 2021, month: 12, day: 1, temperature: 8.0),
WeatherData(year: 2022, month: 1, day: 1, temperature: 5.0),
WeatherData(year: 2022, month: 2, day: 1, temperature: 8.0),
WeatherData(year: 2022, month: 3, day: 1, temperature: 9.0),
WeatherData(year: 2022, month: 4, day: 1, temperature: 11.0),
WeatherData(year: 2022, month: 5, day: 1, temperature: 15.0),
WeatherData(year: 2022, month: 6, day: 1, temperature: 18.0)
]

let taipeiWeatherData = [
WeatherData(year: 2021, month: 7, day: 1, temperature: 31.0),
WeatherData(year: 2021, month: 8, day: 1, temperature: 30.0),
WeatherData(year: 2021, month: 9, day: 1, temperature: 30.0),
WeatherData(year: 2021, month: 10, day: 1, temperature: 26.0),
WeatherData(year: 2021, month: 11, day: 1, temperature: 22.0),
WeatherData(year: 2021, month: 12, day: 1, temperature: 19.0),
WeatherData(year: 2022, month: 1, day: 1, temperature: 17.0),
WeatherData(year: 2022, month: 2, day: 1, temperature: 17.0),
WeatherData(year: 2022, month: 3, day: 1, temperature: 21.0),
WeatherData(year: 2022, month: 4, day: 1, temperature: 23.0),
WeatherData(year: 2022, month: 5, day: 1, temperature: 24.0),

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 808


WeatherData(year: 2022, month: 6, day: 1, temperature: 27.0)
]

The Chart initializer takes in a list of Identifiable objects. This is why we make the
WeatherData conform to the Identifiable protocol. For each city, we create an array to
store the sample weather data.

In the project navigator, create a new file named SimpleLineChartView using the SwiftUI
View template. To create any type of chart using the Charts framework, you first need to
import the Charts framework:

import Charts

Declare a variable to store the sample weather data for the cities:

let chartData = [ (city: "Hong Kong", data: hkWeatherData),


(city: "London", data: londonWeatherData),
(city: "Taipei", data: taipeiWeatherData) ]

In the body variable, update the code like this to create the line chart:

VStack {
Chart {
ForEach(hkWeatherData) { item in
LineMark(
x: .value("Month", item.date),
y: .value("Temp", item.temperature)
)
}
}
.frame(height: 300)
}

The code above plots a line chart displaying the average temperature of Hong Kong. The
ForEach statement loops through all the items stored in hkWeatherData . For each item,
we create a LineMark object where the x axis is set to the date and the y axis is set to

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 809


the average temperature.

Optionally, you can resize the chart using the frame modifier. If you preview the code in
the Xcode preview, you should see the line chart in Figure 5.

Figure 5. A simple line chart

Customizing Chart Axes


You can customize both x and y axes by using the chartXAxis and chartYAxis modifiers
respectively. Let's say, if we want to display the month labels using the digit format, we
can attach the chartXAxis modifier to the Chart view like this:

.chartXAxis {
AxisMarks(values: .stride(by: .month)) { value in
AxisGridLine()
AxisValueLabel(format: .dateTime.month(.defaultDigits))
}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 810


Inside chartXAxis , we create a visual mark called AxisMarks for the values of month. For
each value, we display a value label by using a specific format. This line of code tells
SwiftUI chart to use the digit format:

.dateTime.month(.defaultDigits)

On top of that, we added some grid lines by using AxisGridLine .

For the y-axis, instead of display the axis on the trailing (or right) side, we want to switch
it to the leading (or left) side. To do that, attach the chartYAxis modifier like this:

.chartYAxis {
AxisMarks(position: .leading)
}

If you've made the change, Xcode preview should update the chart like figure 6. The y-
axis is moved to the left side and the format of month is changed. Plus, you should see
some grid lines.

Figure 6. Customizing the chart axes

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 811


Customizing the Background Color of the Plot Area
The chartPlotStyle modifier allows you to change the background color of the plot area.
Attach the modifier to the Chart view like this:

.chartPlotStyle { plotArea in
plotArea
.background(.blue.opacity(0.1))
}

We can then change the plot area using the background modifier. As an example, we
change the plot area to light blue.

Figure 7. Customizing the chart background

Creating a Multi-line Chart


Now that the chart displays a single source of data (i.e. the weather data of Hong Kong),
how can we display the weather data of London and Taipei in the same line chart?

You can rewrite the code of Chart view like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 812


Chart {
ForEach(chartData, id: \.city) { series in
ForEach(series.data) { item in
LineMark(
x: .value("Month", item.date),
y: .value("Temp", item.temperature)
)
}
.foregroundStyle(by: .value("City", series.city))
}
}

We use another ForEach loop to iterate through all the cities in the chart data. Here, we
use the foregroundStyle modifier to apply a different color for each line. You don't have
to specify the color; SwiftUI will automatically pick the color for you.

Figure 8. Rendering a multi-line chart

Right now, all the cities share the same symbol. If you want to use a distinct symbol,
place the following line of code after foregroundStyle :

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 813


.symbol(by: .value("City", series.city))

Now each city has its own symbol in the line chart.

Figure 9. Adding symbols for the lines

Customizing the Interpolation Method


You can alter the interpolation method of the line chart by attaching the
interpolationMethod modifier to LineMark . Here is an example:

.interpolationMethod(.stepStart)

If you change the interpolation method to .stepStart , the line chart now looks like that
displayed in figure 10.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 814


Figure 10. Customizing the interpolation method

Other than .stepStart , you can try out the following options:

cardinal
catmullRom
linear
monotone
stepCenter
stepEnd

Summary
The Charts framework is a valuable addition to SwiftUI. Even if you're new to SwiftUI,
you can create attractive charts with just a few lines of code. Although this chapter
concentrates on line charts and bar charts, the Charts API simplifies the process of
converting your chart into other formats, such as pie and donut charts. For more
information, refer to the Swift Charts documentation. Additionally, we will explain how
to create pie charts and donut charts in a later chapter.

For reference, you can download the complete project here:

Demo project (https://www.appcoda.com/resources/swiftui5/SwiftUICharts.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 815


Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 816
Chapter 39
Capturing Text within Image Using
Live Text API
iOS 15 came with a very useful feature known as Live Text. You may have heard of the
term OCR (short for Optical Character Recognition), which is the process of converting
an image of text into a machine-readable text format. This is what Live Text is all about.

Live Text is built into the Camera app and Photos app. If you haven't tried out this
feature, simply open the Camera app. When you point the device's camera at an image of
text, you will find a Live Text button at the lower-right corner. By tapping the button, iOS
automatically captures the text for you. You can then copy and paste it into other
applications (e.g., Notes).

This is a very powerful and convenient feature for most users. As a developer, wouldn't it
be great if you could incorporate this Live Text feature into your own app? In iOS 16,
Apple released the Live Text API for developers to power their apps with Live Text. In
this chapter, let's see how to use the Live Text API with SwiftUI.

Using DataScannerViewController
In the WWDC session about Capturing Machine-readable Codes and Text with
VisionKit, Apple's engineer showed the following diagram:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 817


Figure 1. APIs provided by AVfoundation and Vision framework

Text recognition is not a new feature in iOS 16. In older versions of iOS, you could use
APIs from the AVFoundation and Vision framework to detect and recognize text.
However, the implementation was quite complicated, especially for those who were new
to iOS development.

In iOS 16, all of the above is simplified with a new class called DataScannerViewController

in VisionKit. By using this view controller, your app can automatically display a camera
UI with Live Text capability.

To use the class, you first import the VisionKit framework and then check if the device
supports the data scanner feature:

DataScannerViewController.isSupported

The Live Text API only supports devices released in 2018 or newer with Neural engine.
On top of that, you also need to check the availability to see if the user approves the use of
data scanner:

DataScannerViewController.isAvailable

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 818


Once both checks come back with positive results, you are ready to start the scanning.
Here is the sample code to launch a camera with Live Text:

let dataScanner = DataScannerViewController(


recognizedDataTypes: [.text()],
qualityLevel: .balanced,
isHighlightingEnabled: true
)

present(dataScanner, animated: true) {


try? dataScanner.startScanning()
}

All you need to do is create an instance of DataScannerViewController and specify the


recognized data types. For text recognition, you pass .text() as the data type. Once the
instance is ready, you can present it and start the scanning process by calling the
startScanning() method.

Working with
DataScannerViewController in SwiftUI
The DataScannerViewController class now only supports UIKit. For SwiftUI, it requires a
bit of work. We have to adopt the UIViewControllerRepresentable protocol to use the class
in SwiftUI projects. Assuming you've created a new Xcode project, you can now create a
new file using the Swift template and name it DataScanner . To port the
DataScannerViewController to SwiftUI, create a new struct named DataScanner and
implement the UIViewControllerRepresentable protocol like this:

import SwiftUI
import VisionKit

struct DataScanner: UIViewControllerRepresentable {

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 819


This struct accepts two binding variables: one for triggering the data scanning and the
other is a binding for storing the scanned text.

@Binding var startScanning: Bool


@Binding var scanText: String

To successfully adopt the UIViewControllerRepresentable protocol, we need to implement


the following methods:

func makeUIViewController(context: Context) -> DataScannerViewController {


let controller = DataScannerViewController(
recognizedDataTypes: [.text()],
qualityLevel: .balanced,
isHighlightingEnabled: true
)

return controller
}

func updateUIViewController(_ uiViewController: DataScannerViewController, context


: Context) {

if startScanning {
try? uiViewController.startScanning()
} else {
uiViewController.stopScanning()
}
}

In the makeUIViewController method, we return an instance of DataScannerViewController .


For updateUIViewController , we start (or stop) the scanning depending on the value of
startScanning .

To capture the scanned text, we have to adopt the following method of


DataScannerViewControllerDelegate :

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 820


func dataScanner(_ dataScanner: DataScannerViewController, didTapOn item: Recogniz
edItem) {
.
.
.
}

The method is called when the user taps the detected text, so we will implement it like
this:

class Coordinator: NSObject, DataScannerViewControllerDelegate {


var parent: DataScanner

init(_ parent: DataScanner) {


self.parent = parent
}

func dataScanner(_ dataScanner: DataScannerViewController, didTapOn item: Reco


gnizedItem) {
switch item {
case .text(let text):
parent.scanText = text.transcript
default: break
}
}

func makeCoordinator() -> Coordinator {


Coordinator(self)
}

We check the recognized item and store the scanned text if any text is recognized. Lastly,
insert this line of code in the makeUIViewController method to configure the delegate:

controller.delegate = context.coordinator

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 821


This controller is now ready for use in SwiftUI views.

Capturing Text Using DataScanner


As an example, we will build a text scanner app with a very simple user interface. When
the app is launched, it will automatically display the camera view for live text. When text
is detected, users can tap on the text to capture it. The scanned text will be displayed in
the lower part of the screen.

Figure 2. Live text demo

Assuming you've created a standard SwiftUI project, open ContentView.swift and the
VisionKit framework.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 822


import VisionKit

Next, declare a couple of state variables to control the operation of the data scanner and
the scanned text.

@State private var startScanning = false


@State private var scanText = ""

For the body part, let's update the code like this:

VStack(spacing: 0) {
DataScanner(startScanning: $startScanning, scanText: $scanText)
.frame(height: 400)

Text(scanText)
.frame(minWidth: 0, maxWidth: .infinity, maxHeight: .infinity)
.background(in: Rectangle())
.backgroundStyle(Color(uiColor: .systemGray6))

}
.task {
if DataScannerViewController.isSupported && DataScannerViewController.isAvaila
ble {
startScanning.toggle()
}
}

We start the data scanner when the app launches. But before that, we call
DataScannerViewController.isSupported and DataScannerViewController.isAvailable to
ensure that Live Text is supported on the device.

The demo app is almost ready to test. Since Live Text requires camera access, please
remember to go to the project configuration and add the key Privacy - Camera Usage
Description in the Info.plist file. Specify the reason why your app needs to access the
device's camera.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 823


Figure 3. Adding a key to enable camera access

After the changes, you can deploy the app to a real iOS device and test the Live Text
function.

Other than English, Live Text also supports French, Italian, German, Spanish, Chinese,
Portuguese, Japanese, and Korean.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 824


Figure 4. App Demo

Summary
It's great to see Apple open up the Live Text feature to iOS developers. Although the new
DataScannerViewController is not specifically designed for SwiftUI, we can easily port it to
our Swift project and incorporate the Live Text feature into our own apps. If you are
going to publish your next app update, consider bundling this great feature. It will
definitely improve the user experience.

For reference, you can download the complete project here:

Demo project (https://www.appcoda.com/resources/swiftui5/SwiftUILiveText.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 825


Chapter 40
How to Use ShareLink for Sharing
Data Like Text and Photos
In iOS 16, SwiftUI released a new view called ShareLink . When users tap on the share
link, it presents a share sheet for users to share content with other applications or copy
the data for later use.

The ShareLink view is designed to share any type of data. In this chapter, we will show
you how to use ShareLink to let users share text, URLs, and images.

Basic Usage of ShareLink


Let's begin with an example. Assuming you've created a new Xcode project, you can
create a share link for sharing a URL by writing the code like this:

struct ContentView: View {


private let url = URL(string: "https://www.appcoda.com")!

var body: some View {


VStack {
ShareLink(item: url)
}
}
}

SwiftUI automatically renders a Share button with a small icon.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 826


Figure 1. The share button

When tapped, iOS brings up a share sheet for users to perform further actions such as
copy and adding the link to Reminders.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 827


Figure 2. Displaying the share sheet

To share text, instead of URL, you can simply pass the a string to the item parameter.

ShareLink(item: "Check out this new feature on iOS 16")

Customizing the Appearance of Share


Link
To customize the appearance of the link, you can provide the view content in the closure
like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 828


ShareLink(item: url) {
Image(systemName: "square.and.arrow.up")
}

In this case, SwiftUI only displays the share icon for the link.

Figure 3. Customizing the share button

Alternatively, you can present a label with a system image or custom image:

ShareLink(item: url) {
Label("Tap me to share", systemImage: "square.and.arrow.up")
}

When initializing the ShareLink instance, you can include two additional parameters to
provide extra information about the shared item:

ShareLink(item: url, subject: Text("Check out this link"), message: Text("If you w
ant to learn Swift, take a look at this website.")) {
Image(systemName: "square.and.arrow.up")
}

The subject parameter allows you to include a title for the URL or any item you want to
share. The message parameter allows you to specify the description of the item.
Depending on the activities that the user shares to, iOS will present the subject, message,

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 829


or both. For example, if you add the URL to Reminders, the Reminders app displays the
preset message.

Figure 4. The Reminders app displays the preset message

Sharing Images
Other than URLs, you can share images using ShareLink . Here is a sample code snippet:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 830


struct ContentView: View {
private let photo = Image("bigben")

var body: some View {


VStack(alignment: .leading, spacing: 10) {
photo
.resizable()
.scaledToFit()

ShareLink(item: photo, preview: SharePreview("Big Ben", image: photo))

}
.padding(.horizontal)
}
}

For the item parameter, you specify the image to share, and you provide a preview of the
image by passing an instance of SharePreview . In the preview, you specify the title of the
image and the thumbnail. When you tap the Share button, iOS displays a share sheet
with the image preview.

Figure 5. Displaying a share sheet with the image preview

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 831


Conforming to Transferable
Other than URLs, the item parameter accepts any objects that conforms to the
Transferable protocol. In iOS, the following types are the standard Transferable types:

String
Data
URL
Attributed String
Image

Transferable is a protocol that describes how a type interacts with transport APIs
such as drag and drop or copy and paste.

So what if you have a custom object, how can you make it transferable? Let's say, you
create the following Photo structure:

struct Photo: Identifiable {


var id = UUID()
var image: Image
var caption: String
var description: String
}

To let ShareLink share this object, you have to adopt the Transferable protocol for
Photo and implement the transferRepresentation property:

extension Photo: Transferable {


static var transferRepresentation: some TransferRepresentation {
ProxyRepresentation(exporting: \.image)
}
}

There are several Transfer Representations, including ProxyRepresentation ,


CodableRepresentation , DataRepresentation , and FileRepresentation . In the code above,
we use ProxyRepresentation , which is a transfer representation that uses another type's

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 832


transfer representation as its own. Here, we use the Image 's built-in Transferable

conformance.

Figure 6. Using the built-in Transferable conformance

Since Photo now conforms to Transferable , you can pass the Photo instance to
ShareLink :

ShareLink(item: photo, preview: SharePreview(photo.caption, image: photo.image))

When users tap the Share button, the app brings up the share sheet for sharing the
photo.

Summary
This chapter shows you how to use ShareLink for sharing text, URLs, and images. In fact,
this new view allows you to share any type of data as long as the type conforms to the
Transferable protocol.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 833


For custom types, you can adopt the protocol and provide a transfer representation by
using one of the built-in TransferRepresentation types. We briefly discussed the
ProxyRepresentation type. If you need to share a file between applications, you can use
the FileRepresentation type.

For reference, you can download the complete project here:

Demo project (https://www.appcoda.com/resources/swiftui5/SwiftUISharelink.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 834


Chapter 41
Using ImageRenderer to Convert
SwiftUI Views into Images
ImageRenderer is another new API for SwiftUI that came with iOS 16. It allows you to
easily convert any SwiftUI views into an image. The implementation is straightforward.
You instantiate an instance of ImageRenderer with a view for the conversion:

let renderer = ImageRenderer(content: theView)

You can then access the cgImage or uiImage property to retrieve the generated image.

As always, I love to demonstrate the usage of an API with an example. In Chapter 38, we
built a line chart using the new Charts framework. Let's see how to let users save the
chart as an image in the photo album and share it using ShareLink .

Revisit the Chart View

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 835


Figure 1. Line chart demo

First, let's revisit the code of the ChartView example. We used the new API of the Charts

framework to create a line chart and display the weather data. Here is the code snippet:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 836


var body: some View {
VStack {
Chart {
ForEach(chartData, id: \.city) { series in
ForEach(series.data) { item in
LineMark(
x: .value("Month", item.date),
y: .value("Temp", item.temperature)
)
}
.foregroundStyle(by: .value("City", series.city))
.symbol(by: .value("City", series.city))
}
}
.chartXAxis {
AxisMarks(values: .stride(by: .month)) { value in
AxisGridLine()
AxisValueLabel(format: .dateTime.month(.defaultDigits))

}
}
.chartPlotStyle { plotArea in
plotArea
.background(.blue.opacity(0.1))
}
.chartYAxis {
AxisMarks(position: .leading)
}
.frame(width: 350, height: 300)
.padding(.horizontal)

}
}

To follow this tutorial, you can first download the starter project from
https://www.appcoda.com/resources/swiftui5/SwiftUIImageRendererStarter.zip. Before
using ImageRenderer , let's refactor the code above in ContentView.swift into a separate
view like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 837


struct ChartView: View {
let chartData = [ (city: "Hong Kong", data: hkWeatherData),
(city: "London", data: londonWeatherData),
(city: "Taipei", data: taipeiWeatherData)
]

var body: some View {


VStack {
Chart {
ForEach(chartData, id: \.city) { series in
ForEach(series.data) { item in
LineMark(
x: .value("Month", item.date),
y: .value("Temp", item.temperature)
)
}
.foregroundStyle(by: .value("City", series.city))
.symbol(by: .value("City", series.city))
}
}
.chartXAxis {
AxisMarks(values: .stride(by: .month)) { value in
AxisGridLine()
AxisValueLabel(format: .dateTime.month(.defaultDigits))

}
.chartPlotStyle { plotArea in
plotArea
.background(.blue.opacity(0.1))
}
.chartYAxis {
AxisMarks(position: .leading)
}
.frame(width: 350, height: 300)

.padding(.horizontal)

}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 838


}

Next, we declare a variable in ContentView to hold the chart view:

var chartView = ChartView()

Converting the View into an Image using


ImageRenderer
Now we are ready to convert the chart view into an image. To allow users to save the
chart view image in the photo album, we will add a button named Save to Photos.

Let's implement the button like this:

var body: some View {

VStack(spacing: 20) {
chartView

HStack {
Button {
let renderer = ImageRenderer(content: chartView)

if let image = renderer.uiImage {


UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}
} label: {
Label("Save to Photos", systemImage: "photo")
}
.buttonStyle(.borderedProminent)
}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 839


In the closure of the button, we create an instance of ImageRenderer with chartView and
retrieve the rendered image using the uiImage property. Then, we call
UIImageWriteToSavedPhotosAlbum to save the image to the photo album.

Note: You need to add a key named Privacy - Photo Library Usage Description in the
info.plist before the app can properly save an image to the built-in photo album.

Adding a Share Button

Figure 2. Adding a share button

In the previous chapter, you learned how to use ShareLink to present a share sheet for
content sharing. With ImageRenderer , you can now easily build a function for users to
share the chart view as an image.

For convenience purpose, let's refactor the code for image rendering into a separate
method:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 840


@MainActor
private func generateSnapshot() -> UIImage {
let renderer = ImageRenderer(content: chartView)

return renderer.uiImage ?? UIImage()


}

The generateSnapshot method converts the chartView into an image.

Note: If you are new to @MainActor , you can check out this article.

With this helper method, we can create a ShareLink like this in the VStack view:

ShareLink(item: Image(uiImage: generateSnapshot()), preview: SharePreview("Weather


Chart", image: Image(uiImage: generateSnapshot())))
.buttonStyle(.borderedProminent)

Now when you tap the Share button, the app captures the line chart and lets you share it
as an image.

Figure 3. Displaying the share sheet

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 841


Adjusting the Image Scale
You may notice that the resolution of the rendered image is a bit low. The ImageRenderer

class has a property named scale that you can adjust to change the scale of the rendered
image. By default, its value is set to 1.0. To generate an image with a higher resolution,
you can set it to 2.0 or 3.0 . Alternatively, you can set the value to the scale of the
screen:

renderer.scale = UIScreen.main.scale

Summary
The ImageRenderer class has made it very easy to convert any SwiftUI views into an
image. If your app supports iOS 16 or later, you can use this new API to create convenient
features for your users. Besides rendering images, ImageRenderer also allows you to
render a PDF document. For further details, you can refer to the official documentation.

For reference, you can download the complete project here:

Demo project
(https://www.appcoda.com/resources/swiftui5/SwiftUIImageRenderer.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 842


Chapter 42
Creating PDF Documents Using
ImageRenderer
Earlier, we showed you how to use ImageRenderer to capture a SwiftUI view and save it as
an image. This new class, introduced in iOS 16, can also allow you to convert a view into a
PDF document.

In this chapter, we will build on top of the previous demo and add the Save to PDF
function.

Revisit the Demo App


If you haven't read the previous chapter, I suggest you to check it out first. It already
covered the basics of ImageRenderer and explained the implementation of the demo app.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 843


Figure 1. App Demo

To follow this chapter, you can first download the starter project from
https://www.appcoda.com/resources/swiftui5/SwiftUIImageRendererPDFStarter.zip.

I have made some modifications to the demo app by adding a heading and a caption for
the line chart. You can refer to the code of the ChartView struct below:

struct ChartView: View {


let chartData = [ (city: "Hong Kong", data: hkWeatherData),
(city: "London", data: londonWeatherData),
(city: "Taipei", data: taipeiWeatherData)
]

var body: some View {


VStack {
Text("Building Line Charts in SwiftUI")
.font(.system(size: 40, weight: .heavy, design: .rounded))
.multilineTextAlignment(.center)
.padding()

Chart {

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 844


ForEach(chartData, id: \.city) { series in
ForEach(series.data) { item in
LineMark(
x: .value("Month", item.date),
y: .value("Temp", item.temperature)
)
}
.foregroundStyle(by: .value("City", series.city))
.symbol(by: .value("City", series.city))
}
}
.chartXAxis {
AxisMarks(values: .stride(by: .month)) { value in
AxisGridLine()
AxisValueLabel(format: .dateTime.month(.defaultDigits))

}
.chartPlotStyle { plotArea in
plotArea
.background(.blue.opacity(0.1))
}
.chartYAxis {
AxisMarks(position: .leading)
}
.frame(width: 350, height: 300)

.padding(.horizontal)

Text("Figure 1. Line Chart")


.padding()

}
}
}

The demo app now also comes with a PDF button for saving the chart view in a PDF
document.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 845


Saving the Chart View as a PDF
Document Using ImageRenderer
What we are going to do is create a PDF document for the ChartView using
ImageRenderer . While it only takes a couple of lines of code to convert a SwiftUI view into
an image, we need a little more work for PDF rendering.

For image conversion, you can access the uiImage property to get the rendered image. To
draw the chart into a PDF, we will use the render method of ImageRenderer . Here is what
we are going to implement:

Look for the document directory and prepare the rendered path for the PDF file (e.g.
linechart.pdf).
Prepare an instance of CGContext for drawing.
Call the render method of the renderer to render the PDF document.

For the implementation, we create a new method named exportPDF in ContentView .


Below is the code of the method :

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 846


@MainActor
private func exportPDF() {
guard let documentDirectory = FileManager.default.urls(for: .documentDirectory
, in: .userDomainMask).first else { return }

let renderedUrl = documentDirectory.appending(path: "linechart.pdf")

if let consumer = CGDataConsumer(url: renderedUrl as CFURL),


let pdfContext = CGContext(consumer: consumer, mediaBox: nil, nil) {

let renderer = ImageRenderer(content: chartView)


renderer.render { size, renderer in
let options: [CFString: Any] = [
kCGPDFContextMediaBox: CGRect(origin: .zero, size: size)
]

pdfContext.beginPDFPage(options as CFDictionary)

renderer(pdfContext)
pdfContext.endPDFPage()
pdfContext.closePDF()
}
}

print("Saving PDF to \(renderedUrl.path())")


}

The first two lines of the code retrieves the document directory of the user and set up the
file path of the PDF file (i.e. line chart.pdf ). We then create the instance of CGContext .
The mediaBox parameter is set to nil. In this case, Core Graphics uses a default page size
of 8.5 by 11 inches (612 by 792 points).

The renderer closure receives two parameters: the current size of the view and a
function that renders the view to the CGContext . To begin the PDF page, we call the
context's beginPDFPage method. The renderer method draws the chart view. Remember
that you need to close the PDF document to complete the whole operation.

To call this exportPDF method, we update a PDF button like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 847


Button {
exportPDF()
} label: {
Label("PDF", systemImage: "doc.plaintext")
}
.buttonStyle(.borderedProminent)

You can run the app in a simulator to have a test. After you tap the PDF button, you
should see the following message in the console:

Saving PDF to /Users/simon/Library/Developer/CoreSimulator/Devices/CA9B849B-36C5-4


608-9D72-B04C468DA87E/data/Containers/Data/Application/04415B8A-7485-48F0-8DA2-59B
97C2B529D/Documents/linechart.pdf

If you open the file in Finder (choose Go > Go to Folder...), you should see a PDF
document like below.

Figure 2. The rendered PDF document

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 848


To adjust the position of the drawing, you can insert this line of code before calling
renderer :

pdfContext.translateBy(x: 0, y: 200)

This will move the chart to the upper part of the document.

Figure 3. Adjusting the chart position

Make the PDF file available to the Files


app
You may wonder why the PDF file cannot be found in the Files app. Before you can make
the file available to the built-in Files app, you have to change a couple of settings in
Info.plist . Switch to Info.plist and add the following keys:

UIFileSharingEnabled - Application supports iTunes file sharing

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 849


LSSupportsOpeningDocumentsInPlace - Supports opening documents in place

Set the value of the keys to Yes. Once you enable both options, run the app on the
simulator again. Open the Files app and navigate to the On My iPhone location. You
should see the app's folder. Inside the folder, you will find the PDF document.

Figure 4. Saving the PDF file to the Files app

Summary
Not only can you create an image from a view, but ImageRenderer also allows developers
to turn a view into a PDF document. With this API, introduced in iOS 16, you can easily
add some PDF-related features to your iOS apps.

For reference, you can download the complete project here:

Demo project
(https://www.appcoda.com/resources/swiftui5/SwiftUIImageRendererPDF.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 850


Chapter 43
Using Gauge to Display Progress and
Create a Speedometer
In iOS 16, SwiftUI introduced a new view called Gauge for displaying progress. You can
use it to show any values within a range. In this chapter, we will see how to use the
Gauge view and work with different gauge styles.

A gauge is a view that shows a current level of a value in relation to a specified finite
capacity, very much like a fuel gauge in an automobile. Gauge displays are
configurable; they can show any combination of the gauge’s current value, the
range the gauge can display, and a label describing the purpose of the gauge itself.

- Apple's official documentation

The simplest way to use Gauge is like this:

struct ContentView: View {


@State private var progress = 0.5

var body: some View {


Gauge(value: progress) {
Text("Upload Status")
}
}
}

In the most basic form, a gauge has a default range from 0 to 1. If we set the value

parameter to 0.5 , SwiftUI renders a progress bar indicating the task is 50% complete.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 851


Figure 1. A basic gauge with descriptive labels

Optionally, you can provide labels for the current, minimum, and maximum values:

Gauge(value: progress) {
Text("Upload Status")
} currentValueLabel: {
Text(progress.formatted(.percent))
} minimumValueLabel: {
Text(0.formatted(.percent))
} maximumValueLabel: {
Text(100.formatted(.percent))
}

Using Custom Range


The default range is set to 0 and 1. However, you can provide your own custom range. For
example, if you are building a speedometer with a maximum speed of 200km/h, you can
specify the range in the in parameter as follows:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 852


struct SpeedometerView: View {
@State private var currentSpeed = 100.0

var body: some View {


Gauge(value: currentSpeed, in: 0...200) {
Text("Speed")
} currentValueLabel: {
Text("\(currentSpeed.formatted(.number))km/h")
} minimumValueLabel: {
Text(0.formatted(.number))
} maximumValueLabel: {
Text(200.formatted(.number))
}
}
}

In the code above, we set the range to 0...200 . If you have already added the
SpeedometerView to the #Preview code block, your preview should show the progress bar
filled halfway, as we set the current speed to 100km/h.

Figure 2. A basic gauge with custom range

Using Image Labels

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 853


You are not limited to use text labels for displaying ranges and current value. Here is an
example:

Gauge(value: currentSpeed, in: 0...200) {


Image(systemName: "gauge.medium")
.font(.system(size: 50.0))
} currentValueLabel: {
HStack {
Image(systemName: "gauge.high")
Text("\(currentSpeed.formatted(.number))km/h")
}
} minimumValueLabel: {
Text(0.formatted(.number))
} maximumValueLabel: {
Text(200.formatted(.number))
}

We changed the text label of the gauge to a system image. For the current value label, we
created a stack to arrange the image and text. Your preview should display the gauge as
shown in Figure 3.

Figure 3. A speedometer view with image labels

Customizing the Gauge Style

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 854


Figure 4. Customizing the gauge view's color

The default color of the Gauge view is blue. To customize its color, attach the tint

modifier and set the value to your preferred color like this:

Gauge(value: currentSpeed, in: 0...200) {


Image(systemName: "gauge.medium")
.font(.system(size: 50.0))
} currentValueLabel: {
HStack {
Image(systemName: "gauge.high")
Text("\(currentSpeed.formatted(.number))km/h")
}
} minimumValueLabel: {
Text(0.formatted(.number))
} maximumValueLabel: {
Text(200.formatted(.number))
}
.tint(.purple)

The look and feel of the Gauge view is very similar to that of ProgressView . You can
optionally customize the Gauge view using the gaugeStyle modifier, which supports
several built-in styles.

linearCapacity

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 855


This is the default style that displays a bar that fills from leading to trailing edges. Figure
4 shows a sample gauge in this style.

accessoryLinear
This style displays a bar with a point marker to indicate the current value.

Figure 5. Using the accessoryLinear style

accessoryLinearCapacity
For this style, the gauge is still displayed as a progress bar but it's more compact.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 856


Figure 6. Using the accessoryLinearCapacity style

accessoryCircular
Instead of displaying a bar, this style displays an open ring with a point marker to
indicate the current value.

Figure 7. Using the accessoryCircular style

accessoryCircularCapacity

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 857


This style displays a closed ring that's partially filled in to indicate the gauge's current
value. The current value is also displayed at the center of the gauge.

Figure 8. Using the accessoryCircularCapacity style

Creating a Custom Gauge Style

Figure 9. A custom gauge view

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 858


The built-in gauge styles are limited, but SwiftUI allows you to create your own gauge
style. Let me show you a quick demo to build a gauge style like the one displayed in
Figure 9.

To create a custom gauge style, you need to adopt the GaugeStyle protocol and provide
your own implementation. Here is our implementation of the custom style:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 859


struct SpeedometerGaugeStyle: GaugeStyle {
private var purpleGradient = LinearGradient(gradient: Gradient(colors: [ Color
(red: 207/255, green: 150/255, blue: 207/255), Color(red: 107/255, green: 116/255,
blue: 179/255) ]), startPoint: .trailing, endPoint: .leading)

func makeBody(configuration: Configuration) -> some View {


ZStack {

Circle()
.foregroundStyle(Color(.systemGray6))

Circle()
.trim(from: 0, to: 0.75 * configuration.value)
.stroke(purpleGradient, lineWidth: 20)
.rotationEffect(.degrees(135))

Circle()
.trim(from: 0, to: 0.75)
.stroke(Color.black, style: StrokeStyle(lineWidth: 10, lineCap: .b
utt, lineJoin: .round, dash: [1, 34], dashPhase: 0.0))
.rotationEffect(.degrees(135))

VStack {
configuration.currentValueLabel
.font(.system(size: 80, weight: .bold, design: .rounded))
.foregroundStyle(.gray)
Text("KM/H")
.font(.system(.body, design: .rounded))
.bold()
.foregroundStyle(.gray)
}

}
.frame(width: 300, height: 300)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 860


To conform to the GaugeStyle protocol, we need to implement the makeBody method to
present our own gauge style. The configuration bundles the current value and value label
of the gauge. In the code above, we use these two values to display the current speed and
compute the arc length.

Once we have implemented our custom gauge style, we can apply it by attaching the
gaugeStyle modifier as follows:

struct CustomGaugeView: View {

@State private var currentSpeed = 140.0

var body: some View {


Gauge(value: currentSpeed, in: 0...200) {
Image(systemName: "gauge.medium")
.font(.system(size: 50.0))
} currentValueLabel: {
Text("\(currentSpeed.formatted(.number))")

}
.gaugeStyle(SpeedometerGaugeStyle())

}
}

I created a separate view for the demo. To preview the CustomGaugeView , you need to add
another #Preview code block:

#Preview("CustomGauge") {
CustomGaugeView()
}

That's it. If you've made the changes, your preview should show a custom gauge.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 861


Figure 10. A custom gauge view

Summary
In this chapter, you learned how to use the new Gauge view to build a speedometer.
Other than a speedometer, you can use Gauge to display progress or any other
measurements. The Gauge view is highly customizable; you just need to adopt the
GaugeStyle protocol and create your own Gauge style.

For reference, you can download the complete project here:

Demo project (https://www.appcoda.com/resources/swiftui5/SwiftUIGauge.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 862


Chapter 44
Creating Grid Layout Using Grid APIs
SwiftUI introduced a new Grid API for composing grid-based layout along with the
release of iOS 16. While you can arrange the same layout using VStack and HStack , the
Grid view makes it a lot easier. A Grid view is a container view that arranges other
views in a two-dimensional layout.

The Basics
Let's start with a simple grid. To create a 2x2 grid, you write the code like below:

struct ContentView: View {


var body: some View {
Grid {
GridRow {
Color.purple
Color.orange
}

GridRow {
Color.green
Color.yellow
}
}
}
}

Assuming you've created a SwiftUI project in Xcode, you should see a 2x2 grid, filled with
different colors.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 863


Figure 1. A basic grid

To create a 3x3 grid, you just need to add another GridRow . And, for each GridRow , you
insert one more child view.

Figure 2. A 3x3 grid

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 864


Comparing Grid Views and Stack Views
You may be wondering why we need to use the Grid APIs when we can create the same
grid UI using HStack and VStack . So, why did Apple introduce these new APIs? It's true
that you can build the same grid with stack views. However, there is a major difference
that makes Grid views more desirable when building grid layouts.

Let's create another 2x2 grid using the code below:

struct ContentView: View {


var body: some View {
Grid {
GridRow {
Image(systemName: "trash")
.font(.system(size: 100))
Image(systemName: "trash")
.font(.system(size: 50))
}

GridRow {
Color.green
Color.yellow
}
}
}
}

This time, the first row displays two system images and the second row shows the color
views. Your preview should show a grid like below.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 865


Figure 3. A 2x2 grid displaying images and color views

To build the same grid layout using nested VStack and HStack , we can write the code
like this:

VStack {
HStack {
Image(systemName: "trash")
.font(.system(size: 100))
.frame(minWidth: 0, maxWidth: .infinity)
Image(systemName: "trash")
.font(.system(size: 50))
.frame(minWidth: 0, maxWidth: .infinity)
}

HStack {
Color.green
Color.yellow
}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 866


By default, each column of the Grid view occupies equal space in a row. For HStack , we
have to attach the frame modifier to make each image take up the same space across the
row.

The Grid view makes it easier to create grid views and provides several modifiers to
customize the grid layout.

Using gridCellColumns to Merge Cells


When creating a grid view, you may want to display a single cell in a specific row, while
other rows continue to show multiple cells. The gridCellColumns modifier is designed for
merging cells. Here is an example:

Grid {
GridRow {
Image(systemName: "trash")
.font(.system(size: 100))
Image(systemName: "trash")
.font(.system(size: 50))
}

GridRow {
Color.purple
.overlay {
Image(systemName: "magazine.fill")
.font(.system(size: 100))
.foregroundColor(.white)
}
.gridCellColumns(2)
}
}

The second row only has one cell. We attach the gridCellColumn modifier and set its value
to 2 to merge the cells. If you do not use the modifier, you'll see a blank cell.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 867


Figure 4. Merging cells

Adding a Blank Cell

Figure 5. A 3x3 grid view

Now let's create another 3x3 grid view like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 868


struct ContentView: View {
var body: some View {
Grid {
GridRow {
IconView(name: "cloud")
IconView(name: "cloud")
IconView(name: "cloud")
}

GridRow {
IconView(name: "cloud")
IconView(name: "cloud")
IconView(name: "cloud")
}

GridRow {
IconView(name: "cloud")
IconView(name: "cloud")
IconView(name: "cloud")
}
}
}
}

struct IconView: View {


var name: String = "trash"

var body: some View {


Image(systemName: name)
.frame(width: 100, height: 100)
.background(in: Rectangle())
.backgroundStyle(.purple)
.foregroundStyle(.white.shadow(.drop(radius: 1, y: 3.0)))
.font(.system(size: 50))
}
}

What if we want to display a blank view for the cell at the center of the grid? To make this
happen, you can use the following code to add a blank cell:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 869


Color.clear
.gridCellUnsizedAxes([.horizontal, .vertical])

If you replace the center cell with the code above, you will see a blank cell at the center of
the grid.

Figure 6. Adding a blank cell

The gridCellUnsizedAxes modifier prevents the blank cell from taking up more space than
the other cells in the row or column need?. If you omit the modifier, you will achieve a
grid layout like figure 7.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 870


Figure 7. Omitting the gridCellUnsizedAxes modifier

Adjusting the Cell Spacing


To control the vertical and horizontal spacing between cells, you can use the
horizontalSpacing and verticalSpacing parameter when instantiating a Grid view:

Grid(horizontalSpacing: 0, verticalSpacing: 0) {
.
.
.
}

If you need to add some spacing between rows, you can attach the padding modifier to
GridRow . Figure 8 shows you an example.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 871


Figure 8. Adjusting the row spacing

Controlling the Cell Alignment


The Grid view has an optional parameter named alignment for you to configure the
default alignment of the cells. For example, here sets the default alignment to .bottom :

Grid(alignment: .bottom, horizontalSpacing: 0, verticalSpacing: 0) {


.
.
.
}

If you've changed the code, the black box should align to the bottom of the cell.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 872


Figure 9. Setting the default grid alignment

To override the default alignment setting, the cell itself can attach the gridCellAnchor

modifier to change the alignment. Figure 10 shows an example.

Figure 10. Changing the cell alignment

Summary

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 873


The introduction of the Grid APIs provides developers with one more option to build
grid-based layouts. While you can use HStack and VStack to build a similar layout,
Grid views save you quite a lot of code and make your code more readable. If your app
only needs to support the latest version of iOS, give the Grid API a try and build some
interesting layouts.

For reference, you can download the complete project here:

Demo project (https://www.appcoda.com/resources/swiftui5/SwiftUIGrid.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 874


Chapter 45
Switching Layout with AnyLayout
Starting from iOS 16, SwiftUI provides AnyLayout and the Layout protocol for
developers to create customized and complex layouts. AnyLayout is a type-erased
instance of the layout protocol. You can use AnyLayout to create a dynamic layout that
responds to users’ interactions or environment changes.

In this chapter, you will learn how to use AnyLayout to switch between vertical and
horizontal layout.

Using AnyLayout
First, create a new Xcode project using the App template. Name the project
SwiftUIAnyLayout or any other name you prefer. What we are going to build is a simple
demo app that switches the UI layout when you tap the stack view. Figure 1 shows the UI
layout for different orientations.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 875


Figure 1. Switching between vertical and horizontal stacks using AnyLayout

The app initially arranges three images vertically using VStack . When a user taps the
stack view, it changes to a horizontal stack. With AnyLayout , you can implement the
layout like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 876


struct ContentView: View {
@State private var changeLayout = false

var body: some View {


let layout = changeLayout ? AnyLayout(HStackLayout()) : AnyLayout(VStackLa
yout())

layout {
Image(systemName: "bus")
.font(.system(size: 80))
.frame(width: 120, height: 120)
.background(in: RoundedRectangle(cornerRadius: 5.0))
.backgroundStyle(.green)
.foregroundStyle(.white)

Image(systemName: "ferry")
.font(.system(size: 80))
.frame(width: 120, height: 120)
.background(in: RoundedRectangle(cornerRadius: 5.0))
.backgroundStyle(.yellow)
.foregroundStyle(.white)

Image(systemName: "scooter")
.font(.system(size: 80))
.frame(width: 120, height: 120)
.background(in: RoundedRectangle(cornerRadius: 5.0))
.backgroundStyle(.indigo)
.foregroundStyle(.white)

}
.animation(.default, value: changeLayout)
.onTapGesture {
changeLayout.toggle()
}
}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 877


We define a layout variable to hold an instance of AnyLayout . Depending on the value of
changeLayout , this layout changes between horizontal and vertical layouts. The
HStackLayout (or VStackLayout ) behaves like a HStack (or VStack ) but conforms to the
Layout protocol so you can use it in the conditional layouts.

By attaching the animation to the layout, the layout change can be animated. Now when
you tap the stack view, it switches between vertical and horizontal layouts.

Switching Layouts based on the device's orientation


Currently, the app lets users change the layout by tapping the stack view. In some
applications, you may want to change the layout based on the device's orientation and
screen size. In this case, you can capture the orientation change by using the
.horizontalSizeClass variable:

@Environment(\.horizontalSizeClass) var horizontalSizeClass

And then you update the layout variable like this:

let layout = horizontalSizeClass == .regular ? AnyLayout(HStackLayout()) : AnyLayo


ut(VStackLayout())

Say, for example, you rotate an iPhone 14 Pro Max to landscape, the layout changes to
horizontally stack view.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 878


Figure 2. Switching to a horizontal stack view when the device is in landscape
orientation

In most cases, we use SwiftUI's built-in layout containers like HStackLayout and
VStackLayout to compose layouts. What if those layout containers are not suitable for
arranging the type of layouts you need? The Layout protocol allows you to define your
custom layout. All you need to do is define a custom layout container by creating a type
that conforms to the Layout protocol and implementing its required methods:

sizeThatFits(proposal:subviews:cache:) - reports the size of the composite layout


view.
placeSubviews(in:proposal:subviews:cache:) - assigns positions to the container’s
subviews.

Summary
The introduction of AnyLayout allows us to customize and change the UI layout with just
a couple of lines of code. This undoubtedly helps us build more elegant and engaging UIs.
In the earlier demo, I showed you how to switch layouts based on the screen orientation.
In fact, you can apply the same technique to other scenarios, like the size of the Dynamic
Type.

For reference, you can download the complete project here:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 879


Demo project
(https://www.appcoda.com/resources/swiftui5/SwiftUIAnyLayout.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 880


Chapter 46
Working with Maps and Annotations
MapKit is a powerful framework that allows developers to add maps, annotations, and
location-based features to their iOS applications. With SwiftUI, you can easily integrate
MapKit into your app and create interactive and dynamic maps that offer a great user
experience. In this tutorial, we will explore how to work with maps and annotations in
SwiftUI, and how to customize the map style and camera position.

The MapKit Basics


Let's start with the basics of MapKit. The MapKit framework includes a Map view that
developers can use to embed a map in any SwiftUI project. Here is an example:

import SwiftUI
import MapKit

struct ContentView: View {


var body: some View {
Map()
}
}

Before using the Map view, you have to import the MapKit framework. Then, to create a
map, simply instantiate a Map view. If you've opened the Preview canvas in Xcode, you
should see a full screen map in the simulator.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 881


Figure 1. The basic map view

Changing the Initial Position with Map Camera


Instead of displaying a default location, the Map view has another init method for you
to change the initial position of the map:

init(
initialPosition: MapCameraPosition,
bounds: MapCameraBounds? = nil,
interactionModes: MapInteractionModes = .all,
scope: Namespace.ID? = nil
) where Content == MapContentView<Never, EmptyMapContent>

You can an instance of MapCameraPosition as the initial position of the map.


MapCameraPosition contains various properties that you can use to control which place or
region is displayed, including:

automatic

item(MKMapItem) - for displaying a specific map item.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 882


region(MKCoordinateRegion) - for displaying a specific region.
rect(MKMapRect) - for displaying specific map boundaries.
camera(MapCamera) - for displaying an existing camera position.
userLocation() - for displaying the user’s location

For instance, you can instruct the map to display a specific region by using
.region(MKCoordinateRegion) :

Map(initialPosition: .region(MKCoordinateRegion(center: CLLocationCoordinate2D(lat


itude: 40.75773, longitude: -73.985708), span: MKCoordinateSpan(latitudeDelta: 0.0
5, longitudeDelta: 0.05))))

The coordinates in the above sample is the GPS coordinates of Times Square in New
York. The value of span is used to define your desired zoom level of the map. The
smaller the value, the higher is the zoom level.

Figure 2. Changing the initial position of the map view

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 883


If you have a particular location for display, you can pass a map item as the initial
position. Here is a sample code snippet:

extension CLLocationCoordinate2D {
static let bigBen = CLLocationCoordinate2D(latitude: 51.500685, longitude: -0.
124570)
}

struct ContentView: View {

var body: some View {


Map(initialPosition: .item(MKMapItem(placemark: .init(coordinate: .bigBen)
)))
}
}

Animating the Change of Map Position


The Map view also provides an additional init method that accepts a binding to
MapCameraPosition . If you need to change the position of the map, this init method is
more appropriate:

@State private var position: MapCameraPosition = .automatic

Map(position: $position) {
.
.
.
}

For example, if you want to add two buttons for users to switch between two locations,
you can write the code like this:

extension CLLocationCoordinate2D {
static let bigBen = CLLocationCoordinate2D(latitude: 51.500685, longitude: -0.
124570)
static let towerBridge = CLLocationCoordinate2D(latitude: 51.505507, longitude

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 884


: -0.075402)
}

struct ContentView: View {

@State private var position: MapCameraPosition = .automatic

var body: some View {


Map(position: $position)
.onAppear {
position = .item(MKMapItem(placemark: .init(coordinate: .bigBen)))
}
.safeAreaInset(edge: .bottom) {
HStack {
Button(action: {
withAnimation {
position = .item(MKMapItem(placemark: .init(coordinate
: .bigBen)))
}
}) {
Text("Big Ben")
}
.tint(.black)
.buttonStyle(.borderedProminent)

Button(action: {
withAnimation {
position = .item(MKMapItem(placemark: .init(coordinate
: .towerBridge)))
}
}) {
Text("Tower Bridge")
}
.tint(.black)
.buttonStyle(.borderedProminent)
}
}
}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 885


By wrapping the position variable with withAnimation , the map view will automatically
animate the position change.

Figure 3. Animating the map position

This animation works even better when you provide a MapCamera with a pitch angle to
create a 3D perspective. To see what happens, you can try changing the position of Big
Ben in the following line of code:

position = .camera(MapCamera(
centerCoordinate: .bigBen,
distance: 800,
heading: 90,
pitch: 50))

When you preview the map view, the camera angle adjusts to show a 3D perspective of
the region.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 886


Figure 4. Using the 3D perspective map style

Adding Markers and Annotations

Figure 5. The map marker

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 887


Markers are a useful feature in MapKit that allow you to display content at a specific
coordinate on the map. It adds an extra layer of information to your map, such as a store
or a restaurant. Markers can be customized with a system image and tint color, making
them visually distinct and easy to recognize. Whether you're building a navigation app or
a travel guide, markers are a valuable tool that can help you create a better user
experience.

To add a marker, you can create the Marker view in the map content builder closure like
this:

Map(position: $position) {
Marker("Pickup here", coordinate: .pickupLocation)
}

Optionally, you can customize the Marker object with a system image. To change the
color of the marker, use the tint modifier:

Marker("Pickup here",
systemImage: "car.front.waves.up",
coordinate: .pickupLocation)
.tint(.purple)

In addition to Marker , SwiftUI now includes an Annotation view in iOS 17 for indicating
a location on a map. It functions similarly to Marker , but offers greater flexibility for
customization.

To add an annotation, you create an Annotation view in the map content closure. Here is
a sample code snippet for adding a simple annotation:

Map(position: $position) {
Annotation("Pick up", coordinate: .pickupLocation, anchor: .bottom) {
Image(systemName: "car.front.waves.up")
}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 888


You have the flexibility to customize the annotation in a variety of ways. By attaching
different modifiers to it, you can change its appearance and behavior. Additionally, you
can use stack views to arrange the different components of the annotation and create a
layout that suits your needs. Here is an example:

Annotation("Pick up", coordinate: .pickupLocation, anchor: .bottom) {


ZStack {
Circle()
.foregroundStyle(.indigo.opacity(0.5))
.frame(width: 80, height: 80)

Image(systemName: "car.front.waves.up")
.symbolEffect(.variableColor)
.padding()
.foregroundStyle(.white)
.background(Color.indigo)
.clipShape(Circle())
}
}

This results an animated annotation as shown in the below illustration.

Figure 6. A custom annotation

Changing the Map Style

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 889


By default, the map view renders the map in a standard style. However, you can change
the style by using the mapStyle modifier:

Map {

}
.mapStyle(.imagery(elevation: .realistic))

This creates a map style based on satellite imagery. By specifying a realistic elevation,
the map view renders a 3D map with a realistic appearance.

Figure 7. The map view renders a 3D map with a realistic appearance

Optionally, you can also change the map style to hybrid like this:

.mapStyle(.hybrid)

Points of Interest

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 890


If you want to highlight points of interest for a certain location, you can use an
MKLocalSearch object to create a search request and display them using Marker . Insert
the following function in ContentView :

private func search(location: CLLocationCoordinate2D, query: String) {


let request = MKLocalSearch.Request()
request.naturalLanguageQuery = query
request.resultTypes = .pointOfInterest
request.region = MKCoordinateRegion(
center: location,
latitudinalMeters: 100,
longitudinalMeters: 100)

Task {
let search = MKLocalSearch(request: request)
let response = try? await search.start()
searchResults = response?.mapItems ?? []
}
}

This search function takes a location variable and a string query. For example, if you
want to search for restaurants around Tower Bridge, you can initiate the call like this:

search(location: .towerBridge, query: "restaurants")

We utilize MKLocalSearch to generate a search query for "restaurants" and specify the
result types as .pointOfInterest . The region property provides a location hint for the
search. The returned result is an array of MKMapItem , which we store in a state variable
that hasn't been declared yet. Therefore, please insert this line of code at the beginning of
ContentView :

@State private var searchResults: [MKMapItem] = []

To initiate the search, insert this line of code in the action closure of the Big Ben button:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 891


search(location: .bigBen, query: "restaurants")

And, insert another line of code in the action closure of the Tower Bridge button:

search(location: .towerBridge, query: "restaurants")

When a user taps either of the buttons to switch the location, the app will also search and
display the points of interest (POI) around that location.

Lastly, to pinpoint the POI on the map, we will use Marker to map the locations. Insert
these lines of code in the Map closure:

ForEach(searchResults, id: \.self) { result in


Marker(item: result)
}

Once you have made the necessary changes, you can test the app in the preview canvas.
The app should display the restaurants located around Tower Bridge or Big Ben,
depending on the button you click.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 892


Figure 8. Displaying points of interest

Summary
This tutorial covers how to work with maps and annotations in SwiftUI, using the MapKit
framework. The latest version of SwiftUI offers additional APIs and views for developers
to further customize the map view. By now, you should know how to embed a map in
your app and add an annotation to highlight a location on the map.

For reference, you can download the demo project here:

Demo project
(https://www.appcoda.com/resources/swiftui5/SwiftUIMapDemo.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 893


Chapter 47
Working with Preview Macro
The Preview feature in SwiftUI allows developers to see what their app will look like in
real-time, without having to run the app on a device or simulator. This feature is
incredibly useful for developers who want to quickly iterate on their designs and make
sure everything looks and functions as intended. With the introduction of Macros in iOS
17, the Preview feature has become even more powerful and versatile, allowing for even
more customization and flexibility. In this article, we'll explore how to use the new
Preview Macro in SwiftUI and take a look at some of its exciting new features.

The New #Preview Macro


Prior to the introduction of the new #Preview macro, you define a structure that
conforms to the PreviewProvider protocol to create a preview of a view. Here is an
example:

struct ContentView_Previews: PreviewProvider {


static var previews: some View {
ContentView()
}
}

With the #Preview macros, you tell Xcode to create a preview like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 894


// The basic syntax
#Preview {
ContentView()
}

// Configure the preview's name


#Preview("Pie Chart View") {
PieChartView()
}

As you can see, #Preview simplifies the way we define previews. Optionally, you can pass
the #Preview macro a name to configure the preview's name.

Figure 1. The Preview Basics

You can use this to set up a preview for any view as needed. Xcode will then render the
preview, which will appear directly in the canvas.

Previewing Multiple Views

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 895


Figure 2. Previewing multiple views

When using PreviewProvider , you can embed several views to preview using Group .

struct ArticleView_Previews: PreviewProvider {


static var previews: some View {
Group {
ArticleListView()
.previewDisplayName("Article List View")

ArticleView()
.previewDisplayName("Article View")
}
}
}

If you use the #Preview macro, you can define multiple blocks of #Preview sections. For
example, to preview both ArticleListView and ArticleView , you can create two #Preview

code blocks as follows:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 896


#Preview("Article List View") {
ArticleListView()
}

#Preview("Article View") {
ArticleView()
}

Preview Views in Landscape Orientation

Figure 3. Previewing a view in landscape mode

The #Preview macro has an optional traits parameter that allows developers to
customize the orientation of the simulator. To preview views in landscape mode, you can
pass .landscapeLeft or .landscapeRight to the traits parameter. Here is an example:

#Preview("Article List View", traits: .landscapeLeft) {


ArticleListView()
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 897


Preview without Device Frame and Fixed Layout
The traits parameters can take another value named .sizeThatFitsLayout so that you
can preview the view without any device frame.

Figure 4. Previewing a view without device frame

On top of .sizeThatFitsLayout , you can also use .fixedLayout to preview the view in a
specific size.

#Preview("Article List View", traits: .fixedLayout(width: 300, height: 300)) {


ArticleListView()
}

Writing UIKit Previews


The Preview feature is no longer limited to SwiftUI. Even if you use UIKit, you can set up
a preview for your UIKit views or view controllers using the #Preview macro. To preview
a view controller, you can instantiate it in the code block:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 898


#Preview {
var controller = ViewController()

return controller
}

If your view controller is designed in the storyboard, you can also preview it using the
macro. Here is the sample code snippet:

#Preview("From Storyboard") {
let storyboard = UIStoryboard(name: "Main", bundle: nil)

var controller = storyboard.instantiateViewController(withIdentifier: "ViewCon


troller")

return controller
}

Assuming you have assigned a storyboard ID for the view controller, you can create the
view controller using the instantiateViewController method. This is how you use
#Preview to preview a UIKit view controller.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 899


Figure 5. Using Preview macro with UIKit views

Summary
The Preview feature in SwiftUI allows developers to see what their app will look like in
real-time, without having to run the app on a device or simulator. The #Preview macro
introduced in iOS 17 makes the preview code cleaner and simpler. It has become even
more powerful and versatile, allowing you to preview views developed in UIKit.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 900


Chapter 48
Building Pie Charts and Donut Charts
Pie charts and donut charts are two popular chart types used in data visualization. Prior
to iOS 17, if you want to create these types of charts using SwiftUI, you’ll have to build the
charts on your own using components like Path and Arc . In chapter 8, we showed you
a technique on implementing pie charts and donut charts from scratch. However, since
the release of iOS 17, this is no longer necessary. SwiftUI simplifies the process of
creating these charts by introducing a new mark type called SectorMark . This makes it
easy for developers to build all kinds of pie and donut charts.

In this chapter, we will guide you through the process of building pie charts and donut
charts using SwiftUI. On top of that, you’ll also learn how to add interactivity to the
charts.

Revisiting Bar Charts


Let’s start by implementing a simple bar chart using the Charts framework. Assuming
you have created a new SwiftUI project, insert the lines of code below to initialize the
sample data for the bar chart:

private var coffeeSales = [


(name: "Americano", count: 120),
(name: "Cappuccino", count: 234),
(name: "Espresso", count: 62),
(name: "Latte", count: 625),
(name: "Mocha", count: 320),
(name: "Affogato", count: 50)
]

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 901


These are just some random data on coffee sales for chart rendering. For simplicity, I
used an array of tuples to hold the data. The Charts framework makes it very easy for
developers to create a bar chart from these data.

First, import the Charts framework and replace the body part with the following code:

VStack {
Chart {
ForEach(coffeeSales, id: \.name) { coffee in
BarMark(
x: .value("Type", coffee.name),
y: .value("Cup", coffee.count)
)
.foregroundStyle(by: .value("Type", coffee.name))
}
}
}
.padding()

Whether you're creating a bar chart or a pie chart, it all starts with the Chart view.
Within this view, we define a set of BarMark for rendering a vertical bar chart that plots
coffee types on the x-axis and counts on the y-axis. The foregroundStyle modifier
automatically assigns a unique color for each of the bars.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 902


Figure 1. A bar chart

You can easily create a different type of bar chart by altering some of the BarMark

parameters.

Figure 2. A one dimensional bar chart

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 903


For example, if you want to create a one dimensional bar chart, you just need to provide
the values for the x or y axis:

VStack {
Chart {
ForEach(coffeeSales, id: \.name) { coffee in

BarMark(
x: .value("Cup", coffee.count)
)
.foregroundStyle(by: .value("Type", coffee.name))
}
}
.frame(height: 100)
}
.padding()

By default, it shows the accumulated count in the x-axis. If you want to normalized the
values, simple specify the stacking parameter for BarMark like this:

BarMark(
x: .value("Cup", coffee.count),
stacking: .normalized
)
.foregroundStyle(by: .value("Type", coffee.name))

Creating Pie Charts with SectorMark


Now that we’ve built a bar chart, let’s how it can be converted to a pie chart using the new
SectorMark introduced in iOS 17.

The SectorMark , as the name suggests, represents a sector of the pie chart that
corresponds to a specific category. Each SectorMark is defined by the value it represents.
By using SectorMark , developers can easily create various types of pie (or donut charts)
without having to build them from scratch using components like Path and Arc .

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 904


For example, if we want to convert the bar chart into a pie chart, all you need to do is
replace BarMark with SectorMark like this:

Chart {
ForEach(coffeeSales, id: \.name) { coffee in

SectorMark(
angle: .value("Cup", coffee.count)
)
.foregroundStyle(by: .value("Type", coffee.name))
}
}
.frame(height: 500)

Instead of specifying the value of x-axis, you pass the values to the angle parameter.
SwiftUI will automatically compute the angular size of the sector and generate the pie
chart.

Figure 3. Using SectionMark to create a pie chart

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 905


Customizing the Pie Chart
SectorMark comes with a number of parameters for you to customize each of the sectors.
To add some spacing between sectors, you can provide the value of angularInset .

Figure 4. Using angularInset to add some spacing between sectors

You can control the size of the sectors by specifying a value for the outerRadius

parameter. For example, if you want to highlight the Latte sector by making it a bit
larger, you can add the outerRadius parameter.

Figure 5. Enlarging one of the sectors

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 906


To add a label for each sector, you can attach the annotation modifier to SectorMark and
set the position to .overlay :

.annotation(position: .overlay) {
Text("\(coffee.count)")
.font(.headline)
.foregroundStyle(.white)
}

Here, we simply overlay a text label on each sector to display the count.

Figure 6. Adding a text label on each sector

Converting the Pie Chart to Donut Chart


So, how can you create a donut chart? The new SectorMark is so powerful that you just
need to add a single line of code to turn the pie chart into a donut chart. There is an
optional parameter for SectorMark that I haven’t mentioned before.

To create a donut chart, simply specify the innerRadius parameter of the sector mark and
pass it your preferred value:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 907


SectorMark(
angle: .value("Cup", coffee.count),
innerRadius: .ratio(0.65),
angularInset: 2.0
)

The value of innerRadius is either a size in points, or a .ratio or .inset relative to the
outer radius. By having a value greater than zero, you create a hole in the pie and turn the
chart into a donut chart.

Figure 7. Converting a pie chart to a donut chart using innerRadius

Optionally, you can attach a cornerRadius modifier to the sector marks to round the
corners of the sector.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 908


Figure 8. Using cornerRadius to round the corners of the sectors

You can also add a view to the chart’s background by attaching the chartBackground

modifier to the Chart view. Here is an example.

Figure 9. Configuring the chart background

Interacting with Charts


Other than introducing SectorMark , the new version of SwiftUI comes with new Chart
APIs for handling user interactions. For both pie and donut charts, you attach the
chartAngleSelection modifier and pass it a binding to capture user’s touches:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 909


@State private var selectedCount: Int?

Chart {

.
.
.

}
.chartAngleSelection(value: $selectedCount)

The chartAngleSelection modifier takes in a binding to a plottable value. Since all our
plottable values are in integer, we declare a state variable of the type Int . With the
implementation, the chart now can detect user’s touch and capture the selected count of
the donut (or pie) chart.

Figure 10. Detecting user's touch

You may attach the onChange modifier to the chart to reveal the selected value.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 910


.onChange(of: selectedCount) { oldValue, newValue in
if let newValue {
print(newValue)
}
}

The value captured doesn’t directly tell you the exact sector the user touched. Instead, it
gives a value of the selected coffee count. For example, if the user taps the trailing edge of
the green sector, SwiftUI returns you a value of 354 .

Figure 11. Understanding how the value is captured

To figure out the sector from the given value, we need to create a new function. This
function takes the selected value and returns the name of the corresponding sector.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 911


private func findSelectedSector(value: Int) -> String? {

var accumulatedCount = 0

let coffee = coffeeSales.first { (_, count) in


accumulatedCount += count
return value <= accumulatedCount
}

return coffee?.name
}

With the implementation above, we can declare a state variable to hold the selected
sector and make some interesting changes to the donut chart.

@State private var selectedSector: String?

When a sector of the chart is selected, we will dim the remaining sectors to highlight the
selected sector. Update the onChange modifier like this:

.onChange(of: selectedCount) { oldValue, newValue in


if let newValue {
selectedSector = findSelectedSector(value: newValue)
} else {
selectedSector = nil
}
}

And then attach the opacity modifier to SectorMark like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 912


SectorMark {

...

}
.opacity(selectedSector == nil ? 1.0 : (selectedSector == coffee.name ? 1.0 : 0.5)
)

We keep the original opacity when there is no selected sector. Once a user touches a
specific sector, we change the opacity of those unselected sectors. Below shows the
appearance of the donut chart when the Latte sector is selected.

Figure 12. Changing the opacity when a section is touched

Summary

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 913


In this tutorial, we have guided you through the process of building pie charts and donut
charts using SwiftUI. Prior to iOS 17, if you wanted to create these types of charts using
SwiftUI, you had to build the charts on your own using components like Path . However,
with the introduction of the new Chart API called SectorMark , it's now easier than ever to
create all kinds of pie and donut charts. As you can see, turning a bar chart into a pie (or
donut) chart only requires some simple changes.

We also discussed with you how to add interactivity to the charts. This is another new
feature of the SwiftUI Charts framework. With a few lines of code, you can detect users’
touches and highlight a certain part of the chart. I hope you enjoy reading this chapter
and start building great charts with all the new functionalities provided in iOS 17.

For reference, you can download the demo project here:

Demo project
(https://www.appcoda.com/resources/swiftui5/SwiftUIPieDonutChart.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 914


Chapter 49
Detecting scroll positions in
ScrollView with SwiftUI
One common question that arises when using scroll views in SwiftUI is how to detect the
scroll position. Prior to the release of iOS 17, developers had to come up with their own
solutions to capture the scroll position. However, the new version of SwiftUI has updated
ScrollView with a new modifier called scrollPosition . With this new feature, developers
can effortlessly identify the item that is being scrolled to.

In this chapter, we will take a closer look at this new modifier and examine how it can
assist you in detecting scroll positions in scroll views.

Using the ScrollPosition Modifier


Let’s begin with a simple scroll view that displays a list of 50 items.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 915


struct ColorListView: View {
let bgColors: [Color] = [ .yellow, .blue, .orange, .indigo, .green ]

var body: some View {


ScrollView {
LazyVStack(spacing: 10) {
ForEach(0...50, id: \.self) { index in

bgColors[index % 5]
.frame(height: 100)
.overlay {
Text("\(index)")
.foregroundStyle(.white)
.font(.system(.title, weight: .bold))
}
}
}
}
.contentMargins(.horizontal, 10.0, for: .scrollContent)

}
}

The code is quite straightforward if you are familiar with implementing a scroll view in
SwiftUI. We utilize a ForEach loop to present 50 color items, which are then embedded
within a vertical scroll view. If you add this code to a SwiftUI project, you should be able
to preview it and see something similar to Figure 1.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 916


Figure 1. The sample scroll view

To keep track of the current scroll position or item, you should first declare a state
variable to hold the position:

@State private var scrollID: Int?

Next, attach the scrollPosition modifier to the scroll view. This modifier takes a binding
to scrollID , which stores the scroll position:

ScrollView {

...

}
.scrollPosition(id: $scrollID)

As the scroll view scrolls, the binding will be updated with the index of the color view.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 917


Lastly, attach the scrollTargetLayout modifier to the LazyVStack view as follows:

LazyVStack(spacing: 10) {

...

}
.scrollTargetLayout()

Without the scrollTargetLayout() modifier, the scrollPosition modifier will not work
correctly. The scrollPosition modifier relies on the scrollTargetLayout() modifier to
configure which the layout that contains your scroll targets.

To observe the changes of the scroll position, you can attach a onChange modifier to the
scroll view and print the scroll ID to the console:

.onChange(of: scrollID) { oldValue, newValue in


print(newValue ?? "")
}

As you scroll through the list, the current scroll position should be displayed in the
console.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 918


Figure 2. Detecting the scroll position

Scroll to the Top


Let’s implement one more feature using the scrollPosition modifier. When a user taps
any of the color views, the list should automatically scroll back to the top.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 919


Figure 3. Auto scroll to the top

This can be achieved by adding a onTapGesture modifier to each color view, and passing a
closure that sets the scrollID to 0 within it. When the user taps on any of the color
views, the scrollID will be updated to 0 , which will cause the list to scroll back to the
top.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 920


bgColors[index % 5]
.frame(height: 100)
.overlay {
Text("\(index)")
.foregroundStyle(.white)
.font(.system(.title, weight: .bold))
}
.onTapGesture {
withAnimation {
scrollID = 0
}
}

Adjusting Content Margins of Scroll Views


Now that you know how to detect the scroll position, let's discuss one more new feature
of scroll views in iOS 17. First, it’s the contentMargins modifier. This is a new feature in
SwiftUI that allows developers to customize the margins of scrollable views. With
contentMargins , you can easily adjust the amount of space between the content and the
edges of the scroll view, giving you more control over the layout of your app.

We have already used this modifier in the sample code that sets the horizontal margin to
10 points.

.contentMargins(.horizontal, 10.0, for: .scrollContent)

The .scrollContent parameter value indicates that the content margin should only be
applied to the scroll content, rather than the entire scroll view. If you do not specify the
edges and placement parameters, the contentMargins modifier will apply the margins to
all edges of the scroll view. For example, in the code below, it insets the entire scroll view
by 50 points, including the scroll bar.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 921


Figure 4. Setting the content margins

In case if you want to keep the scroll bar at its original position, you can set the
placement parameter to .scrollContent .

Figure 5. Changing the position of the scroll bar

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 922


Summary
The new scrollPosition modifier is one of the most anticipated features of scroll views.
In iOS 17, SwiftUI finally introduces this feature allowing developers to detect scroll
positions. In this tutorial, we have demonstrated the usage of this new modifier and
introduced another new modifier called contentMargins , which enables developers to
customize the margins of scrollable views.

With these new features in SwiftUI, developers can now effortlessly create more
customized and visually appealing layouts for their apps.

For reference, you can download the demo project here:

Demo project
(https://www.appcoda.com/resources/swiftui5/SwiftUIScrollPosition.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 923


Chapter 50
Animating Scroll View Using SwiftUI
In the earlier chapter, we introduced a new feature of ScrollView in iOS 17, which
enables developers to easily detect the scroll position and implement the scroll-up (or
scroll-down) feature. In addition to this functionality, the latest version of SwiftUI also
introduces a new modifier called scrollTransition that allows us to observe the
transition of views and apply various animated effects.

Previously, we built a basic scroll view. Let's continue using it as an example. For
reference, here is the code for creating a scroll view:

ScrollView {
LazyVStack(spacing: 10) {
ForEach(0...50, id: \.self) { index in

bgColors[index % 5]
.frame(height: 100)
.overlay {
Text("\(index)")
.foregroundStyle(.white)
.font(.system(.title, weight: .bold))
}
.onTapGesture {
withAnimation {
scrollID = 0
}
}
}
}
.scrollTargetLayout()
}
.contentMargins(10.0, for: .scrollContent)
.scrollPosition(id: $scrollID)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 924


Using ScrollTransition Modifier
A transition in scroll views describes the changes a child view should undergo when its
appearing or disappearing. The new scrollTransition modifier enables us to monitor
these transitions and apply different visual and animated effects accordingly.

Figure 1. Using ScrollTransition modifier

To demonstrate how it works, let's modify the code from the previous section by adding
the scrollTransition modifier to the color view. Here is the updated code:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 925


bgColors[index % 5]
.frame(height: 100)
.overlay {
Text("\(index)")
.foregroundStyle(.white)
.font(.system(.title, weight: .bold))
}
.scrollTransition { content, phase in
content
.opacity(phase.isIdentity ? 1.0 : 0.3)
.scaleEffect(phase.isIdentity ? 1.0 : 0.3)
}

We apply a subtle animation by changing the opacity and size of child views. The
scrollTransition closure passes two parameters: the child view and the transition phase.
There are three possible values for transition phases: .identity , .topLeading , and
.bottomTrailing . Based on the phase, we can apply different visual effects.

The .identity value indicates that the child view is fully visible in the scroll view's visible
region. The .topLeading value indicates that the view is about to move into the visible
area at the top edge of the scroll view, while .bottomTrailing indicates that the view is
about to move into the visible area at the bottom edge of the scroll view.

During the identity phase, scroll transitions should not typically result in any visual
changes to the view. Therefore, in the code above, we reset both opacity and size to their
original state when the view is in the identity phase. For other phases, we make the
view smaller and more transparent. This is how we animate views during the scroll
transition.

Working with Scroll Transition Configuration


A scroll transition configuration controls how a view transitions as it appears or
disappears. When you use the .scrollTransition modifier, the default configuration is
.interactive . This configuration lets you smoothly blend the transition effect as you
scroll your view into the visible region of the container.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 926


Figure 2. Using the default configuration for scroll transition

Other than the default configuration, you also have the option to use .animated to
smoothly animate the transition when the view is displayed. You can replace the
.scrollTransition modifier like this to achieve a slightly different animated effect:

.scrollTransition(.animated) { content, phase in

content
.opacity(phase.isIdentity ? 1.0 : 0.3)
.scaleEffect(phase.isIdentity ? 1.0 : 0.3)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 927


Optionally, you can also define a threshold for the transition animation. Let me provide
an example to illustrate why we may need to adjust the threshold. In the code, modify the
frame height of the color view from 100 to 300 like this:

bgColors[index % 5]
.frame(height: 300)

After making the change, you should notice that the third item in the scroll view is
minimized and has already been applied with the transparent effect.

Figure 3. Changing the frame height to 300 points

This is not the desired UI layout. The expected behavior is for the third item to be fully
visible and not in a transitional state. In this case, we can alter the threshold to define
when the animation takes place.

The threshold determines when the view is considered visible (i.e. the identity phase)
based on how much of the view intersects with the scroll view. To change the threshold,
you can update the scrollTransition modifier like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 928


.scrollTransition(.animated.threshold(.visible(0.3))) { content, phase in

.
.
.

The code above sets a threshold where the view is considered fully visible when it is 30%
visible within the scrolling area. As soon as you update the code, the third item of the
scroll view will be fully displayed. The animation will only occur when the item is less
than 30% visible.

Figure 4. The animation occurs when the item is less than 30% visible

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 929


Using the Phase Value
The phase parameter provides the value of the transition phase, ranging from -1.0 to 1.0.
This value can be utilized to apply scaling or other animated effects. When the phase is -1,
it represents the topLeading phase, and when it's 1, it corresponds to the bottomTrailing
phase. The identity phase is represented by a value of 0.

Figure 5. The rotation effect

For example, we can utilize the phase value to apply a 3-dimensional rotation effect:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 930


.scrollTransition(.animated.threshold(.visible(0.3))) { content, phase in

content
.opacity(phase.isIdentity ? 1.0 : 0.3)
.scaleEffect(phase.isIdentity ? 1.0 : 0.3)
.rotation3DEffect(.radians(phase.value), axis: (1, 1, 1))

Summary
In this tutorial, we explain how to use the scrollTransition modifier in SwiftUI to
animate the transition of views in a ScrollView. The modifier allows developers to apply
various visual and animated effects to child views based on the transition phase.

By mastering this modifier, you can take their app's user experience to the next level.

For reference, you can download the demo project here:

Demo project
(https://www.appcoda.com/resources/swiftui5/SwiftUIScrollViewAnimation.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 931


Chapter 51
Using UnevenRoundedRectangle to
Round Specific Corners
In SwiftUI, there is a convenient built-in modifier called cornerRadius that allows you to
easily create rounded corners for a view. By applying the cornerRadius modifier to a
Rectangle view, you can transform it into a rounded rectangle. The value you provide to
the modifier determines the extent of the rounding effect.

Rectangle()
.cornerRadius(10.0)

Alternatively, SwiftUI also provides a standard RoundedRectangle view for creating a


rounded rectangle:

RoundedRectangle(cornerRadius: 25.0)

Unfortunately, both the cornerRadius modifier and the RoundedRectangle view can only
apply the same corner radius to all corners of the shape.

What if you need to round a specific corner of a view?

In iOS 17, the SwiftUI framework introduces a new view called UnevenRoundedRectangle .
What sets this view apart is the ability to specify a distinct radius value for each corner,
allowing developers to create highly customizable shapes.

Working with UnevenRoundedRectangle


With UnevenRoundedRectangle , you can easily create rectangular shapes with rounded
corners of different radii. To use UnevenRoundedRectangle , you simply need to specify the
corner radius for each corner. Here is an example:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 932


UnevenRoundedRectangle(cornerRadii: .init(
topLeading: 50.0,
bottomLeading: 10.0,
bottomTrailing: 50.0,
topTrailing: 30.0),
style: .continuous)
.frame(width: 300, height: 100)
.foregroundStyle(.indigo)

Optionally, you can indicate the style of the corners. A continuous corner style will give
the corners a smoother look. If you’ve put the code above in Xcode 15, you can create a
rectangular shape like below.

Figure 1. A sample uneven rounded rectangle

You can use this shape and transform it into a button by using the background modifier.
Here is a sample code snippet:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 933


Button(action: {

}) {
Text("Register")
.font(.title)
}
.tint(.white)
.frame(width: 300, height: 100)
.background {
UnevenRoundedRectangle(cornerRadii: .init(
topLeading: 50.0,
bottomLeading: 10.0,
bottomTrailing: 50.0,
topTrailing: 30.0),
style: .continuous)
.foregroundStyle(.indigo)
}

Animating the Rounded Corners

Figure 2. Animating the rounded corners

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 934


To animate the rounded corners of the UnevenRoundedRectangle , you can use the
withAnimation function and toggle a Boolean variable. Here is an example code snippet:

struct AnimatedCornerView: View {

@State private var animate = false

var body: some View {

UnevenRoundedRectangle(cornerRadii: .init(
topLeading: animate ? 10.0 : 80.0,
bottomLeading: animate ? 80.0 : 10.0,
bottomTrailing: animate ? 80.0 : 10.0,
topTrailing: animate ? 10.0 : 80.0))
.foregroundStyle(.indigo)
.frame(height: 200)
.padding()
.onTapGesture {
withAnimation {
animate.toggle()
}
}

}
}

In this example, tapping the rectangle will toggle the animate variable, which controls
the corner radii of the rectangle. The withAnimation function will animate the transition
between the two sets of corner radii.

Creating Unique Shapes

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 935


Figure 3. A sample shape built using uneven rounded rectangles

By overlapping multiple UnevenRoundedRectangle views, you can create a wide variety of


shapes. The example provided above demonstrates how to create the specific shape
shown using the following code:

ZStack {
ForEach(0..<18, id: \.self) { index in
UnevenRoundedRectangle(cornerRadii: .init(topLeading: 20.0, bottomLeading:
5.0, bottomTrailing: 20.0, topTrailing: 10.0), style: .continuous)
.foregroundStyle(.indigo)
.frame(width: 300, height: 30)
.rotationEffect(.degrees(Double(10 * index)))
}
}
.overlay {
Image(systemName: "briefcase")
.foregroundStyle(.white)
.font(.system(size: 100))
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 936


To add an additional visual effect, you can animate the change in opacity by modifying
the code as follows:

ZStack {
ForEach(0..<18, id: \.self) { index in
UnevenRoundedRectangle(cornerRadii: .init(topLeading: 20.0, bottomLeading:
5.0, bottomTrailing: 20.0, topTrailing: 10.0), style: .continuous)
.foregroundStyle(.indigo)
.frame(width: 300, height: 30)
.opacity(animate ? 0.6 : 1.0)
.rotationEffect(.degrees(Double(10 * index)))
.animation(.easeInOut.delay(Double(index) * 0.02), value: animate)
}
}
.overlay {
Image(systemName: "briefcase")
.foregroundStyle(.white)
.font(.system(size: 100))
}
.onTapGesture {
animate.toggle()
}

Implementing this modification will result in a visually captivating effect that adds an
extra level of interest to your design.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 937


Figure 4. Adding animations to the unique shape

Summary
The addition of the UnevenRoundedRectangle view in SwiftUI provides developers with a
convenient solution for rounding specific corners of rectangular views. It also gives you
the flexibility and options to achieve the look you want. With the UnevenRoundedRectangle

shape, you can seamlessly incorporate unique and eye-catching shapes into your app,
enhancing its overall design and user experience.

For reference, you can download the demo project here:

Demo project
(https://www.appcoda.com/resources/swiftui5/SwiftUIUnevenRoundedRect.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 938


Chapter 52
Getting Started with SwiftData
One common question in SwiftUI app development is how to work with Core Data to
save data permanently in the built-in database. Despite Apple's ongoing efforts to
simplify the APIs of Core Data, new comers often find the framework challenging to use.
However, there is good news on the horizon. Apple will be releasing a new framework
called SwiftData in iOS 17 to replace Core Data. SwiftData is designed to be much easier
to use for data modelling and management, offering a more user-friendly approach.

What’s SwiftData
First and foremost, it's important to note that the SwiftData framework should not be
confused with a database. Built on top of Core Data, SwiftData is actually a framework
designed to help developers manage and interact with data on a persistent store. While
the default persistent store for iOS is typically the SQLite database, it's worth noting that
persistent stores can take other forms as well. For example, Core Data can also be used to
manage data in a local file, such as an XML file.

Regardless of whether you're using Core Data or the SwiftData framework, both tools
serve to shield developers from the complexities of the underlying persistent store.
Consider the SQLite database, for instance. With SwiftData, there's no need to worry
about connecting to the database or understanding SQL in order to retrieve data records.
Instead, developers can focus on working with APIs and Swift Macros, such as @Query

and @Model , to effectively manage data in their applications.

The SwiftData framework is newly introduced in iOS 17 to replace the previous


framework called Core Data. Core Data has long been the data management APIs for iOS
development since the era of Objective-C. Even though developers can integrate the
framework into Swift projects, Core Data is not a native solution for both Swift and
SwiftUI.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 939


In iOS 17, Apple finally introduces a native framework called SwiftData for Swift on
persistent data management and data modeling. It's built on top of Core Data but the
APIs are completely redesigned to make the most out of Swift.

Using Code to Create the Data Model

Figure 1. Data model editor

If you have used Core Data before, you may remember that you have to create a data
model (with a file extension .xcdatamodeld) using a data model editor for data
persistence. With the release of SwiftData, you no longer need to do that. SwiftData
streamlines the whole process with macros, another new Swift feature in iOS 17. Say, for
example, you already define a model class for Song as follows:

class Song {
var title: String
var artist: String
var album: String
var genre: String
var rating: Double
}

To use SwiftData, the new @Model macro is the key for storing persistent data using
SwiftUI. Instead of building the data model with model editor, SwiftData just requires
you to annotate the model class with the @Model macro like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 940


@Model class Song {
var title: String
var artist: String
var album: String
var genre: String
var rating: Double
}

This is how you define the schema of the data model in code. With this simple keyword,
SwiftData automatically enables persistence for the data class and offers other data
management functionalities such as iCloud sync. Attributes are inferred from properties
and it supports basic value types such as Int and String.

SwiftData allows you to customize how your schema is built using property metadata.
You can add uniqueness constraints by using the @Attribute annotation, and delete
propagation rules with the @Relationship annotation. If there are certain properties you
do not want included, you can use the @Transient macro to tell SwiftData to exclude
them. Here is an example:

@Model class Album {


@Attribute(.unique) var name: String
var artist: String
var genre: String

// The cascade relationship instructs SwiftData to delete all


// songs when the album is deleted.
@Attribute(.cascade) var songs: [Song]? = []
}

To drive the data persistent operations, there are two key objects of SwiftData that you
should be familiar with: ModelContainer and ModelContext . The ModelContainer serves as
the persistent backend for your model types. To create a ModelContaine r, you simply need
to instantiate an instance of it.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 941


// Basic
let container = try ModelContainer(for: [Song.self, Album.self])

// With configuration
let container = try ModelContainer(for: [Song.self, Album.self],
configurations: ModelConfiguration(url: URL("p
ath"))))

In SwiftUI, you can set up the model container at the root of the application:

import SwiftData
import SwiftUI

@main
struct MusicApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer (for: [Song.self, Album.self]))
}
}

Once you have set up the model container, you can begin using the model context to fetch
and save data. The context serves as your interface for tracking updates, fetching data,
saving changes, and even undoing those changes. When working with SwiftUI, you can
typically obtain the model context from your view's environment:

struct ContextView: View {


@Environment(\.modelContext) private var modelContext
}

With the context, you are ready to fetch data. The simplest way is to use the @Query

property wrapper. You can easily load and filter anything stored in your database with a
single line of code.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 942


@Query(sort: \.artist, order: .reverse) var songs: [Song]

To insert item in the persistent store, you can call the insert method of the model context
and pass it the model objects to insert.

modelContext.insert(song)

Similarly, you can delete the item via the model context like this:

modelContext.delete(song)

This is a brief introduction of SwiftData. If you're still feeling confused about how to use
SwiftData? No worries. You will understand its usage after building a ToDO app.

Building a Simple To Do App


Now that you have a basic understanding of SwiftData, I would like to demonstrate how
to build a simple to-do app using this framework. Please note that the app is not fully
functional and only allows users to add a random task to the to-do list. However, it serves
as a good starting point to familiarize yourself with the SwiftData framework.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 943


Figure 2. The simple to-do app

Assuming you’ve created a SwiftUI project in Xcode, let’s first create the data model of
the app. Create a new file named ToDoItem and update the content like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 944


import Foundation
import SwiftData

@Model class ToDoItem: Identifiable {


var id: UUID
var name: String
var isComplete: Bool

init(id: UUID = UUID(), name: String = "", isComplete: Bool = false) {


self.id = id
self.name = name
self.isComplete = isComplete
}
}

As discussed earlier, SwiftData simplifies the process of defining a schema using code. All
you need to do is annotate the model class with the @Model macro. SwiftData will then
automatically enable persistence for the data class.

Before we move onto building the UI of the app and handling the data persistent, let’s
create a helper function for generating a random to-do item:

func generateRandomTodoItem() -> ToDoItem {


let tasks = [ "Buy groceries", "Finish homework", "Go for a run", "Practice Yo
ga", "Read a book", "Write a blog post", "Clean the house", "Walk the dog", "Atten
d a meeting" ]

let randomIndex = Int.random(in: 0..<tasks.count)


let randomTask = tasks[randomIndex]

return ToDoItem(name: randomTask, isComplete: Bool.random())


}

Next, let’s build the main UI of the to-do app. In the ContentView.swift file, update the
code like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 945


import SwiftData

struct ContentView: View {


@Query var todoItems: [ToDoItem]

var body: some View {


NavigationStack {
List {
ForEach(todoItems) { todoItem in
HStack {
Text(todoItem.name)

Spacer()

if todoItem.isComplete {
Image(systemName: "checkmark")
}
}
}
}

.navigationTitle("To Do List")
}
}
}

We mark the todoItems array with the @Query property wrapper. This @Query property
automatically fetches the required data for you. In the provided code, we specify to fetch
the ToDoItem instances. Once we retrieve the to-do items, we utilize the List view to
display the items.

Set up the model container


To drive the data persistent operations, we also need to set up the model container.
Switch over to ToDoDemoAppApp.swift and attach the modelContainer modifier like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 946


struct ToDoDemoAppApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: ToDoItem.self)
}
}

Here, we set a shared model container for storing instances of ToDoItem .

If you preview the ContentView , the list view is empty. Obviously, we haven’t stored any
to-do items in the database. Now, let’s add a “Add item” button to insert a random to-do
item into the database.

Storing to-do items into the database


In ContentView.swift , declare the following variable to retrieve the model context:

@Environment(\.modelContext) private var modelContext

After obtaining the model context, we can easily insert data into the database. We’ll add a
toolbar button for adding a random to-do item. Insert the following code inside the
NavigationStack view (place it after navigationTitle ):

.toolbar {
Button("", systemImage: "plus") {
modelContext.insert(generateRandomTodoItem())
}
}

To store an item into database, you simply call the insert method of the model context.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 947


Figure 3. Storing to-do items into the database

Now you're ready to test the app in the simulator. However, if you intend to test it in the
preview canvas, you need to make one additional modification by adding the model
container within the #Preview block:

#Preview {
ContentView()
.modelContainer(for: ToDoItem.self)
}

When you tap the "+" button, the app instantly stores the to-do item. Simultaneously, it
retrieves the new item from the database and displays it in the list view.

Updating an existing item


SwiftData significantly reduces the amount of work required to handle item updates or
modifications in the persistent store. By simply marking your model objects with the
@Model macro, SwiftData automatically modifies the setters for change tracking and
observation. This means that no code changes are needed to update the to-do items.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 948


To test the update behavior, you can simply run the app on a simulator. When you tap a
to-do item, it should be marked as complete. This change is now saved permanently in
the device's database. Even after restarting the app, all the items will still be retained.

Deleting the item from the database


Now that you know how to perform fetch, update, and insert, how about data deletion?
We will add a feature to the app for removing a to-do item.

In the ContentView struct, attach the onDelete modifier to the ForEach loop:

.onDelete(perform: { indexSet in
for index in indexSet {
let itemToDelete = todoItems[index]
modelContext.delete(itemToDelete)
}
})

This closure takes an index set that stores the indices of the items to be deleted. To
remove an item from the persistent store, simply call the delete function of the model
context and specify the item to be deleted.

The onDelete modifier automatically enables the swipe-to-delete feature in the list view.
To try this out, simply run the app and swipe to delete an item. This will completely
remove the item from the database.

Populating Dummy Data for Preview


To populate the database with dummy data, you can set up a preview container loaded
with sample data.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 949


@MainActor
let previewContainer: ModelContainer = {
do {
let container = try ModelContainer(for: ToDoItem.self, configurations: Mod
elConfiguration(isStoredInMemoryOnly: true))

for _ in 1...10 {
container.mainContext.insert(generateRandomTodoItem())
}

return container
} catch {
fatalError("Failed to create container")
}
}()

In the code above, we set up an in-memory database that is populated with 10 random
to-do items. Once the preview container is ready, you can attach it to the ContentView in
the #Preview block:

#Preview {
ContentView()
.modelContainer(previewContainer)
}

Summary
I hope that you now have a better understanding of how to integrate SwiftData into a
SwiftUI project and how to perform all basic CRUD (create, read, update & delete)
operations. Apple has put a lot of efforts to make persistent data management and data
modeling easier for Swift developers and new comers.

While Core Data remains an option for backward compatibility, it’s time to learn the
SwiftData framework, especially if you are developing an app exclusively for iOS 17 or
later. Embrace this new framework to leverage the enhanced capabilities and benefits
SwiftData offers.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 950


For reference, you can download the demo project here:

Demo project (https://www.appcoda.com/resources/swiftui5/SwiftUISwiftData.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 951


Chapter 53
How to Embed Photo Pickers in iOS
Apps
Starting with iOS 16, SwiftUI introduces a native photo picker view known as
PhotosPicker . If your app requires access to users' photo library, the PhotosPicker view
seamlessly manages the photo selection process. This built-in view offers remarkable
simplicity, allowing developers to present the picker and handle image selection with just
a few lines of code.

When presenting the PhotosPicker view, it showcases the photo album in a separate
sheet, rendered atop your app’s interface. In earlier versions of iOS, you couldn’t
customize or change the appearance of the photos picker view to align with your app’s
layout. However, Apple has introduced enhancements to the PhotosPicker view in iOS 17,
enabling developers to seamlessly embed it inline within the app. Additionally, you have
the option to modify its size and layout using standard SwiftUI modifiers such as .frame

and .padding .

In this tutorial, I will show you how to implement an inline photo picker with the
improved PhotosPicker view.

Revisiting PhotosPicker
To use the PhotosPicker view, you can first declare a state variable to store the photo
selection and then instantiate a PhotosPicker view by passing the binding to the state
variable. Here is an example:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 952


import SwiftUI
import PhotosUI

struct ContentView: View {

@State private var selectedItem: PhotosPickerItem?

var body: some View {


PhotosPicker(selection: $selectedItem,
matching: .images) {
Label("Select a photo", systemImage: "photo")
}
}
}

The matching parameter allows you to specify the asset type to display. Here, we just
choose to display images only. In the closure, we create a simple button with the Label

view.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 953


Figure 1. The standard Photos picker

Upon selecting a photo, the photo picker automatically dismisses itself, and the chosen
photo item is stored in the selectedItem variable, which is of type PhotosPickerItem . To
load the image from the item, you can use loadTransferable(type:completionHandler:) . You
can attach the onChange modifier to listen to the update of the selectedItem variable.
Whenever there is a change, you call the loadTransferable method to load the asset data
like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 954


@State private var selectedImage: Image?

.
.
.

.onChange(of: selectedItem) { oldItem, newItem in


Task {
if let image = try? await newItem?.loadTransferable(type: Image.self) {
selectedImage = image
}
}
}

When using loadTransferable , it is necessary to specify the asset type for retrieval. In this
case, we employ the Image type to directly load the image. If the operation is successful,
the method will return an Image view, which can be used to directly render the photo on
the screen.

if let selectedImage {
selectedImage
.resizable()
.scaledToFit()
.padding(.horizontal, 10)
}

Implementing an Inline PhotosPicker


Now that you should understand how to work with a PhotosPicker , let’s see how to
embed it in our demo app. What we are going to do is to replace the “Select a photo”
button with an inline Photos picker. The updated version of PhotosPicker comes with a
new modifier called photosPickerStyle . By specify a value of .inline , the Photos picker
will be automatically embedded in the app:

.photosPickerStyle(.inline)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 955


You can also attach standard modifiers like .frame and .padding to adjust the size of the
picker.

Figure 2. A sample inline photos picker

By default, the top accessory of the picker is the navigation bar and the bottom accessory
is the toolbar. To disable both bars, you can apply the photosPickerAccessoryVisibility

modifier:

.photosPickerAccessoryVisibility(.hidden)

Optionally, you can hide either one of them:

.photosPickerAccessoryVisibility(.hidden, edges: .bottom)

Handling Multiple Photo Selections


Presently, the Photos picker only allows users to select a single photo. To enable multiple
selections, you can opt in the continuous selection behavior by setting the
selectionBehavior to .continuous or .continuousAndOrdered :

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 956


PhotosPicker(selection: $selectedItems,
maxSelectionCount: 5,
selectionBehavior: .continuousAndOrdered,
matching: .images) {
Label("Select a photo", systemImage: "photo")
}

If you wish to restrict the number of items available for selection, you can specify the
maximum count using the maxSelectionCount parameter.

Figure 3. Selecting multiple photos

Once the user has selected a set of photos, they are stored in the selectedItems array. The
selectedItems array has been modified to accommodate multiple items and is now of
type PhotosPickerItem .

@State private var selectedItems: [PhotosPickerItem] = []

To load the selected photos, you can update the onChange closure like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 957


.onChange(of: selectedItems) { oldItems, newItems in

selectedImages.removeAll()

newItems.forEach { newItem in

Task {
if let image = try? await newItem.loadTransferable(type: Image.self) {
selectedImages.append(image)
}
}

}
}

I used an Image array to store the retrieved images.

@State private var selectedImages: [Image] = []

To display the chosen images, you may use a horizontal scroll view. Here is the sample
code that can be placed at the beginning of the VStack view:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 958


if selectedImages.isEmpty {
ContentUnavailableView("No Photos", systemImage: "photo.on.rectangle", descrip
tion: Text("To get started, select some photos below"))
.frame(height: 300)
} else {

ScrollView(.horizontal) {
LazyHStack {
ForEach(0..<selectedImages.count, id: \.self) { index in
selectedImages[index]
.resizable()
.scaledToFill()
.frame(height: 250)
.clipShape(RoundedRectangle(cornerRadius: 25.0))
.padding(.horizontal, 20)
.containerRelativeFrame(.horizontal)
}

}
}
.frame(height: 300)
}

If you'd like to learn more about how to create image carousels, you can check out this
tutorial. In iOS 17, a new view called ContentUnavailableView is introduced. This view is
recommended for use in scenarios where the content of a view cannot be displayed. So,
when no photo is selected, we use the ContentUnavailableView to present a concise and
informative message.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 959


Figure 4. Using ContentUnavailableView for empty content

Summary
In iOS 17, Apple made improvements to the native Photos picker. Now, you can easily
include it within your app instead of using a separate sheet. This tutorial explains the
new modifiers that come with the updated PhotosPicker view and shows you how to
create an inline photo picker.

For reference, you can download the demo project here:

Demo project
(https://www.appcoda.com/resources/swiftui5/SwiftUIPhotosPicker.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 960


Chapter 54
Using PhaseAnimator to Create
Dynamic Multi-Step Animations
SwiftUI already streamlines the creation of view animations. One example is the
matchedGeometryEffect modifier, which enables developers to define the appearance of
two views. The modifier calculates the disparities between the two views and
automatically animates the size and position changes. With iOS 17, Apple continues to
improve a new view called PhaseAnimator in the SwiftUI framework, which allows us to
build more sophisticated animations.

In this tutorial, we will explore the capabilities of PhaseAnimator and learn how to utilize
it to create multi-step animations.

Building a Simple Animation with PhaseAnimator


The PhaseAnimator view, or the .phaseAnimator modifier, enables you to generate multi-
step animations. By cycling through a collection of phases that you provide, each
representing a distinct step, you can create dynamic and engaging animations.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 961


Figure 1. A sample animation built using PhaseAnimator

Let me give a simple example, so you will understand how to work the phase animator.
We will animate the transformation of a rounded rectangle. It begins as a blue rectangle,
then scales up, changes color to indigo, and incorporates a 3D rotation animation.

We can use the RoundedRectangle view to create the rounded rectangle and attach the
phaseAnimator modifier to the rectangle like this:

struct ContentView: View {


var body: some View {
RoundedRectangle(cornerRadius: 25.0)
.frame(height: 200)
.phaseAnimator([ false, true ]) { content, phase in
content
.scaleEffect(phase ? 1.0 : 0.5)
.foregroundStyle(phase ? .indigo : .blue)
}
}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 962


Within the phase animator, we specify two phases: false and true . The view builder
closure takes two parameters. The first parameter is a proxy value that represents the
modified view. The second parameter indicates the current phase.

When the view initially appears, the first phase (i.e. false ) is active. We set the scale to
50% of the original size and the foreground color to blue. In the second phase, the
rectangle scales back to its original size and the color transitions to indigo.

The phase animator automatically animates the change between these two phases.

Figure 2. Animating the change between two phases

To create the 3D rotation animation, you can attach the rotation3DEffect modifier to the
content view like below:

.rotation3DEffect(
phase ? .degrees(720) : .zero,
axis: (x: 0.0, y: 1.0, z: 0.0)
)

If you want to customize the animation, phaseAnimator also provides the animation

parameter for defining your preferred animation. Based on the given phase, you can
specify the animation to be used when moving from one phase to another. Here is an

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 963


example:

.phaseAnimator([ false, true ]) { content, phase in


content
.scaleEffect(phase ? 1.0 : 0.5)
.foregroundStyle(phase ? .indigo : .blue)
.rotation3DEffect(
phase ? .degrees(720) : .zero,
axis: (x: 0.0, y: 1.0, z: 0.0)
)
} animation: { phase in
switch phase {
case true: .smooth.speed(0.2)
case false: .spring
}
}

Using Enum to Define Multi Step Animations


In the previous example, the animation consisted of only two phases: false and true .
However, in more complex animations, there are often multiple steps or phases involved.
In this case, an enum is a great way to define a list of steps for the animation.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 964


Figure 3. A multi-step animation

Let's consider an example of animating an emoji icon with the following steps:

1. Initially, the emoji icon is centered on the screen.


2. It scales up by 50% and rotates itself by 720 degrees.
3. Next, it moves upward by 250 points while simultaneously scaling down by 20%.
4. Then, it moves downward by 450 points. While descending, it rotates itself by 360
degrees and scales down by 50%.
5. Finally, it returns to its original position.

With these steps, we can create a dynamic animation for the emoji icon.

To implement this multi-step animation, we can define an enum like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 965


enum Phase: CaseIterable {
case initial
case rotate
case jump
case fall

var scale: Double {


switch self {
case .initial: 1.0
case .rotate: 1.5
case .jump: 0.8
case .fall: 0.5
}
}

var angle: Angle {


switch self {
case .initial, .jump: Angle(degrees: 0)
case .rotate: Angle(degrees: 720)
case .fall: Angle(degrees: 360)
}
}

var offset: Double {


switch self {
case .initial, .rotate: 0
case .jump: -250.0
case .fall: 450.0
}
}
}

In this enum, we have four cases that represent different steps of the animation. During
each phase, we perform scaling, rotation, or movement on the emoji icon. To accomplish
this, we define three computed properties for each action. Within each property, we
specify the values for the particular animation phase or step.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 966


For instance, in the "rotate" phase, the emoji should be scaled up by 50% and rotated by
720 degrees. The scale property returns 1.5, and the angle property returns
Angle(degrees: 720) .

With the Phase enum, we can now easily animate the emoji with the phase animator like
below:

Text(" ")
.font(.system(size: 100))
.phaseAnimator(Phase.allCases) { content, phase in
content
.scaleEffect(phase.scale)
.rotationEffect(phase.angle)
.offset(y: phase.offset)

} animation: { phase in
switch phase {
case .initial: .bouncy
case .rotate: .smooth
case .jump: .snappy
case .fall: .interactiveSpring
}
}

The Phase.allCases automatically informs the phase animator about the available
phases. Depending on the given phase, the emoji icon is scaled, rotated, and moved
according to the computed values.

To customize the animation, we can specify a particular animation, such as snappy , for
different phases instead of using the default animation.

Using Triggers
Currently, the phase animator initiates the animation automatically and repeats it
indefinitely. However, there may be situations where you prefer to trigger the animation
manually. In such cases, you can define your criteria by specifying the desired conditions
in the trigger parameter of the phase animator.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 967


For example, the emoji animation should be triggered when a user taps on it. You can
first declare a state variable like this:

@State private var startAnimation = false

Next, you update the phaseAnimator modifier by adding the trigger parameter:

.phaseAnimator(Phase.allCases, trigger: startAnimation, content: { content, phase


in
content
.scaleEffect(phase.scale)
.rotationEffect(phase.angle)
.offset(y: phase.offset)
}, animation: { phase in
switch phase {
case .initial: .bouncy
case .rotate: .smooth
case .jump: .snappy
case .fall: .interactiveSpring
}
})

After making the code changes, the animation will only be triggered when the value of
startAnimation is switched from false to true . To achieve this, attach the
onTapGesture modifier to the Text view.

.onTapGesture {
startAnimation.toggle()
}

When a user taps the emoji, we toggle the value of startAnimation . This triggers the
multi-step animation.

Summary

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 968


The introduction of PhaseAnimator has made the process of creating multi-step
animations incredibly simple. By using an enum to define what changes should happen at
each step of the animation, you can create dynamic and engaging animations with just a
few lines of code. SwiftUI's PhaseAnimator , along with other helpful features, takes care of
the hard work for you, so developers can focus on making impressive animations without
any hassle.

For reference, you can download the demo project here:

Demo project
(https://www.appcoda.com/resources/swiftui5/SwiftUIPhaseAnimator.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 969


Chapter 55
Creating Advanced Animations with
KeyframeAnimator
In addition to the PhaseAnimator , SwiftUI introduced the KeyframeAnimator in iOS 17,
allowing developers to create advanced animations using keyframes. In this tutorial, we
will delve into the KeyframeAnimator and learn how to create a more intricate animation.

The PhaseAnimator view (or modifier), which we discussed in the previous tutorial,
provides developers with the ability to create multi-step animations over a sequence of
phases. By specifying the desired animations for each phase, the PhaseAnimator

automatically animates the content whenever the phase changes. It simplifies the process
of building complex animations by handling the transitions between phases for you.

Working with KeyframeAnimator


For phase-based animation, it works well for animations that can be represented as
discrete states. When a state transition happens, all properties are animated
simultaneously. Once the animation for a particular state is completed, SwiftUI smoothly
transitions to the next state. This process continues across all animation phases.

Keyframe-based animation is designed to accommodate a specific type of animation


where each property is animated independently. By utilizing keyframes, we can animate
individual properties separately, which in turn offers us greater flexibility and control
over our animations.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 970


Figure 1. A sample animation built using KeyframeAnimator

Let’s try to animate an emoji icon (as illustrated above) and you will understand how we
can use keyframe animator.

Defining the Animation Values


As mentioned earlier, keyframe-based animation enables us to animate individual
properties independently. To utilize the keyframe animator, we begin by defining a struct
that encompasses all the properties we wish to animate. Here's an example:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 971


struct AnimationValues {
var scale = 1.0
var verticalStretch = 1.0
var translation = CGSize.zero
var opacity = 1.0
}

The initial values define the initial state of the emoji icon. Later, we will change each of
the properties to scale, stretch, and move the emoji icon.

Applying the Keyframe Animator


In the body closure, let’s update the code like this to apply the keyframe animator:

Text(" ")
.font(.system(size: 100))
.keyframeAnimator(initialValue: AnimationValues()) {
content, value in

content
.scaleEffect(value.scale)
.scaleEffect(y: value.verticalStretch)
.offset(value.translation)
.opacity(value.opacity)

} keyframes: { _ in

KeyframeTrack(\.scale) {
CubicKeyframe(0.8, duration: 0.2)
}
}

As usual, we use a Text view to display the emoji icon. To create a keyframe-based
animation, we attach the keyframeAnimator modifier to the text view.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 972


The initialValue parameter is provided with the initial values that the keyframes will
animate from. Within the view builder closure, we have access to two parameters. The
first parameter is a proxy value that represents the modified view. The second parameter
holds the interpolated value generated by the keyframes.

We apply the desired animation effects to the content view by adjusting its scale, offset,
and opacity. Lastly, it’s the keyframes parameter. This is where we define the value
changes that occur over time. These defined keyframes will be responsible for applying
the corresponding animations to the specified value.

Keyframes are arranged into tracks, with each track governing a distinct property of the
animated type. In the provided code snippet, we specifically designated the keyframe
track for the scale property using the CubicKeyframe type. We adjust the size of the emoji,
reducing it to 80% of its original size.

Figure 2. A scale animation

Once you've made the code changes, you should be able to see the animation instantly in
the preview canvas. The keyframe animator animates the size change and repeats it
continuously.

Keyframe Types

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 973


While we use the CubicKeyframe type, there are actually four different types of keyframes
available:

LinearKeyframe - it interpolates linearly in vector space from the previous keyframe.


SpringKeyframe - uses a spring function to interpolate to the target value from the
previous keyframe.
CubicKeyframe - uses a cubic Bézier curve to interpolate between keyframes.
MoveKeyframe - immediately jumps to a value without interpolation.

Try to explore and test different keyframe types and durations to see their behaviors in
action. By experimenting with various keyframe types and adjusting the duration, you
can gain a deeper understanding of how they impact and shape your animations.

Currently, we only apply a single change for the scale property. You are free to define
other value changes over time. Here is an example:

KeyframeTrack(\.scale) {
CubicKeyframe(0.8, duration: 0.2)
CubicKeyframe(0.6, duration: 0.3)
CubicKeyframe(1.0, duration: 0.3)
CubicKeyframe(0.8, duration: 0.2)
CubicKeyframe(0.6, duration: 0.3)
CubicKeyframe(1.0, duration: 0.3)
}

The code describes the scale factor at specific times within the animation. In the preview
canvas, you'll notice a smoother and more fluid animation for the emoji icon.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 974


Figure 3. A smoother and more fluid animation

Multiple Keyframe Tracks


Up until now, we have focused on a single keyframe track to alter the scale factor.
However, keyframes provide the ability to animate multiple effects independently by
defining separate tracks, each with its own unique timing. By incorporating multiple
tracks, we can simultaneously animate various properties, enabling us to create more
advanced animations.

In the same demo, we can define separate keyframe tracks for the vertical stretch,
translation, and opacity properties. Here is the sample code:

Text(" ")
.font(.system(size: 100))
.keyframeAnimator(initialValue: AnimationValues()) { content, value in

content
.scaleEffect(value.scale)
.scaleEffect(y: value.verticalStretch)
.offset(value.translation)
.opacity(value.opacity)

} keyframes: { _ in
KeyframeTrack(\.verticalStretch) {
LinearKeyframe(1.2, duration: 0.1)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 975


SpringKeyframe(2.0, duration: 0.2, spring: .snappy)
CubicKeyframe(1.05, duration: 0.3)
CubicKeyframe(1.2, duration: 0.2)
CubicKeyframe(1.1, duration: 0.32)
CubicKeyframe(1.2, duration: 0.2)
CubicKeyframe(1.05, duration: 0.25)
CubicKeyframe(1.3, duration: 0.23)
CubicKeyframe(1.0, duration: 0.3)
}

KeyframeTrack(\.scale) {
CubicKeyframe(0.8, duration: 0.2)
CubicKeyframe(0.6, duration: 0.3)
CubicKeyframe(1.0, duration: 0.3)
CubicKeyframe(0.8, duration: 0.2)
CubicKeyframe(0.6, duration: 0.3)
CubicKeyframe(1.0, duration: 0.3)
}

KeyframeTrack(\.translation) {
SpringKeyframe(CGSize(width: 100, height: 100), duration: 0.4)
SpringKeyframe(CGSize(width: -50, height: -300), duration: 0.4)
SpringKeyframe(.zero, duration: 0.2)
SpringKeyframe(CGSize(width: -50, height: 200), duration: 0.3)
SpringKeyframe(CGSize(width: -90, height: 300), duration: 0.3)
SpringKeyframe(.zero, duration: 0.4)
}

KeyframeTrack(\.opacity) {
LinearKeyframe(0.5, duration: 0.2)
LinearKeyframe(1.0, duration: 0.23)
LinearKeyframe(0.7, duration: 0.25)
LinearKeyframe(1.0, duration: 0.33)
LinearKeyframe(0.8, duration: 0.2)
LinearKeyframe(1.0, duration: 0.23)
}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 976


By employing multiple keyframe tracks, we can achieve an intriguing animation effect. In
this case, the emoji icon will move around randomly, while its opacity varies at specific
points in time.

Figure 4. A move around animation

By default, keyframe animator keeps running the animation continuously. If you want to
stop the animation, you can set the repeat parameter to false .

You can use ZStack and overlay another emoji icon to create animation as shown below.
I'll leave this as an exercise for you to explore and implement.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 977


Figure 5. A complex animation with two emoji icons

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 978


Summary
KeyframeAnimator is a valuable feature introduced in iOS 17. In contrast to the
PhaseAnimator modifier, this new tool in SwiftUI empowers developers to create
advanced animations using keyframes.

By leveraging keyframes, developers can define specific points in time and precisely
manipulate the properties of their animations. This enhanced control allows for the
creation of intricate and dynamic visual effects, resulting in a more fluid animations.

For reference, you can download the demo project here:

Demo project
(https://www.appcoda.com/resources/swiftui5/SwiftUIKeyframeAnimator.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 979


Chapter 56
Using TipKit to Display Tooltips
It's always important to make your app as intuitive as possible. However, for some
features, it may be helpful to provide extra information to teach users how to use them
effectively. That's where TipKit comes in. Introduced in iOS 17, TipKit is a framework for
displaying tips in your app, allowing developers to offer additional guidance and ensuring
users to make the most of your app’s features.

In this tutorial, we will explore the TipKit framework and see how to create tips for a
demo app.

Using the TipKit Framework


To use the TipKit framework, you have to first import it into your project:

import TipKit

Understanding the Tip Protocol


To create a tip using the TipKit framework, you need to adopt the Tip protocol to
configure the content of the tip. Tips consist of a title and a short description. Optionally,
you can include an image to associate with the tip.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 980


Figure 1. A sample tip with a title, a short description, and an image

For example, to setup the “Save as favorite” tip, you can create a struct that conforms to
the Tip protocol like this:

struct FavoriteTip: Tip {


var title: Text {
Text("Save the photo as favorite")
}

var message: Text? {


Text("Your favorite photos will appear in the favorite folder.")
}
}

If you want to add an image to the tip, you can define the image property:

struct FavoriteTip: Tip {


var title: Text {
Text("Save the photo as favorite")
}

var message: Text? {


Text("Your favorite photos will appear in the favorite folder.")
}

var image: Image? {


Image(systemName: "heart")
}
}

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 981


Displaying Tips Using Popover and TipView
The TipKit framework provides the flexibility to display tips either as a popover or an
inline view. In the popover view, it appears over your app's UI, which could be a button,
an image, or other UI elements. On the other hand, the inline view behaves like other
standard UI elements, adjusting its position to fit around other views, ensuring that no
UI elements are blocked.

Figure 2. Display tips in the demo app

To show the tip as an inline view, you can create an instance of TipView and pass it the
tip to display. Here is an example:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 982


private let getStartedTip = GetStartedTip()

var body: some View {


.
.
.

TipView(getStartedTip)

.
.
.
}

If you want to display a tip as a popover view, you can attach the modifier popoverTip to
the button or other UI elements:

private let favoriteTip = FavoriteTip()

Image(systemName: "heart")
.font(.system(size: 50))
.foregroundStyle(.white)
.padding()
.popoverTip(favoriteTip, arrowEdge: .top)

To enable the appearance of tips within your apps, the final step is to configure the Tips

center. Assuming your Xcode project is named TipKitDemo , you can switch over to
TipKitDemoApp and update the struct like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 983


@main
struct TipKitDemoApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.task {
try? Tips.configure([
.displayFrequency(.immediate),
.datastoreLocation(.applicationDefault)
])
}
}
}
}

We can customize the display frequency and the data store location by utilizing the
configure method of the Tips center. In the code snippet above, the display frequency
is set to immediate , which means the tips will be shown right away. If you prefer the tips
to appear once every 24 hours, you can use the .daily option. Moreover, you have the
flexibility to customize the display frequency to any desired time interval, as
demonstrated in the following example:

let threeDays: TimeInterval = 3 * 24 * 60 * 60

Tips.configure([
.displayFrequency(threeDays),
.dataStoreLocation(.applicationDefault)
])

With the Tips center configured, you should be able to see the tips when running the
app in the simulator.

Previewing the Tips

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 984


Figure 3. Showing tips in the preview canvas

If you want to preview the tips in the preview canvas, you also need to set up the Tips

center in the #Preview block. Here is an example:

#Preview {
ContentView()
.task {
try? Tips.resetDatastore()

try? Tips.configure([
.displayFrequency(.immediate),
.datastoreLocation(.applicationDefault)
])
}
}

An important point to note is the inclusion of an extra line of code for resetting the data
store. Once a tip is dismissed, it won't be displayed again in the app. However, when it
comes to previewing the app and ensuring that the tips are consistently shown, it is
recommended to reset the data store.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 985


Dismissing the Tips
Users have the option to dismiss a tip by tapping the X symbol. If there is a need to
dismiss the tip view programmatically, you can utilize the invalidate method and
provide a specific reason as demonstrated below:

getStartedTip.invalidate(reason: .actionPerformed)

The reason actionPerformed means that the user performed the action that the tip
describes.

Specifying Display Rules


The Tip protocol has an optional property for you to set tup the display rules of the tip.
It supports two types of rules: parameter-based and event-based. Parameter-based rules
are ideal for displaying tips based on specific Swift value types. On the other hand, event-
based rules enable you to define actions that need to be fulfilled before a user becomes
eligible to receive a tip.

For instance, the favorite tip should only be displayed after the “Getting Started” tip. We
can set up the parameter-based rule like this:

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 986


struct FavoriteTip: Tip {

var title: Text {


Text("Save the photo as favorite")
}

var message: Text? {


Text("Your favorite photos will appear in the favorite folder.")
}

var rules: [Rule] {


#Rule(Self.$hasViewedGetStartedTip) { $0 == true }
}

@Parameter
static var hasViewedGetStartedTip: Bool = false
}

In the code above, we introduce a parameter called hasViewedGetStartedTip using the


@Parameter macro, initially set to false . The rules property incorporates a rule that
validates the value of the hasViewedGetStartedTip variable, indicating that the tip should
be displayed when the value is true .

When the image is tapped, the “Getting Started” tip is dismissed. In the same closure, we
can set the value of hasViewedGetStartedTip to true .

.onTapGesture {
withAnimation {
showDetail.toggle()
}

getStartedTip.invalidate(reason: .actionPerformed)

FavoriteTip.hasViewedGetStartedTip = true
}

Upon launching the app, only the "Getting Started" tip is displayed. However, once you
tap the image to dismiss the tip, the app then presents the "Favorite" tip.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 987


Figure 4. The favorite tip is shown after users tap the image

You can also define event-based rules to achieve the same result. Instead of using
@Parameter to define the hasViewedGetStartedTip variable, you configure the variable as a
Tips.Event like this:

static let hasViewedGetStartedTip = Tips.Event(id: "hasViewedGetStartedTip")

When employing event-based rules, you verify if the donation count meets your specified
criteria. In this case, we examine whether the hasViewedGetStartedTip action has taken
place at least once within your app.

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 988


var rules: [Rule] {
#Rule(Self.hasViewedGetStartedTip) {
$0.donations.count >= 1
}
}

To increase the event count, you use donate() to donate to the event when the action
occurs. For the demo, I'll donate the event when the "Get Started" tip is dismissed. As a
result, we can update the onTapGesture closure like this:

.onTapGesture {
withAnimation {
showDetail.toggle()
}

getStartedTip.invalidate(reason: .actionPerformed)

Task {
await FavoriteTip.hasViewedGetStartedTip.donate()
}

By invoking the donate() method, the donation count for hasViewedGetStartedTip will
increment by one. This, in turn, will trigger the display of the favorite tip.

Summary
In this tutorial, we covered the TipKit framework available on iOS 17. It's a handy tool for
showcasing hidden app features and teaching users how to effectively utilize them. With
TipKit, you can effortlessly create and display tips to enhance the user experience. If you
find TipKit useful, consider integrating it into your next app update for added benefits.

For reference, you can download the demo project here:

Demo project (https://www.appcoda.com/resources/swiftui5/SwiftUITipKit.zip)

Mastering SwiftUI for iOS 17 and Xcode 15 | AppCoda © 2023 989

You might also like