A SwiftUI List and Navigation Tutorial

The previous chapter introduced the List, NavigationStack, and NavigationLink views and explained how these can be used to present a navigable and editable list of items to the user. This chapter will create a project that provides a practical example of these concepts.

About the ListNavDemo Project

When completed, the project will consist of a List view in which each row contains a cell displaying image and text information. Selecting a row within the list will navigate to a details screen containing more information about the selected item. In addition, the List view will include options to add and remove entries and to change the ordering of rows in the list.

The project extensively uses state properties and observable objects to synchronize the user interface with the data model.

Creating the ListNavDemo Project

Launch Xcode and select the option to create a new Multiplatform App named ListNavDemo.

Preparing the Project

Before beginning the development of the app project, some preparatory work needs to be performed involving the addition of image and data assets which will be needed later in the chapter.

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

The assets to be used in the project are included in the source code sample download provided with the book available from the following URL:

https://www.ebookfrenzy.com/retail/ios17_web/

Once the code samples have been downloaded and unpacked, open a Finder window, locate the CarAssets.xcassets folder and drag and drop it onto the project navigator panel, as illustrated in Figure 31-1:

Figure 31-1

When the options dialog appears, enable the Copy items if needed option so that the assets are included within the project folder before clicking on the Finish button.

Adding the Car Structure

A structure needs to be declared to represent each car model. Add a new Swift file to the project by selecting the File -> New -> File… menu option, selecting Swift File in the template dialog and clicking on the Next button. Name the file Car.swift on the subsequent screen before clicking on the Create button.

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

Once created, the new file will load into the code editor, where it needs to be modified so that it reads as follows:

import SwiftUI
 
struct Car : Hashable, Codable, Identifiable {
    var id: UUID = UUID()
    var name: String
    
    var desc: String
    var isHybrid: Bool
    
    var imageName: String
}Code language: Swift (swift)

As we can see, the structure is declared as conforming to the Identifiable protocol and includes a unique identifier property so that each instance can be uniquely identified within the List view.

Adding the Data Store

When the user interface has been designed, the List view will rely on observation to ensure that the latest data is always displayed to the user. The last step in getting the data ready for use in the app is to add a data store structure. This structure will contain an array of Car objects that will be observed by the user interface to keep the List view current. Add another Swift file to the project, this time named CarStore.swift, and implement the class as follows:

import SwiftUI
 
@Observable class CarStore: Identifiable {
    
    var cars: [Car] = [Car(name: "Tesla Model 3", desc: "Luxury 4-door.", 
                              isHybrid: false, imageName: "tesla_model_3"),
                   Car(name: "Tesla Model S", desc: "5-door liftback.", 
                              isHybrid: false, imageName: "tesla_model_s"),
                   Car(name: "Toyota Prius", desc: "5-door liftback", 
                              isHybrid: false, imageName: "toyota_prius"),
                   Car(name: "Nissan Leaf", desc: "Compact five-door.", 
                              isHybrid: false, imageName: "nissan_leaf"),
                   Car(name: "Chevrolet Volt", desc: "5-door hatchback.", 
                              isHybrid: false, imageName: "chevrolet_volt")]
}Code language: Swift (swift)

With the data side of the project complete, it is now time to begin designing the user interface.

Designing the Content View

Edit the ContentView.swift file, add a state object, and initialize it with an instance of CarStore class:

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

import SwiftUI
 
struct ContentView: View {
    
    @State var carStore: CarStore = CarStore()
.
.Code language: Swift (swift)

The content view is will require a List view to display information about each car. Now that we have access to the array of cars via the carStore property, we can use a ForEach loop to display a row for each car model. The cell for each row will be implemented as an HStack containing an Image and a Text view, the content of which will be extracted from the carData array elements. Remaining in the ContentView.swift file, add a view named ListCell:

struct ListCell: View {
    
    var car: Car
    
    var body: some View {
        HStack {
            Image(car.imageName)
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: 100, height: 60)
            Text(car.name)
        }
    }
}Code language: Swift (swift)

Next, modify the ContentView body so that it reads as follows:

