9.
List Views
Written by Joey deVilla
Ready to get started on your next app? Here you go!
To-do list apps are one of the most popular types of app on the App Store. Do a search on the web for “iOS to-do list apps” and you’ll see reviews of dozens of list apps, even though iOS comes bundled with the Reminders app. From pilots to busy parents to surgeons to Santa Claus, many people need to have a good checklist.
Building a to-do list app is a rite of passage for budding iOS developers. It’s the kind of app that’s so useful that it has its own category. Making one will teach you to master code features and functionality that you’ll end up using in so many other apps. It makes sense for you to create one as well.
Your own to-do list app, Checklist, will look like this when you’re finished:
This app lets you organize to-do items into a list with various priority levels. You can check off the items once you’ve completed them.
As far as to-do list apps go, Checklist is very basic, but don’t let that fool you. Even a simple app such as this has a lot of complexity behind the scenes. In building it, you’ll learn many different programming concepts. You’re going to continue to use SwiftUI, the brand new way to build iOS apps, to build Checklist.
This chapter will cover:
-
NavigationViewandListviews: You’ll quickly reviewNavigationView, then you’ll learn to useList. You’ll also see how both views are often used together in apps that you probably use every day. Finally, you’ll build an app that displays a basic list. -
Arrays: Just as
Lists organize arrange views in a row, arrays organize data in the same way. Arrays are so useful that they’re the most common data structure that programmers use. -
Loops: One of the reasons that computers are so useful is that they’re very good at doing the same thing over and over again, no matter how tedious it is. How do they do this? With loops. You’ll learn how to make your first dynamic list using
List, arrays and loops. - Deleting items from the list: At this point, you’ve filled the onscreen list with items. You’ll then learn how to give the user the power to delete any item with a swipe of their finger.
- Moving list items: Deleting items is pretty cool, but giving the user the ability to rearrange the items on the list is even more impressive. It’s amazing how few lines of code this takes.
- Key points: A quick review of what you’ve learned in this chapter.
NavigationView and List views
In the previous app, Bullseye, you learned to use NavigationView. NavigationView acts as a container for screens and makes it possible to navigate between them by creating a navigation hierarchy that’s similar to how you navigate the web.
Embedding Bullseye’s main screen, ContentView, inside a NavigationView lets users navigate to other screens by stacking those screens on top of ContentView. You did this by using a NavigationLink as the Info button. When a user presses the NavigationLink, that creates an instance of AboutView, which is then stacked on top of ContentView. Since AboutView is on top of the stack contained within NavigationView, the user sees that screen:
When the stack within a NavigationView is two or more screens deep, the navigation bar displays a “Back” button with the title of the screen just below. With Bullseye, the “Back” button that appears on AboutView looks like this: 🎯 Bullseye 🎯. Pressing the “Back” button removes AboutView from the stack of screens within the NavigationView, which makes ContentView visible again:
List views
As its name implies, a List view displays lists, which are rows of data arranged in a single column. This user interface element is extremely versatile, the most important one to master in iOS development.
Take a look at the apps that come with your iPhone – Notes, Reminders, Music, Mail and Settings. You’ll notice that even though they look slightly different, all these apps work the same way, using a combination of Navigation and List views.
The Music app also has a tab bar at the bottom, which you’ll learn about later on in this book.
If you want to learn how to program iOS apps in SwiftUI, you need to master these views as they make an appearance in almost every app. That’s exactly what you’ll focus on in this section of the book. You’ll also learn how to pass data from one screen to another, a very important topic that often puzzles beginners.
A basic list
Start with a static list, which is one where the items and their order don’t change. You might use this sort of list when you want to present the user with several choices or a table of contents.
This first app will simply display the following “to do” items in a list:
- Walk the dog.
- Brush my teeth.
- Learn iOS development.
- Soccer practice.
- Eat ice cream.
Go ahead and create a new Xcode Project using the Single View App template.
Since you just created a new project using the Single View App template, ContentView’s body property will look like this:
var body: some View {
Text("Hello World")
}
➤ Replace Text with List so that the declaration for body looks like this:
var body: some View {
List {
Text("Walk the dog")
Text("Brush my teeth")
Text("Learn iOS development")
Text("Soccer practice")
Text("Eat ice cream")
}
}
➤ Build and run the app in the Simulator. You’ll see the “to do” items in a list form:
Lists are often used as the “master” part of a master-detail interface. In these interfaces, the user sees a master list of items, where each item in the list contains the item’s name and a couple of details about it. The user can select an item in the master list, which takes them to the detail view for the item, which displays all its information.
Many apps that come with your iOS device, like Calendar, Messages, Notes, Contacts, Mail and Settings, use a master-detail interface. For example, Mail has a master list showing each incoming email’s sender and subject line along with a short excerpt. Selecting an email from the master list takes you to a screen that displays the full email.
On the iPhone, you construct master-detail interfaces by putting a List inside a Navigation. List functions as the master list and Navigation makes it possible to navigate to the detail view and back.
Now, put the List you just created into a Navigation view.
➤ Change ContentView’s body so that Navigation now contains List. The result should be the following:
var body: some View {
NavigationView {
List {
Text("Walk the dog")
Text("Brush my teeth")
Text("Learn iOS development")
Text("Soccer practice")
Text("Eat ice cream")
}
}
}
➤ Run the app. You’ll see that List no longer takes up the whole screen:
Navigation is a container that lets you build a hierarchy of screens that lead from one screen to another. It adds a navigation bar at the top, which can hold a title and some controls that allow the user to navigate between screens and perform edits on the screen that Navigation contains.
At the moment, the navigation bar is empty, which makes the app look like it’s missing something. Fix that by adding a title to the navigation bar using navigationBarTitle().
➤ Add a call to navigationBarTitle() to the List view so that the declaration for body becomes:
var body: some View {
NavigationView {
List {
Text("Walk the dog")
Text("Brush my teeth")
Text("Learn iOS development")
Text("Soccer practice")
Text("Eat ice cream")
}
.navigationBarTitle("Checklist")
}
}
➤ Run the app. It now has a title:
Lists with sections
There are two styles of List: plain and grouped. Right now, the app’s List uses the plain style.
Use the plain style for lists where all the items in the list are similar to one another, yet independent. One example is an email app, where each item in the List represents an email.
Use the grouped style when you can organize the items in the list by a particular attribute, like genre categories for a list of books. You could also use the grouped style table to show related information that doesn’t necessarily have to go together, like a contact’s address, contact information, and e-mail.
Lists default to the plain style. Using the grouped style requires the following:
- Using
List’slistStyle()to specify that the list should use the grouped style. - Adding a
Sectioninside theListfor each group, which appears as a separate sublist within the list.Sections can contain their own headers.
Now, change the list to the grouped style. You’ll split the list into two groups using two Section views: One for high-priority and one for low-priority tasks. The first three items in the list will be high-priority, and the last two will be low-priority.
➤ Change the declaration for body to the following:
var body: some View {
NavigationView {
List {
Section(header: Text("High priority")) {
Text("Walk the dog")
Text("Brush my teeth")
Text("Learn iOS development")
}
Section(header: Text("Low priority")) {
Text("Soccer practice")
Text("Eat ice cream")
}
}
.listStyle(GroupedListStyle())
.navigationBarTitle("Checklist")
}
}
➤ Run the app to see the grouped list in action:
The limits of views
Most people who use checklists have more than just five to-do items. At this point, make a more realistic list by adding more items so that both the high-priority and low-priority groups each have ten to-do items.
➤ Add more items to each Section so that the declaration for body looks like this:
var body: some View {
NavigationView {
List {
Section(header: Text("High priority")) {
Text("Walk the dog")
Text("Brush my teeth")
Text("Learn iOS development")
Text("Make dinner")
Text("Do laundry")
Text("Pay bills")
Text("Finish homework")
Text("Change internet provider")
Text("Read Raywenderlich.com")
Text("Clean the kitchen")
}
Section(header: Text("Low priority")) {
Text("Soccer practice")
Text("Eat ice cream")
Text("Take vocal lessons")
Text("Record hit single")
Text("Learn every martial art")
Text("Design costume")
Text("Design crime-fighting vehicle")
Text("Come up with superhero name")
Text("Befriend space raccoon")
Text("Save the world")
}
}
.listStyle(GroupedListStyle())
.navigationBarTitle("Checklist")
}
}
Make a note of the number of Texts inside each Section: Ten. This number will become important shortly.
➤ Run the app. Even on the largest iPhones, you’ll need to scroll to see the entire list:
Now, add one more item to the high-priority group in the list, making for eleven Texts inside the first Section.
➤ Add one more item to the first Section so that the declaration for body looks like this:
var body: some View {
NavigationView {
List {
Section(header: Text("High priority")) {
Text("Walk the dog")
Text("Brush my teeth")
Text("Learn iOS development")
Text("Make dinner")
Text("Do laundry")
Text("Pay bills")
Text("Finish homework")
Text("Change internet provider")
Text("Read RayWenderlich.com")
Text("Clean the kitchen")
Text("Wash the car")
}
Section(header: Text("Low priority")) {
Text("Soccer practice")
Text("Eat ice cream")
Text("Take vocal lessons")
Text("Record hit single")
Text("Learn every martial art")
Text("Design costume")
Text("Design crime-fighting vehicle")
Text("Come up with superhero name")
Text("Befriend space raccoon")
Text("Save the world")
}
}
.listStyle(GroupedListStyle())
.navigationBarTitle("Checklist")
}
}
Shortly after you add that item, Xcode will respond with a cryptic complaint: Ambiguous reference to member ‘buildBlock()’.
This is one of those technically correct, but ultimately unhelpful error messages that Xcode will surprise you with from time to time. Rather than bore you with the internal details of how SwiftUI builds user interfaces based on a View’s body, I’ll keep it simple. You’ve just run into a big limit of working with Views: They’re limited to holding a maximum of ten Views.
In a static list, the simplest way to get around this limitation is to use a Group. Its sole purpose is to provide a way for you to treat a group of two to ten views as a single view.
➤ In body’s first List, put the first six Text views into a Group, then do the same for the last five. You should end up with a body that looks like this:
var body: some View {
NavigationView {
List {
Section(header: Text("High priority")) {
Group {
Text("Walk the dog")
Text("Brush my teeth")
Text("Learn iOS development")
Text("Make dinner")
Text("Do laundry")
Text("Pay bills")
}
Group {
Text("Finish homework")
Text("Change internet provider")
Text("Read RayWenderlich.com")
Text("Clean the kitchen")
Text("Wash the car")
}
}
Section(header: Text("Low priority")) {
Text("Soccer practice")
Text("Eat ice cream")
Text("Take vocal lessons")
Text("Record hit single")
Text("Learn every martial art")
Text("Design costume")
Text("Design crime-fighting vehicle")
Text("Come up with superhero name")
Text("Befriend space raccoon")
Text("Save the world")
}
}
.listStyle(GroupedListStyle())
.navigationBarTitle("Checklist")
}
}
➤ Run the app. It should work now because the first List now contains only two views, which is well under the limit of ten.
The limits of static lists
Static lists have their uses, but they’re missing a lot of features that a checklist app needs. The user needs to be able to add and delete items from the list, edit existing items and change their order in the list.
So far, you’ve seen only one way to change the contents of a list: By changing the contents of the List view during the programming process. You lock in these changes at compile time — that is, when you compile your code into an app that the device can run. What you need is a way to change the list at run time, which is while the app is running.
This is what you’ll do next, but before you can, you’ll need to become acquainted with arrays.
Arrays
Up to this point, you’ve been working with scalar variables and constants. These are variables and constants that hold one value at any given time. You used several scalar variables and constants in the Bullseye app. Each one acted as a container for one value, such as the score, the round, the value of the target and the message that the user received based on their score.
Not all data return a single value. A lot of data come as an ordered series of values, such as the mid-day temperature of a given location for the past month, the grades for every student in a given course… or the items in a checklist. Most programming languages, Swift included, have a data type called an array, which can hold many values at the same time.
If you think of scalar variables and constants as boxes that hold one value, think of an array as a collection of boxes. Each box holds one value, and the boxes are numbered in ascending order starting with 0. The numbering scheme lets you point to the contents of a specific box (“What’s inside box 5?”) or to put a value into a specific box (“Put the value 42 into box 3”).
In an array, you call these boxes elements.
In Swift, arrays can grow by having more elements added to them; they can also shrink by having elements removed from them. You’ll use this ability, coupled with arrays’ ability to hold many values, to store information about the checklist in this app.
The key to mastering programming is to learn by doing, especially when you’re dealing with arrays and other concepts that you don’t use outside of coding. So put this knowledge into practice by setting up the app to make use of an array to store list items.
Creating an array
Your next step is to create an array to hold the checklist items. Enter the following, immediately after the start of ContentView (the struct ContentView: View { line):
var checklistItems = ["Walk the dog", "Brush my teeth", "Learn iOS development", "Soccer practice", "Eat ice cream"]
To walk through this line of code:
-
The
var checklistItemssays “This is a variable namedchecklistItems. It’s not followed by a colon (:), which means that it’s up to Swift to infer what kind of variablechecklistItemsis. -
The
=in Swift stands for “takes the following value”. You should readvar checklistItems =as “This is a variable namedchecklistItemsand it takes the following value.” -
The simplest way to define an array is to take a list of values separated by commas (
,) and surround them with square brackets, which is what appears on the right side of the=. This value is an array of the five strings that made up our original set of checklist items.
Accessing array elements
The name checklistItems refers to the entire array. When you need to provide the entire array to an object or method, as you’ll need to do shortly, you’ll use that.
To access a specific element of an array, you use the array’s name followed by the number of the element you want to access in square brackets. For example, if you wanted to access element 2 of the checklistItems array, you’d use this syntax:
checklistItems[2]
The number inside the square bracket specifies its location in the array; it’s called the index. The combination of the square brackets and the number inside is called the subscript. In the case of checklistItems[2]:
-
checklistItems[2]is an element ofchecklistItems. -
2is the index that specifies the third element. Remember, the first array index is 0, not 1. -
[2]is the subscript that specifies that you want a specific element of the array and not the whole thing.
Now that you know how to access individual array elements, it’s time to put that knowledge to use.
➤ Change the body property to the following:
var body: some View {
NavigationView {
List {
Text(checklistItems[0])
Text(checklistItems[1])
Text(checklistItems[2])
Text(checklistItems[3])
Text(checklistItems[4])
}
.navigationBarTitle("Checklist")
}
}
Just so that there isn’t any confusion at this point, the complete code for ContentView should look like this:
struct ContentView: View {
var checklistItems = ["Walk the dog", "Brush my teeth", "Learn iOS development", "Soccer practice", "Eat ice cream"]
var body: some View {
NavigationView {
List {
Text(checklistItems[0])
Text(checklistItems[1])
Text(checklistItems[2])
Text(checklistItems[3])
Text(checklistItems[4])
}
.navigationBarTitle("Checklist")
}
}
}
➤ Before we get into a discussion about array syntax, run the app to see what the new code does. You should see this:
Changing array elements and responding to taps on list items
Changing an array element is simple: You put the array element you want to change on the left side of an = and the value you want to change it to on the right side. For example, to change the contents of the first item in checklistItems to “Take the dog to the vet”, you’d use this code:
checklistItems[0] = "Take the dog to the vet"
Now, change the list so that tapping on the first item in the list, Walk the dog, changes the item to Take the dog to the vet. You’ll take advantage of a method built into every view object: onTapGesture(), which causes code to be executed whenever a user taps the view.
➤ Update body so that it looks like this:
var body: some View {
NavigationView {
List {
Text(checklistItems[0])
.onTapGesture {
self.checklistItems[0] = "Take the dog to the vet"
}
Text(checklistItems[1])
Text(checklistItems[2])
Text(checklistItems[3])
Text(checklistItems[4])
}
.navigationBarTitle("Checklist")
}
}
Note that you use self.checklistItems[0] instead of just plain checklistItems[0]. That’s because the code in the braces after .onTapGesture is a closure — a self-contained bit of code that you can pass around as if it were a value like an integer or a string. That means it needs to use the self keyword to refer to checklistItems.
Even with the use of self, Xcode still reports an error: Cannot assign through subscript: ’self’ is immutable…
Oh look, it’s another technically correct but not-so-helpful message from Xcode! What does it mean?
To keep things simple here, just remember that by default, code inside a struct object is not allowed to change the values of that struct’s properties. Normally, only code outside the struct is allowed to do that.
Note the emphasized words in the previous paragraph: by default and normally. There are a couple of ways for code inside a struct to change the values of that struct’s own properties — and better yet, you’ve already used one of them!
That way is the @State attribute, which marks a property as a state variable. Remember, state variables determine what happens in the app, and the user’s actions often affect them, and the view must respond to those changes. Marking a property with @State makes it exempt from the rule that code inside a struct can’t change that struct’s own properties.
So go ahead and add the @State attribute to the declaration of checklistItems. While you’re at it, reformat checklistItems so that it’s easier to read and change.
➤ Update the declaration of checklistItems to the following:
@State var checklistItems = [
"Walk the dog",
"Brush my teeth",
"Learn iOS development",
"Soccer practice",
"Eat ice cream",
]
Now that it’s marked as a state variable, checklistItems is exempt from the “structs can’t modify their own properties” rule and the onTapGesture() code can now change the value inside checklistItems[0]. As a result, the error message should disappear.
Note the new formatting, which makes the checklistItems array is easier to read. It’s also easier to get a sense of how many items are in the array. Swift ignores most “white space” — spaces, tabs, new lines and so on — which allows you to format the code for maximum legibility.
Note that the last item, Eat ice cream, has a , after it, even though it’s not followed by another item. That’s not an error; that’s a deliberate addition that makes it easy to add another item after it, should it become necessary. Good code is code that’s easy to update.
➤ Run the app and tap the “Walk the dog” item in the list. By default, SwiftUI is a little fussy and won’t register the tap unless you tap right on the Walk the dog text. When tapped, the item should change to “Take the dog to the vet”:
The limits of the current approach
Suppose you added an additional item — “Learn every martial art” — to checklistItems:
@State var checklistItems = [
"Walk the dog",
"Brush my teeth",
"Learn iOS development",
"Soccer practice",
"Eat ice cream",
"Learn every martial art",
]
If you were to run the app, it wouldn’t display the newly-added item. That’s because List in the body is currently set to display Text views for checklistItems[0] through checklistItems[4]:
List {
Text(checklistItems[0])
.onTapGesture {
self.checklistItems[0] = "Take the dog to the vet"
}
Text(checklistItems[1])
Text(checklistItems[2])
Text(checklistItems[3])
Text(checklistItems[4])
}
What you need is a way for the list to display the complete contents of checklistItems without having to make any changes to body as the array changes. To help you do that, it’s time to introduce a concept that goes hand-in-hand with arrays (and many other aspects of programming): loops.
Loops
Up to this point, you’ve experienced two different kinds of flow control, or ways of executing your code. Let’s look at them, and then you’ll learn a new kind of flow control: Looping.
Flow control
The first kind of flow control that you’ve seen is sequence, which is simply performing instructions in the order in which they appear.
Here’s an example from Bullseye:
func startNewGame() {
score = 0
round = 1
resetSliderAndTarget()
}
In startNewGame(), the code performs its instructions in order. First, the value of score is set to 0, then the value of round is set to 1 and finally, it executes resetSliderAndTarget().
You’ve also seen the second kind of flow control: Branching, which some computer science people also like to call selection. This is the “decision-making” flow control, which offers two or more courses of action, depending on some kind of test. Here’s an example from Bullseye:
func alertTitle() -> String {
let title: String
if sliderTargetDifference == 0 {
title = "Perfect!"
} else if sliderTargetDifference < 5 {
title = "You almost had it!"
} else if sliderTargetDifference <= 10 {
title = "Not bad."
} else {
title = "Are you even trying?"
}
return title
}
In alertTitle(), the code performs different instructions based on the value of sliderTargetDifference. Each of these instructions is a branch in a structure of multiple choices called a decision tree.
It’s time to introduce a third kind of flow control: Looping, which some computer science people also call iteration. This is the “repetition” flow control, where instructions are performed over and over again, either indefinitely or until some condition is met.
You’re going to use looping to go through the elements in checklistItems, one at a time, in order, to display each one onscreen. You can also call this process looping through or iterating through each element.
for loops
You’ll start by displaying all the elements in checklistItems in Xcode’s debug console. Another way of saying this is “For every item in checklistItems, print its name,” which is pretty close to the way you’d code it in Swift:
for item in checklistItems {
print(item)
}
The code above goes through every item in checklistItems, starting with the item in index 0 and ending after the last item in the array. item is a temporary variable that exists only as long as we’re still in the loop. The first time through the loop, item is set to the first item in the array, “Walk the dog”.
The code inside the loop is then executed: It’s print(item), which prints “Walk the dog” in Xcode’s debug console. This completes the first iteration of the loop.
The program returns to the beginning of the loop, and there are still more items in checklistItems to go through. item is set to the next item in the array, “Brush my teeth”, after which the code in the loop executes, printing “Brush my teeth” in Xcode’s debug console. The second iteration of the loop is now complete.
The program returns to the beginning of the loop, and the cycle repeats until it’s gone through every item in checklistItems.
You can see this process in action.
➤ First, make sure that checklistItems has the original five items:
@State var checklistItems = [
"Walk the dog",
"Brush my teeth",
"Learn iOS development",
"Soccer practice",
"Eat ice cream",
]
Take the loop code shown earlier and put it inside a method.
➤ Enter the following method after the end of body:
func printChecklistContents() {
for item in checklistItems {
print(item)
}
}
Finally, you need to do two things with body:
- Since you won’t be displaying the contents of
checklistItemsonscreen yet, you’ll simplify the list that appears onscreen. - You’ll also use
onAppear(), which is built into every view, to callhowChecklistContents()when the list is first drawn onscreen.
➤ Change body to the following:
var body: some View {
NavigationView {
List {
Text("Nothing to see here...yet!")
}
.navigationBarTitle("Checklist")
.onAppear() {
self.printChecklistContents()
}
}
}
With all the changes you made, ContentView should look like this:
struct ContentView: View {
@State var checklistItems = [
"Walk the dog",
"Brush my teeth",
"Learn iOS development",
"Soccer practice",
"Eat ice cream",
]
var body: some View {
NavigationView {
List {
Text("Nothing to see here...yet!")
}
.navigationBarTitle("Checklist")
.onAppear() {
self.printChecklistContents()
}
}
}
func printChecklistContents() {
for item in checklistItems {
print(item)
}
}
}
➤ Run the app but don’t bother looking at the simulator. All the action is happening in the Xcode window, in the debug console:
When List first appears on the screen, its onAppear() is called, which then calls printChecklistContents(). printChecklistContents() goes through checklistItems in order, printing each item’s value in the debug console.
As you’ve seen before, printing to the debug console doesn’t affect the user interface; it’s only for the benefit of the programmer. How do you show the contents of the checklist to the user?
The ForEach view
SwiftUI has a view called ForEach that takes a collection of data, such as an array, and generates views based on that data. This is one of those cases where “Show, don’t tell” is the better approach, so take a look at ForEach in action first, then you’ll look at it in more detail afterward.
➤ Change body to the following:
var body: some View {
NavigationView {
List {
ForEach(checklistItems, id: \.self) { item in
Text(item)
}
}
.navigationBarTitle("Checklist")
.onAppear() {
self.printChecklistContents()
}
}
}
➤ Run the app. You’ll see this:
For the user, there’s no difference between an app that uses ForEach and one that simply uses a Text view for every item in checklistItems. The benefit of ForEach becomes clear when you make changes to checklistItems.
➤ Change the declaration of checklistItems to the items on the following page, which has no items in common with the original list.
@State var checklistItems = [
"Take vocal lessons",
"Record hit single",
"Learn every martial art",
"Design costume",
"Design crime-fighting vehicle",
"Come up with superhero name",
"Befriend space raccoon",
"Save the world",
"Star in blockbuster movie",
]
➤ Run the app to see this all-new list:
With ForEach, you don’t have to make any changes to the user interface to change the items in the checklist. You only have to change the underlying data.
Take a closer look at ForEach, which you added to body:
ForEach(checklistItems, id: \.self) { item in
Text(item)
}
There are four key parts here:
First, there’s the collection of data that you’re using ForEach to display in a view. In this case, that collection of data is checklistItems.
Next is a “digital fingerprint” that uniquely identifies each element in the collection of data provided to ForEach. That’s what id: is for. SwiftUI uses it to update the views that ForEach creates when a user moves or deletes elements in the collection, and when they add new ones. For this parameter, you use \.self, which means: “Use the item’s value as its identifier.”
Then there’s the value representing the current collection item, which lets you refer to that item in the code in the ForEach block. If you’re using the most recent version of checklistItems, this value will be ““Eat ice cream” during the first pass through ForEach, then “Take vocal lessons”, then “Record hit single” and so on, until you hit the final item, “Star in blockbuster movie”.
And finally, there’s the block, which contains the code that specifies the views that will display the collection of data. In this case, it’s a Text view that contains the current collection item.
The similarity between for and ForEach is intentional, especially the way the values represent the collection of data that you pass to them and the value that represents the item in the collection that you’re currently working on.
The diagram below shows how the two relate to each other:
Deleting items from the list
Now that the list is based on an array and is no longer a “hardwired” part of the user interface, changing the array’s contents can change the list’s contents.
Adding items to the end of an array (and the checklist)
Since arrays are ordered lists of items, and lists often grow by adding items to the end, arrays have an append() method, which lets you add new items to the end of the array.
Suppose you have an array named myArray that contains the following items:
- This is an item.
- This is another item.
- Third item!
If you wanted to add another item, “One more item!!!” to the array, you’d use the append() method this way:
append("One more item!!!")
Once that code executes, myArray would contain:
- This is an item.
- This is another item.
- Third item!
- One more item!!!
You’d be able to access this new item with myArray[3]. Remember, array indexes begin at 0, so the fourth item is at index 3.
Now, try out append() in the app. Now that List is connected to checklistItems, which is a state variable, adding an item to the end of the array will add an item to the end of the List.
You’ll change the app so that tapping on a checklist item will add a copy of that item to the end of the list. You’ll use onTapGesture() to respond to the user’s taps.
➤ Start by changing checklistItems back to its original contents:
@State var checklistItems = [
"Walk the dog",
"Brush my teeth",
"Learn iOS development",
"Soccer practice",
"Eat ice cream",
]
➤ Change body to the following:
var body: some View {
NavigationView {
List {
ForEach(checklistItems, id: \.self) { item in
Text(item)
.onTapGesture {
self.checklistItems.append(item)
self.printChecklistContents()
}
}
}
.navigationBarTitle("Checklist")
.onAppear() {
self.printChecklistContents()
}
}
}
Take a look at the ForEach view, which contains the lines of code you just added:
ForEach(checklistItems, id: \.self) { item in
Text(item)
.onTapGesture {
self.checklistItems.append(item)
self.printChecklistContents()
}
}
You added a call to the Text view’s onTapGesture(), which contains code that does two things:
-
It uses
append()to additemto the end ofchecklistItems. This means that tapping on the “Walk the dog” list item adds a new “Walk the dog” item to the end ofchecklistItems, which in turn causes a new “Walk the dog” item to appear at the end of the list onscreen. -
It calls
printChecklistContents()so that you can look at Xcode’s debug console to confirm that a new item was added tochecklistItems.
➤ Run the app and tap the Walk the dog item. You’ll see a new Walk the dog item at the end of the list…
If you look at Xcode’s debug console, you’ll see the output of printChecklistContents(), which shows that checklistItems contains the following items in the given order:
- Walk the dog.
- Brush my teeth.
- Learn iOS development.
- Soccer practice.
- Eat ice cream.
- Walk the dog.
Later on, you’ll change the app so that the user will be able to add new items of their choice instead of simply adding duplicates of existing items. The underlying principle will still be the same, however: You’ll use append() to add the new, user-provided item to the end of checklistItems.
Removing items from an array (and the checklist)
There are several methods that remove an element from an array. You’ll try two of them out in your app.
Removing items using remove(at:)
The simplest one is remove(at:), which removes the element at the given index. For example, if you wanted to remove the first element of checklistItems, you’d use the code checklistItems.remove(at: 0).
Now, use that code by changing the app so that tapping a list item removes the first item from the list. Do this by replacing this line in onTapGesture():
self.checklistItems.append(item)
with this:
self.checklistItems.remove(at: 0)
➤ Incorporate this change by changing body to:
var body: some View {
NavigationView {
List {
ForEach(checklistItems, id: \.self) { item in
Text(item)
.onTapGesture {
self.checklistItems.remove(at: 0)
self.printChecklistContents()
}
}
}
.navigationBarTitle("Checklist")
.onAppear() {
self.printChecklistContents()
}
}
}
➤ Run the app and start tapping on list items. The list will shrink from the top, with the first item in the list disappearing with each tap until the list is empty:
Using remove(atOffsets:) to remove list items
Another way to remove list items is remove(atOffsets:). This is a bulk version of remove(at:), which removes a specific range of elements from an array. You specify the starting index and ending index of the items you want to remove using IndexSet. To see it in action, change the app so that tapping a list item removes the elements from index 0 through index 4. You do this by replacing this line in the onTapGesture() method:
self.checklistItems.remove(at: 0)
with this:
let indexesToRemove = IndexSet(integersIn: 0...4)
self.checklistItems.remove(atOffsets: indexesToRemove)
➤ Incorporate this change by changing body to:
var body: some View {
NavigationView {
List {
ForEach(checklistItems, id: \.self) { item in
Text(item)
.onTapGesture {
let indexesToRemove = IndexSet(integersIn: 0...4)
self.checklistItems.remove(atOffsets: indexesToRemove)
self.printChecklistContents()
}
}
}
.navigationBarTitle("Checklist")
.onAppear() {
self.printChecklistContents()
}
}
}
➤ Run the app and tap on any list item. That will call the onTapGesture() method, which will remove elements 0 through to 4 of checklistItems. This will empty the array, which in turn will empty the list.
Now that you know how to remove items from an array, it’s time to learn how to respond to the “swipe to delete” gesture.
Responding to the “swipe to delete” gesture
Even though many apps use the list control that comes standard with iOS, many people still don’t know the standard “swipe to delete” gesture.
In many apps, you can put your finger on a list item and drag it slightly to the left to reveal a Delete button:
To delete the list item, you can either tap the Delete button or continue swiping to the left. As your finger moves leftward, the list item gets moved offscreen and the Delete button grows wider until it’s half the width of the list.
At that point, it expands to fill the entire width and deletes the list item:
The user can cancel the delete action either by swiping to the right or by tapping on the list item.
Your next step is to add this “swipe to delete” capability to the app. Do this by using ForEach’s onDelete(perform:), which is called whenever the user completes the “swipe to delete” gesture.
onDelete(perform:) has one parameter, perform:, where you specify the name of a method that deletes the data behind the list item that the user just swiped. onDelete(perform:) will pass this method an IndexSet that starts and ends with the list item’s index, so that it knows which data to delete.
It’s time to write this method, which you’ll call deleteListItem.
➤ Add this method after printChecklistContents():
func deleteListItem(whichElement: IndexSet) {
checklistItems.remove(atOffsets: whichElement)
printChecklistContents()
}
The method removes the item whose index is contained in index, then prints the contents of checklistItems in Xcode’s debug console.
Now, add onDelete(perform:) to ForEach, which will enable “swipe to delete” for the user. You’ll also remove onTapGesture() from the Text in ForEach, just to keep things simple.
➤ Change body to the following:
var body: some View {
NavigationView {
List {
ForEach(checklistItems, id: \.self) { item in
Text(item)
}
.onDelete(perform: deleteListItem)
}
.navigationBarTitle("Checklist")
.onAppear() {
self.printChecklistContents()
}
}
}
➤ Run the app, then swipe a list item to delete it. Check printChecklistContents()’s output in Xcode’s debug console to confirm that the item you swiped no longer exists in checklistItems.
Now the user can delete items from the checklist!
Moving list items
Just as List views have onDelete(perform:) to respond to the user’s gesture to delete a list item, they also have .onMove(perform:) to respond to the user’s gesture to move a list item.
Unlike deleting items from a List, you can only move list items when the list is in “Edit” mode. To make “Edit” mode available to the user, you need to add the button that enables it to the navigation bar using navigationBarItems().
➤ Change body to the following:
var body: some View {
NavigationView {
List {
ForEach(checklistItems, id: \.self) { item in
Text(item)
}
.onDelete(perform: deleteListItem)
}
.navigationBarItems(trailing: EditButton())
.navigationBarTitle("Checklist")
.onAppear() {
self.printChecklistContents()
}
}
}
Here, you’ve added navigationBarItems() to List. Its parameter adds an Edit button to the “trailing” side of the navigation bar. In languages like English, which read from left to right, the trailing side is the right side.
➤ Run the app. There’s now an Edit button in the navigation bar:
➤ Tap the Edit button to switch to edit mode. The screen will look like this:
In edit mode, each list item has a Delete button, which looks like a minus sign in a red circle, on its left side. You can tap the button to delete the item. There’s still no visual indicator for moving list items, because you haven’t made use of .onMove(perform:) yet.
Like onDelete(perform:), .onMove(perform:) has a perform: parameter, where you specify the name of a method that moves the data behind the list item that the user just moved. .onMove(perform:) will pass this method an IndexSet that starts and ends with the list item’s index, so that it knows which data to move and an Int that indicates where to move it to. Now, write this method, which you’ll call moveListItem.
➤ Add this method after deleteListItem():
func moveListItem(whichElement: IndexSet, destination: Int) {
checklistItems.move(fromOffsets: whichElement, toOffset: destination)
printChecklistContents()
}
The first line of moveListItem(whichElement:destination:) uses an array method you haven’t seen yet: move(fromOffsets:, toOffset:), which moves one or more array elements within the array. You put the indexes of the starting and ending elements that you want to move in fromOffsets:, and the index of the place to move them to in toOffset:.
The second line displays the newly-rearranged contents of checklistItems so that you can confirm that the array element moved correctly.
Now, make use of oveListItem in .onMove.
➤ Change body to the following:
var body: some View {
NavigationView {
List {
ForEach(checklistItems, id: \.self) { item in
Text(item)
}
.onDelete(perform: deleteListItem)
.onMove(perform: moveListItem)
}
.navigationBarItems(trailing: EditButton())
.navigationBarTitle("Checklist")
.onAppear() {
self.printChecklistContents()
}
}
}
➤ Run the app and tap the Edit button. This time, when you enter edit mode, you see the Delete buttons on the left side of each list item and move handles on the right side:
➤ Press down on the move handle of any list item and drag it to a new location:
When you let go of the item, it “snaps” to the closest “slot”, changing the order of the list. You can confirm that checklistItems reflects the onscreen changes by checking the output of printChecklistContents() in Xcode’s debug console.
Key points
In this chapter, you did the following:
- You started with a static list of items that were “hard-wired” into the user interface, using both plain and grouped styles.
- You learned about arrays, the most-used data structure, and loops, one of the key means of flow control in programs.
- You applied your newly-gained knowledge about arrays and loops, as well as
ForEach, to build a dynamic list. The contents of that list aren’t hard-wired into the user interface; underlying data determine the contents. - With your dynamic list, you used the power of SwiftUI to give the user the ability to delete items from the list and rearrange the list’s items.
Phew! That was a lot of new stuff to take in, so I hope you’re still with me. If not, then take a break and start at the beginning again. You’re learning a whole bunch of new concepts all at once, and that can be overwhelming.
But don’t worry, it’s OK if everything doesn’t make perfect sense yet. As long as you get the gist of what’s going on, you’re good to go.
If you want to check your work up to this point, you can find the project files for the app under 09 - List Views in the Source Code folder.