Chapters

Hide chapters

iOS Apprentice

Eighth Edition · iOS 13 · Swift 5.2 · Xcode 11

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

My Locations

Section 3: 11 chapters
Show chapters Hide chapters

Store Search

Section 4: 12 chapters
Show chapters Hide chapters

48. List Views
Written by Joey deVilla

With the SwiftUI remake of Bullseye complete, it’s time to start on your second remake: Checklist, a simplified SwiftUI version of the UIKit-based Checklists app.

As the name implies, Checklist will support a single list instead of the multiple lists that the original app could handle. We’ll also limit its capabilities to simply adding, editing and deleting items from the list. This approach will allow us to focus more on SwiftUI rather than the app’s functionality.

Here’s what the finished app will look like:

The finished Checklist app
The finished Checklist app

This chapter covers the following:

  • A brand new project: You’ll create a new project for the SwiftUI-based Checklist app and take a closer look at the content view and preview structs.
  • Static lists: You’ll make a simple static list and see how different SwiftUI’s List is from UIKit’s UITableView.
  • Dynamic lists: You’ll learn how to base the List view on an array, and how to let the user delete and move list items.

A brand new project

Start a new project using the Single View App template:

The finished Checklist app
The finished Checklist app

In the Choose options for your new project pop-up, enter Checklist in the Product Name field, and make sure that Swift and SwiftUI are selected in the Language and User Interface fields.

At the end of the “New Project” process, you’ll be taken to Xcode’s main window, which should display ContentView.swift. Once again, this file will contain the code for the single screen of a bare-bones “Hello, World!” project, featuring these two structs:

  1. ContentView, which is responsible for drawing the user interface, and
  2. ContentView_Previews, which draws a live preview of the interface in the Canvas.

The content view

Now that you’ve read the previous chapter and have been briefed on structs and protocols, we can look at ContentView in greater depth. It looks pretty simple, but you now know that there’s a lot going on underneath:

The bones of ContentView
The bones of ContentView

The app’s single screen or view is defined by the struct named ContentView, which adopts or conforms to the View protocol. This protocol defines the properties and methods for a view, which is anything that can be drawn onscreen, such as a label, a button, a slider and so on. In the case of ContentView, the view in question is the app’s “screen” or “page”.

The SwiftUI designers designed the View protocol to be easy to use. Any object that adopts it has to include just one thing: a variable property named body, whose type must be some kind of view, or more technically, an object that adopts the View protocol. By adopting the View protocol, ContentView gains the ability to be drawn onscreen and respond to user interaction and other events.

Remember: You may find it helpful to read the : character in Swift as “is a(n).” For example, you can use it to read var count: Int as “the variable count is an integer” and struct ContentView: View as “the struct ContentView is a View.”

The second line, var body: some View can be read as “the variable body is a some View. We covered this before, but it’s worth mentioning again: some View means “some type of view,” or in more technical terms, “some type that adopts the View protocol.” The specific type is determined by the value returned by the closure that follows some View. In this case, the closure returns a Text object, which is a kind of view.

The preview

ContentView_Previews provides a regularly-updated preview of what ContentView will look like as you code. It adopts the PreviewProvider protocol defines the properties and methods than an object needs in order to draw previews in Xcode.

Any object that adopts the PreviewProvider protocol has to include just one thing: a static variable property named previews whose type is some View. Normally, you’d want this property to contain the screen you want to preview, which in this case is an instance of ContentView, which is why previews contains ContentView().

Remember, ContentView refers to the “blueprint”, while ContentView() — note the parentheses — creates an instance of ContentView.

Static lists

UIKit table views and yak shaving

There’s a term called “yak shaving” that refers to small tasks that seem silly and seemingly unrelated to your goal until you put them all together. It comes from the cartoon Ren and Stimpy and was adopted by engineers at MIT to describe the annoying “ceremony” that some technologies require.

Table views in UIKit are a good example of yak shaving in action. Think about the tasks involved in setting up a UITableView inside a UIViewController. You need to make its containing view controller conform to a couple of protocols, set up methods to report the number of sections in the table and the number of rows in each section, and then there’s the matter of generating and recycling table cells. When you first used a table view, you probably wondered if displaying lists in iOS had to be so complicated.

I have good news for you: while SwiftUI’s equivalent — the List view — is based on the same underlying operating system code that makes up UIKit’s table views, it requires considerably less yak shaving on your part. Let’s get started with the SwiftUI version of the first list you built oh-so-many chapters ago.

Just as you did with the original Checklists app, let’s make the app display a static list of these familiar to-do items:

  • Walk the dog
  • Brush my teeth
  • Learn iOS development
  • Soccer practice
  • Eat ice cream

We’ll do this by replacing the text view in ContentView’s body property with a List view containing a set of Text views, one for each list item.

➤ Update ContentView 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")
  }
}

List is one of those views that can act as a container for other views. Its syntax is like that for VStack or HStack, but instead of creating vertical or horizontal stacks of the views it contains — also known as child views — it takes its child views and places them into a table view in the given order.

➤ Refresh the Canvas or run the app in the Simulator. You’ll see the “to do” items in list form, with “Walk the dog” as the first item in the list and “Eat ice cream” as the last item:

A basic list with five 'to-do' items
A basic list with five 'to-do' items

That’s all it takes to make a static list in SwiftUI. That’s much easier than the UIKit way!

Adding a navigation view

In the original Checklists, the app’s screens were contained within a UINavigationController so that the user could navigate between screens. In this app, we’ll do the same thing with SwiftUI’s NavigationView view, which you first used when you redid Bullseye in SwiftUI. Let’s add one right now.

➤ Change ContentView’s body property so that the List view is contained within a NavigationView view. 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:

The list, now contained within a navigation view
The list, now contained within a navigation view

A Navigation view 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. We can add a title to the navigation bar by calling the navigationBarTitle() method of any child view of the Navigation view. In this case, Navigation has only one child view — the List, so we’ll call the List’s navigationBarTitle() method.

➤ 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:

The app with a title in the navigation bar
The app with a title in the navigation bar

Lists with sections

Like UITableView, the List view has plain and grouped styles. Right now, the app’s List uses the default: The plain style.

In order to use the grouped style, the following are required:

  1. Using List’s listStyle() modifer to specify that the list should use the grouped style.
  2. Adding a Section view inside the List for each group. Each group will then appear a separate sublist within the list. Sections can contain their own headers.

Let’s 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 app with a grouped style list
The app with a grouped style list

The limits of views

Most people who use checklists have more than just five to-do items. Let’s make the list more realistic 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")
  }
}

Once again, note that each section has ten Text views. This number will become important shortly.

➤ Run the app. Even on the largest iPhones, you’ll need to scroll to see the entire list:

The list with ten items in each section
The list with ten items in each section

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 — a Text view that says “Wash the car” — to the end of 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 lot of error icons and one of its cryptic messages: Argument passed to call that takes no arguments:

Cryptic error messages
Cryptic error messages

This is yet another one of Xcode’s technically correct, but ultimately unhelpful error messages. What actually happened is that you’ve just run into one of the limits of SwiftUI views: View that can contain other views are limited to a maximum of ten child views. With the addition of “Wash the car”, the list’s “High priority” section contains eleven Text views.

In a static list, the simplest way to get around this limitation is to use a Group view. The sole purpose of Group is to provide a way for you to “wrap” two to ten other views into a package so they can be treated as a single view. Thing of Group as being like a VStack or an HStack, but without the vertical or horizontal arrangement.

➤ 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 view now contains only two views instead of eleven.

Dynamic lists

Switching to an array

Static lists have their uses, but you already know from experience that they’re not what you need for a checklist. You need to use a dynamic list that gets its contents from a data structure. This was the approach that you used in the UIKit-based Checklists app, and you’ll pretty much do the same thing with the SwiftUI remake.

We’ll start by adding an array property to ContentView to contain the checklist items, populating it with the original five checklist items.

➤ Enter the following, immediately after the start of ContentView:

var checklistItems = [
  "Walk the dog",
  "Brush my teeth",
  "Learn iOS development",
  "Soccer practice",
  "Eat ice cream"
]

Just for fun, let’s first use the array to populate the list in a brute-force style:

➤ Edit the List view so that it looks like this:

List {
  Text(checklistItems[0])
  Text(checklistItems[1])
  Text(checklistItems[2])
  Text(checklistItems[3])
  Text(checklistItems[4])
}

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")
    }
  }
}

➤ Run the app to see what the new code does. It should be pretty unsurprising:

The list, using an array
The list, using an array

Responding to taps on list items

In the previous section, you were introduced to a method in the View protocol called onAppear(). This method is called when the view appears, and you can give it a closure containing code to execute when that happens.

The View protocol contains a similar method called onTapGesture(). It’s called whenever the user taps the view, and like onAppear(), you can give it code to execute whenever onTapGesture() is called. We’ll use this method to do something when the user taps on the first item in the list.

➤ 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")
  }
}

Xcode will report an error: Cannot assign through subscript: ’self’ is immutable

The ’self is immutable’ error message
The ’self is immutable’ error message

You saw a similar error message that ended with ’self’ is immutable in the previous chapter. The error occurred when you tried to change a struct’s property from within the struct. That’s exactly what you’re trying to do by modifying the first element of ContentView’s checklistItems property. By default, only code outside a struct can change that struct’s properties.

You also saw that there were a couple of ways to get around the “structs can’t change their own properties” rule. One of them was to mark any method containing code that changed its struct’s properties with the mutating keyword like so:

mutating func changeThisStructsProperties() {
  // Somewhere in this method, there is code
  // that changes this struct’s properties.
}

Unfortunately, that’s not an option here, as the code that’s trying to change ContentView’s checklistItems property isn’t inside a method, but a closure. There’s no place to put the mutating keyword.

The other way around the “structs can’t change their own properties” rule is one specific to SwiftUI: Marking the properties that you want the struct’s own code to change with @State. Structs are allowed to change their state properties.

State properties are exempt from the “structs can’t change their own properties” rule because you need to be able to change them to change the view’s state. Without a change in state, the view doesn’t do anything.