.
.
    var body: some View {
    
        List {              
            ForEach($carStore.cars, id: \.self) { $car in
                ListCell(car: car)
            }
       }
    }
}
.
.Code language: Swift (swift)

With the change made to the view, use the preview canvas to verify that the list populates with content, as shown in Figure 31-2:

Figure 31-2

Designing the Detail View

When a user taps a row in the list, a detail screen will appear showing additional information about the selected car. The layout for this screen will be declared in a separate SwiftUI View file which now needs to be added to the project. Use the File -> New -> File… menu option once again, this time selecting the SwiftUI View template option and naming the file CarDetailView.

When the user navigates to this view from within the List, it must be passed the Car instance for the selected car so that the correct details are displayed. Begin by adding a property to the structure and configuring the preview provider to display the details of the first car in the carData array within the preview canvas as follows:

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

import SwiftUI
 
struct CarDetailView: View {
    
    let selectedCar: Car
    
    var body: some View {
.
.
#Preview {
    CarDetailView(selectedCar: CarStore().cars.first!)
}Code language: Swift (swift)

For this layout, a Form container will be used to organize the views. This is a container view that allows views to be grouped and divided into different sections. The Form also places a line divider between each child view. Within the body of the CarDetailView.swift file, implement the layout as follows:

var body: some View {
    Form {
        Section(header: Text("Car Details")) {
            Image(selectedCar.imageName)
                .resizable()
                .cornerRadius(12.0)
                .aspectRatio(contentMode: .fit)
                .padding()
                
            Text(selectedCar.name)
                .font(.headline)
        
            Text(selectedCar.desc)
                .font(.body)
    
            HStack {
                Text("Hybrid").font(.headline)
                Spacer()
                Image(systemName: selectedCar.isHybrid ?
                        "checkmark.circle" : "xmark.circle" )
            }
        }
    }
}Code language: Swift (swift)

Note that the Image view is configured to be resizable and scaled to fit the available space while retaining the aspect ratio. Rounded corners are also applied to make the image more visually appealing, and either a circle or checkmark image is displayed in the HStack based on the setting of the isHybrid Boolean setting of the selected car.

When previewed, the screen should match that shown in Figure 31-3:

Figure 31-3

Adding Navigation to the List

The next step in this tutorial is to return to the List view in the ContentView.swift file and implement navigation so that selecting a row displays the detail screen populated with the corresponding car details.

With the ContentView.swift file loaded into the code editor, locate the call to the ListCell subview declaration and embed it within a NavigationLink using the current ForEach counter as the link value:

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

Code language: Swift (swift)

For this navigation link to function, the List view must also be embedded in a NavigationStack as follows:

var body: some View {
    NavigationStack {
        List {
            ForEach($carStore.cars, id: \.self) { $car in
                NavigationLink(value: car) {
                    ListCell(car: car)
                }
            }
        }
    }
}Code language: Swift (swift)

Next, we need to add the navigation destination modifier to the list so that tapping an item navigates to the CarDetailView view containing details of the selected car:

NavigationStack {
    List {        
        ForEach($carStore.cars, id: \.self) { $car in
            NavigationLink(value: car) {
                ListCell(car: car)
            }
        }
    }
    .navigationDestination(for: Car.self) { car in
        CarDetailView(selectedCar: car)
    }
}Code language: Swift (swift)

Test that the navigation works using the preview canvas in Live mode and selecting different rows, confirming each time that the detail view appears containing information matching the selected car model.

Designing the Add Car View

The final view to be added to the project is the screen to be displayed when the user is adding a new car to the list. Add a new SwiftUI View file to the project named AddNewCarView.swift, including some state properties and a declaration for storing a reference to the carStore binding (this reference will be passed to the view from the ContentView when the user taps an Add button). Also, modify the preview provider to pass the carData array into the view for testing purposes:

import SwiftUI
 
struct AddNewCarView: View {
    