We can fix the error by turning checklistItems from an ordinary property to a state propery.

➤ 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",
]

➤ 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 list, with the first item changed to ’Take the dog to the vet’ after the user tapped on it
The list, with the first item changed to ’Take the dog to the vet’ after the user tapped on it

Now that you can modify checklistItems from within the view, it’s time to look at a view that can build a list by iterating over the array.

The ForEach view

SwiftUI’s ForEach view takes a collection of data that can be iterated through — such as an array — and generates views based on that data. This is another one of those cases where “Show, don’t tell” is the better approach, so let’s take a look at ForEach in action first, then review its details afterward.

➤ Change body to the following:

var body: some View {
  NavigationView {
    List {
      ForEach(checklistItems, id: \.self) { item in
        Text(item)
      }
    }
    .navigationBarTitle("Checklist")
  }
}

➤ Run the app. You’ll see this:

The original five-element array displayed using ’ForEach’
The original five-element array displayed using ’ForEach’

Take a closer look at the ForEach view, which you added to body:

ForEach(checklistItems, id: \.self) { item in
  Text(item)
}

There are four key parts here:

The four key parts of the ForEach view
The four key parts of the ForEach view

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 the checklistItems array.

Next is a “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, we’re using \.self. This is a KeyPath, which is like an object reference, except that it points to an object property instead. The \. marks the start of the KeyPath, which is then followed by the property name. \.self means “the object’s self property. In this case, we’re telling ForEach to use the current value of the object, which is a string, as its identifier.

Next is the value representing the current collection item, which lets you refer to that item in the code in the ForEach block.

And finally, there’s the block, which contains the code that specifies the views that will display the collection’s contents. In this case, it’s a Text view that contains the current collection item.

Adding items to the list

Adding items to the list is a matter of adding items to the checklistItems array. Let’s experiment with that.

➤ Change body to the following:

var body: some View {
  NavigationView {
    List {
      ForEach(checklistItems, id: \.self) { item in
        Text(item)
          .onTapGesture {
            self.checklistItems.append(item)
        }
      }
    }
    .navigationBarTitle("Checklist")
  }
}

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)
  }
}

You added a call to the Text view’s onTapGesture() method. The closure attached to it contains a single line of code to add a copy of the tapped checklist item to the end of the checklistItems array.

➤ 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:

Adding a new item to the end of the list
Adding a new item to the end of the list

The final app will provide a button that the user will tap to add a new item to the list. This button will take them to a view where they will be able to enter that item’s details and confirm that the item should be added to list, at which point the newly-created item will be appended to the the checklistItems array.

Removing items from the list

Just as items will be added to the list by adding items to the checklistItems array, removing items from the list will be done by removing items from checklistItems.

As with the Checklists UIKit app, the SwiftUI-based Checklist will support “swipe to delete”. We’ll do this by making use of the ForEach view’s onDelete(perform:) perform method, 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.

Let’s write this method, which you’ll call deleteListItem(whichElement:).

➤ Add this method to ContentView:

func deleteListItem(whichElement: IndexSet) {
  checklistItems.remove(atOffsets: whichElement)
}

This method removes the item whose index is contained in whichElement, which will be provided by onDelete(perform:).

Now that we have deleteListItem(whichElement:), we need to call it from onDelete(perform:), which you’ll add to ForEach. You’ll also remove onTapGesture() from the Text in ForEach.

➤ Change body to the following:

var body: some View {
  NavigationView {
    List {
      ForEach(checklistItems, id: \.self) { item in
        Text(item)
      }
      .onDelete(perform: deleteListItem)
    }
    .navigationBarTitle("Checklist")
  }
}

➤ Run the app, then swipe left on a list item:

Removing an item from the list
Removing an item from the list

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")
  }
}

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:

The app with an ’Edit’ button in the navigation bar
The app with an ’Edit’ button in the navigation bar

➤ Tap the Edit button to switch to edit mode. The screen will look like this:

The app in edit mode, showing ’Delete’ buttons
The app in edit mode, showing ’Delete’ buttons

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)
}

The first line of moveListItem(whichElement:destination:) uses the array method move(fromOffsets:, toOffset:), which moves one or more array elements within the array. The indexes of the starting and ending elements to be moved go into fromOffsets:, and the index of the place to move them to goes in toOffset:.

The moveListItem(whichElement:destination:) needs to be called by a method that responds to the user attempting to move an element. That method is View’s onMove(perform:) method.

➤ 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")
  }
}

➤ 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:

The app in edit mode, showing ’Delete’ buttons and move handles
The app in edit mode, showing ’Delete’ buttons and move handles

➤ Press down on the move handle of any list item and drag it to a new location:

Dragging ’Eat ice cream’ to the top of the list
Dragging ’Eat ice cream’ to the top of the list

When you let go of the item, it “snaps” to the closest “slot”, changing the order of the list.

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 built a dynamic list using an array and the ForEach view. 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.

Now that you’ve been introduced to SwiftUI’s List view, it’s time to start building the app in earnest. We’ll get started in the next chapter!

If you want to check your work up to this point, you can find the project files for the app under 48 - List Views in the Source Code folder.

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