    @State var carStore : CarStore  
    @State private var isHybrid = false
    @State private var name: String = ""
    @State private var description: String = ""
.
.
#Preview {
    AddNewCarView(carStore: CarStore())
}Code language: Swift (swift)

Next, add a new subview to the declaration that can be used to display a Text and TextField view pair into which the user will enter details of the new car. This subview will be passed a String value for the text to appear on the Text view and a state property binding into which the user’s input is to be stored. As outlined in the chapter entitled SwiftUI State Properties, Observable, State and Environment Objects, a property must be declared using the @Binding property wrapper if the view is being passed a state property. Remaining in the AddNewCarView.swift file, implement this subview as follows:

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

struct DataInput: View {
    
    var title: String
    @Binding var userInput: String
    
    var body: some View {
        VStack(alignment: HorizontalAlignment.leading) {
            Text(title)
                .font(.headline)
            TextField("Enter \(title)", text: $userInput)
                        .textFieldStyle(RoundedBorderTextFieldStyle())
        }
        .padding()
    }
}Code language: Swift (swift)

With the subview added, declare the user interface layout for the main view as follows:

var body: some View {
    
    Form {
        Section(header: Text("Car Details")) {
            Image(systemName: "car.fill")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .padding()
            
            DataInput(title: "Model", userInput: $name)
            DataInput(title: "Description", userInput: $description)
 
            Toggle(isOn: $isHybrid) {
                Text("Hybrid").font(.headline)
            }.padding()        
        }
        
        Button(action: addNewCar) {
                Text("Add Car")
            }
        }
}Code language: Swift (swift)

Note that two instances of the DataInput subview are included in the layout with an Image view, a Toggle, and a Button. The Button view is configured to call an action method named addNewCar when clicked. Within the body of the ContentView declaration, add this function now so that it reads as follows:

.
.
            Button(action: addNewCar) {
                Text("Add Car")
                }
            }
    }
    
    func addNewCar() {
        let newCar = Car(id: UUID(),
                      name: name, desc: description,
                      isHybrid: isHybrid, imageName: "tesla_model_3" )
        
        carStore.cars.append(newCar)
    }
}Code language: Swift (swift)

The new car function creates a new Car instance using the Swift UUID() method to generate a unique identifier for the entry and the content entered by the user. For simplicity, rather than add code to select a photo from the photo library, the function reuses the tesla_model_3 image for new car entries. Finally, the new Car instance is appended to the carStore car array.

When rendered in the preview canvas, the AddNewCarView view should match Figure 31-4 below:

Figure 31-4

With this view completed, the next step is to modify the ContentView layout to include Add and Edit buttons.

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

Implementing Add and Edit Buttons

The Add and Edit buttons will be added to a navigation bar applied to the List view in the ContentView layout. The Navigation bar will also be used to display a title at the top of the list. These changes require the use of the navigationTitle() and toolbar() modifiers as follows:

var body: some View {
    
    NavigationStack {
        List {
            
            ForEach($carStore.cars, id: \.self) { $car in
                NavigationLink(value: car) {
                    ListCell(car: car)
                }
            }
            .onDelete(perform: deleteItems)
            .onMove(perform: moveItems)
        }
        .navigationDestination(for: Car.self) { car in
            CarDetailView(selectedCar: car)
        }
        .navigationTitle(Text("EV Cars"))
        .toolbar {
                ToolbarItem(placement: .navigationBarLeading) {
                    NavigationLink(value: "Add Car") { Text("Add") }
                }
                ToolbarItem(placement: .navigationBarTrailing) {
                    EditButton()
                }
            }
    }
}Code language: Swift (swift)

The Add button is configured to appear at the leading edge of the navigation bar and is implemented as a NavigationLink using a string value that reads “Add Car”. We now need to add a second navigation destination modifier for this string-based link that displays the AddNewCarView view:

NavigationStack {
    List {
        ForEach($carStore.cars, id: \.self) { $car in
            NavigationLink(value: car) {
                ListCell(car: car)
            }
        }
    }
    .navigationDestination(for: Car.self) { car in
        CarDetailView(selectedCar: car)
    }
    .navigationDestination(for: String.self) { _ in
        AddNewCarView(carStore: carStore)
    }
    .navigationTitle(Text("EV Cars"))
    .toolbar {
         ToolbarItem(placement: .navigationBarLeading) {
            NavigationLink(value: "Add Car") { Text("Add") }
         }
.
.Code language: Swift (swift)

The Edit button, on the other hand, is positioned on the trailing edge of the navigation bar and is configured to display the built-in EditButton view. A preview of the modified layout at this point should match the following figure:

Figure 31-5

Using Live Preview mode, test that the Add button displays the new car screen and that entering new car details and clicking the Add Car button causes the new entry to appear in the list after tapping the back button to return to the content view screen. Ideally, the Add Car button should automatically return us to the content view without using the back button. To implement this, we will need to use a navigation path.

Adding a Navigation Path

Begin by editing the ContentView.swift file, adding a NavigationPath declaration, and passing it through to the NavigationStack:

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

struct ContentView: View {
    
    @State var carStore: CarStore = CarStore()
    @State private var stackPath = NavigationPath()
    
    var body: some View {
        NavigationStack(path: $stackPath) {
.
.Code language: Swift (swift)

Having created a navigation path, we need to pass it to the AddNewCarView view so that it can be used to return to the content view within the addNewCar() function. Edit the string-based navigationDestination() modifier to pass the path binding to the view:

.navigationDestination(for: String.self) { _ in
    AddNewCarView(carStore: carStore: carStore, path: $stackPath)
}Code language: Swift (swift)

Edit the AddNewCarView.swift file to add the path binding parameter as follows:

struct AddNewCarView: View {
    
    @State var carStore : CarStore
    @Binding var path: NavigationPath
.
.Code language: Swift (swift)

We also need to comment out the preview provider since the view is now expecting to be passed a navigation view to which we do not have access within the live preview:

/*
#Preview {
    AddNewCarView(carStore: CarStore())
}
*/Code language: Swift (swift)

The last task in this phase of the tutorial is to call removeLast() on the navigation path to pop the current view off the navigation stack and return to the content view:

func addNewCar() {
    let newCar = Car(id: UUID(),
                  name: name, desc: description,
                  isHybrid: isHybrid, imageName: "tesla_model_3" )
    
    carStore.cars.append(newCar)
    path.removeLast()
}Code language: Swift (swift)

Build and run the app on a device or simulator and test that the Add Car button returns to the content view when clicked.

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

Adding the Edit Button Methods

The final task in this tutorial is to add some action methods to be used by the EditButton view added to the navigation bar in the previous section. Because these actions are to be available for every row in the list, the actions must be applied to the list cells in the ContentView.swift file as follows:

var body: some View {
    
    NavigationStack(path: $stackPath) {
        List {
            
            ForEach($carStore.cars, id: \.self) { $car in
                NavigationLink(value: car) {
                    ListCell(car: car)
                }
            }
            .onDelete(perform: deleteItems)
            .onMove(perform: moveItems)
        }
        .navigationDestination(for: Car.self) { car in
            CarDetailView(selectedCar: car)
.
.Code language: Swift (swift)

Next, add the deleteItems() and moveItems() functions within the scope of the body declaration:

.
.
        .navigationTitle(Text("EV Cars"))
.
.
        }
    }
    
    func deleteItems(at offsets: IndexSet) {
        carStore.cars.remove(atOffsets: offsets)
    }
    
    func moveItems(from source: IndexSet, to destination: Int) {
        carStore.cars.move(fromOffsets: source, toOffset: destination)
    }
}Code language: Swift (swift)

In the case of the deleteItems() function, the offsets of the selected rows are provided and used to remove the corresponding elements from the car store array. The moveItems() function, on the other hand, is called when the user moves rows to a different location within the list. This function is passed source and destination values which are used to match the row position in the array.

Using Live Preview, click the Edit button and verify that it is possible to delete rows by tapping the red delete icon next to a row and to move rows by clicking and dragging on the three horizontal lines at the far-right edge of a row. In each case, the list contents should update to reflect the changes:

Figure 31-6

Summary

The main objective of this chapter has been to provide a practical example of using lists, navigation views, and navigation links within a SwiftUI project. This included the implementation of dynamic lists and list editing features. The chapter also reinforced topics covered in previous chapters, including using observable objects, state properties, and property bindings. The chapter also introduced additional SwiftUI features, including the Form container view, navigation bar items, and the TextField view.

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 


Categories