SwiftUI Cookbook

2 (2 reviews total)
By Giordano Scalzo , Edgar Nzokwe
    Advance your knowledge in tech with a Packt subscription

  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Chapter 2: Going Beyond the Single Component with Lists and Scroll Views

About this book

SwiftUI is an innovative and simple way to build beautiful user interfaces (UIs) for all Apple platforms, right from iOS and macOS through to watchOS and tvOS, using the Swift programming language. In this recipe-based book, you’ll work with SwiftUI and explore a range of essential techniques and concepts that will help you through the development process. The recipes cover the foundations of SwiftUI as well as the new SwiftUI 2.0 features introduced in iOS 14. Other recipes will help you to make some of the new SwiftUI 2.0 components backward-compatible with iOS 13, such as the Map View or the Sign in with Apple View.

The cookbook begins by explaining how to use basic SwiftUI components. Then, you’ll learn the core concepts of UI development such as Views, Controls, Lists, and ScrollViews using practical implementation in Swift. By learning drawings, built-in shapes, and adding animations and transitions, you’ll discover how to add useful features to the SwiftUI. When you’re ready, you’ll understand how to integrate SwiftUI with exciting new components in the Apple development ecosystem, such as Combine for managing events and Core Data for managing app data. Finally, you’ll write iOS, macOS, and watchOS apps while sharing the same SwiftUI codebase.

By the end of this SwiftUI book, you'll have discovered a range of simple, direct solutions to common problems found in building SwiftUI apps.

Publication date:
October 2020
Publisher
Packt
Pages
614
ISBN
9781838981860

 

Chapter 2: Going Beyond the Single Component with Lists and Scroll Views

In this chapter, we'll learn how to display lists in SwiftUI. List views are similar to UITableViews in UIKit but are significantly simpler to use. No storyboards or prototype cells are required, and we do not need to know how many rows there are. SwiftUI's lists are designed to be modular so that you can build bigger things from smaller components.

We'll also look at lazy stacks and lazy grids, which are used to optimize the display of large amounts of data by loading only the subset of the content that is currently being displayed or is about to be displayed. Lastly, we'll take a look at how to present hierarchical data in an expanding list with sections that can be expanded or collapsed.

By the end of this chapter, you will understand how to display lists of static or dynamic items, add or remove rows from lists, edit lists, add sections to list views, and much more.

In this chapter, we will cover the following recipes:

  • Using scroll views
  • Creating a list of static items
  • Using custom rows in a list
  • Adding rows to a list
  • Deleting rows from a list
  • Editing a list
  • Moving rows in a list
  • Adding sections to a list
  • Using LazyHStack and LazyVStack (iOS 14+)
  • Using LazyHGrid and LazyVGrid (iOS 14+)
  • Using ScrollViewReader (iOS 14+)
  • Using expanding lists (iOS 14+)
  • Using DisclosureGroup to hide and show content (iOS 14+)
 

Technical requirements

The code in this chapter is based on Xcode 12 and iOS 13. However, some recipes are only compatible with iOS 14 and higher.

You can find the code in the book's GitHub repository at https://github.com/PacktPublishing/SwiftUI-Cookbook/tree/master/Chapter02%20-%20Lists%20and%20ScrollViews.

 

Using scroll views

SwiftUI scroll views are used to easily create scrolling containers. They automatically size themselves to the area where they are placed. Scroll views are vertical by default and can be made to scroll horizontally or vertically by passing in the .horizontal() or .vertical() modifiers as the first parameter to the scroll view.

Getting ready

Let's start by creating a SwiftUI project called ScrollViewApp.

Optional: Download the San Francisco (SF) Symbols app here: https://developer.apple.com/sf-symbols/.

SF Symbols is a set of over 2,400 symbols provided by Apple. They follow Apple's San Francisco system font and automatically ensure optical vertical alignment for different sizes and weights.

How to do it…

We will add two scroll views to a VStack component: one horizontal and one vertical. Each scroll view will contain SF symbols for the letters A–L:

  1. Between the ContentView struct declaration and its body, create an array of SF symbol names called imageNames. Add the strings for SF symbols A–L:
    let imageNames = [
            "a.circle.fill",
            "b.circle.fill",
            "c.circle.fill",
            "d.circle.fill",
            "e.circle.fill",
            "f.circle.fill",
            "g.circle.fill",
            "h.circle.fill",
            "i.circle.fill",
            "j.circle.fill",
            "k.circle.fill",
            "l.circle.fill",
        ]
  2. Add a VStack component and scroll views to the app:
        var body: some View {
            VStack{
                ScrollView {
                        ForEach(self.imageNames, id: \.self)                    { name in
                            Image(systemName: name)
                                .font(.largeTitle)
                                .foregroundColor(Color.                                yellow)
                                .frame(width: 50, height: 50)
                                .background(Color.blue)
                        }
                }
                .frame(width:50, height:200)
                
                ScrollView(.horizontal, showsIndicators:               false) {
                    HStack{
                        ForEach(self.imageNames, id: \.self)                    { name in
                            Image(systemName: name)
                                .font(.largeTitle)
                                .foregroundColor(Color.                               yellow)
                                .frame(width: 50, height: 50)
                                .background(Color.blue)
                        }
                    }
                }
            }
        }

    The result is a view with a vertical and horizontal scroll view:

Figure 2.1 – App with horizontal and vertical scroll views

Figure 2.1 – App with horizontal and vertical scroll views

How it works…

VStack allows us to display multiple scroll views within the ContentView struct's body.

By default, scroll views display items vertically. The first ScrollView component in VStack displays items in a vertical way, even though no axis was specified.

Within the first ScrollView component, a ForEach loop is used to iterate over a static array and display the contents. In this case, the ForEach loop takes two parameters: the array we are iterating over and an identifier, id: \.self, used to distinguish between the items being displayed. The id parameter would not be required if the collection used conformed to the Identifiable protocol.

Two parameters are passed to the second ScrollView component: the axis and showIndicators (ScrollView(.horizontal, showsIndicators: false). The .horizontal axis parameter causes content to be horizontal, and showIndictors:false prevents the scrollbar indicator from appearing in the view.

See also

Refer to the Apple ScrollView documentation at https://developer.apple.com/documentation/swiftui/scrollview.

 

Creating a list of static items

Lists are similar to scroll views in that they are used to view a collection of items. Lists are used for larger datasets, whereas scroll views are used for smaller datasets; the reason being that list views do not load the whole dataset in memory at once and thus are more efficient at handling large data.

Getting ready

Let's start by creating a new SwiftUI app called StaticList.

How to do it…

We'll create a struct to hold weather information and an array of weather data. The data will then be used to create a view that provides weather information from several cities. Proceed as follows:

  1. Before the ContentView struct, create a struct called WeatherInfo with four properties: id, image, temp, and city:
    	struct WeatherInfo: Identifiable {
                var id = UUID()
                var image: String
                var temp: Int
                var city: String
    }
  2. Within the ContentView struct, create a weatherData property as an array of WeatherInfo:
        let weatherData: [WeatherInfo] = [
        WeatherInfo(image: "snow", temp: 5, city:"New York"),
        WeatherInfo(image: "cloud", temp:5, city:"Kansas       City"),
        WeatherInfo(image: "sun.max", temp: 80, city:"San       Francisco"),
        WeatherInfo(image: "snow", temp: 5, city:"Chicago"),
        WeatherInfo(image: "cloud.rain", temp: 49,      city:"Washington DC"),
        WeatherInfo(image: "cloud.heavyrain", temp: 60,       city:"Seattle"),
        WeatherInfo(image: "sun.min", temp: 75,       city:"Baltimore"),
        WeatherInfo(image: "sun.dust", temp: 65,       city:"Austin"),
        WeatherInfo(image: "sunset", temp: 78,       city:"Houston"),
        WeatherInfo(image: "moon", temp: 80, city:"Boston"),
        WeatherInfo(image: "moon.circle", temp: 45,      city:"denver"),
        WeatherInfo(image: "cloud.snow", temp: 8,      city:"Philadelphia"),
        WeatherInfo(image: "cloud.hail", temp: 5,       city:"Memphis"),
        WeatherInfo(image: "cloud.sleet", temp:5,       city:"Nashville"),
        WeatherInfo(image: "sun.max", temp: 80,       city:"San Francisco"),
        WeatherInfo(image: "cloud.sun", temp: 5,       city:"Atlanta"),
        WeatherInfo(image: "wind", temp: 88,       city:"Las Vegas"),
        WeatherInfo(image: "cloud.rain", temp: 60,       city:"Phoenix"),
        ]
  3. Add the List view to the ContentView body. The list should display the information contained in our weatherData array. Also add the font(size: 25) and padding() modifiers:
    List {
                ForEach(self.weatherData){ weather in
                    HStack {
                        Image(systemName: weather.image)
                            .frame(width: 50, alignment:                                .leading)
                        Text("\(weather.temp)°F")
                            .frame(width: 80, alignment:                                .leading)
                        Text(weather.city)
                    }
                    .font(.system(size: 25))
                    .padding()
                }
            }

    The resulting preview should be as follows:

Figure 2.2 – List in the weather app

Figure 2.2 – List in the weather app

How it works…

We started the recipe by creating a WeatherInfo struct to hold and model our weather data. The id = UUID() property creates a unique identifier for each variable we create from the struct. Adding the id property to our struct allows us to later use it within a ForEach loop without providing id: .\self parameter as seen in the previous recipe.

The ForEach loop within the List view iterates through the items in our model data and copies one item at a time to the weather variable declared on the same line.

The ForEach loop contains HStack because we would like to display multiple items on the same line – in this case, all the information contained in the weather variable.

The font(.system(size: 25)) and padding() modifiers add some padding to the list rows to improve the design and readability of the information.

The images displayed are based on SF Symbols, explained in Chapter 1, Using the Basic SwiftUI Views and Controls.

 

Using custom rows in a list

The number of lines of code required to display items in a list view row could vary from one to several lines of code. A custom list row is used when working with several lines of code within a list view row. Implementing custom lists improves modularity and readability, and allows code reuse.

Getting ready

Let's start by creating a new SwiftUI app called CustomRows.

How to do it…

We will reuse part of the code in the static list and clean it up to make it more modular. We create a separate file to hold the WeatherInfo struct, a separate SwiftUI file for the custom WeatherRow, and finally, we implement the components in the ContentView.swift file. The steps are as follows:

  1. Create a new Swift file called WeatherInfo by selecting File | New | File | Swift File or by pressing the ⌘ + N keys.
  2. Define the WeatherInfo struct within the WeatherInfo.swift file:
    struct WeatherInfo: Identifiable {
        var id = UUID()
        var image: String
        var temp: Int
        var city: String
    }
  3. Create a weatherData variable that holds an array of WeatherInfo:
    let weatherData: [WeatherInfo] = [
         WeatherInfo(image: "snow", temp: 5,        city:"New York"),
         WeatherInfo(image: "cloud", temp:5,        city:"Kansas City"),
         WeatherInfo(image: "sun.max", temp: 80,        city:"San Francisco"),
         WeatherInfo(image: "snow", temp: 5, city:"Chicago"),
         WeatherInfo(image: "cloud.rain", temp: 49,        city:"Washington DC"),
         WeatherInfo(image: "cloud.heavyrain", temp: 60,        city:"Seattle"),
         WeatherInfo(image: "sun.min", temp: 75,       city:"Baltimore"),
         WeatherInfo(image: "sun.dust", temp: 65,       city:"Austin"),
         WeatherInfo(image: "sunset", temp: 78,       city:"Houston"),
         WeatherInfo(image: "moon", temp: 80, city:"Boston"),
         WeatherInfo(image: "moon.circle", temp: 45,         city:"denver"),
         WeatherInfo(image: "cloud.snow", temp: 8,        city:"Philadelphia"),
         WeatherInfo(image: "cloud.hail", temp: 5,       city:"Memphis"),
         WeatherInfo(image: "cloud.sleet", temp:5,       city:"Nashville"),
         WeatherInfo(image: "sun.max", temp: 80,        city:"San Francisco"),
         WeatherInfo(image: "cloud.sun", temp: 5,        city:"Atlanta"),
         WeatherInfo(image: "wind", temp: 88,        city:"Las Vegas"),
         WeatherInfo(image: "cloud.rain", temp: 60,        city:"Phoenix"),
         ]
  4. Create a new SwiftUI file by selecting File | New | File | SwiftUI View or by pressing the + N keys.
  5. Name the file WeatherRow.
  6. Add the following code to design the look and functionality of the weather row:
    struct WeatherRow: View {
        var weather: WeatherInfo
        var body: some View {
            HStack {
                Image(systemName: weather.image)
                    .frame(width: 50, alignment: .leading)
                Text("\(weather.temp)°F")
                    .frame(width: 80, alignment: .leading)
                Text(weather.city)
            }
            .font(.system(size: 25))
            .padding()
        }
    }
  7. Add the following code to the WeatherRow_Previews struct to display information from a sample WeatherInfo struct instance:
    static var previews: some View {
            WeatherRow(weather: WeatherInfo(image: "snow",           temp: 5, city:"New York"))
        }
  8. The resulting WeatherRow.swift canvas preview should look as follows:
    Figure 2.3 – WeatherRow preview

    Figure 2.3 – WeatherRow preview

  9. Open the ContentView.swift file and create a list to display data using the custom WeatherRow component:
    var body: some View {
                List {
                    ForEach(weatherData){ weather in
                        WeatherRow(weather: weather)
                    }
                }
            }

    The resulting canvas preview should look as follows:

Figure 2.4 – CustomRowApp preview

Figure 2.4 – CustomRowApp preview

Run the app live preview and admire the work of your own hands.

How it works…

The WeatherInfo Swift file contains the description of the WeatherInfo struct. Our dataset, an array of WeatherInfo variables, was also declared in the file to make it available to other sections of the project.

The WeatherRow SwiftUI file contains the design we will use for each weather row. The weather property within WeatherRow will hold the WeatherInfo arguments passed to the view. HStack in the body of WeatherRow is used to display data from weather variables since a SwiftUI view can only return one view at a time. When displaying multiple views – an image view and two text views, in this case – the views are all encased in an HStack component. The .frame(width: 50, alignment: .leading) modifier added to the image and first text view sets the width used by the element it modifies to 50 units and the alignment to the .leading parameter.

Finally, the .font(.system(size: 25)) and .padding() modifiers are added to HStack to increase the text and image font sizes and add padding to all sides of WeatherRow.

 

Adding rows to a list

Lists are usually used to add, edit, remove, or display content from an existing dataset. In this section, we will go over the process of adding items to an already existing list.

Getting ready

Let's start by creating a new SwiftUI app called AddRowsToList.

How to do it…

To implement the add functionality, we will enclose the List view in NavigationView, and add a button to navigationBarItems that triggers the add function we will create. The steps are as follows:

  1. Create a state variable in the ContentView struct that holds an array of integers:
    @State var numbers = [1,2,3,4]
  2. Add a NavigationView component containing a List view to the ContentView body:
    NavigationView{
                List{
                    ForEach(self.numbers, id:\.self){                  number in
                        Text("\(number)")
                    }
                }
    }
  3. Add a navigationBarItems modifier to the list closing brace that contains a button that triggers the addItemToRow() function:
    .navigationBarItems(trailing: Button(action: {
                        self.addItemToRow()
                    }){
                        Text("Add")
                    })
  4. Implement the addItemToRow() function, which appends a random integer to the numbers array. Place the function within the ContentView struct, immediately after the body variable's closing brace:
    private func addItemToRow() {
            self.numbers.append(Int.random(in: 0 ..< 100))
        }
  5. For the beauty and aesthetics, add a navigationBarTitle modifier to the end of the list so as to make it display a title at the top of the list:
    .navigationBarTitle("Number List", displayMode: .inline)
  6. The resulting code should be as follows:
    struct ContentView: View {
        @State var numbers = [1,2,3,4]
        var body: some View {
            NavigationView{
                List{
                    ForEach(self.numbers, id:\.self){                  number in
                        Text("\(number)")
                    }
                }.navigationBarTitle("Number List",                displayMode: .inline)
                    .navigationBarItems(trailing:                   Button("Add", action: addItemToRow))
            }
        }
        private func addItemToRow() {
            self.numbers.append(Int.random(in: 0 ..< 100))
        }
    }

    The resulting preview should be as follows:

Figure 2.5 – AddRowToList preview

Figure 2.5 – AddRowToList preview

Run the app live preview and admire the work of your own hands!

How it works…

The array of numbers is declared as a @State variable because we want the view to be refreshed each time the value of the items in the array changes – in this case, each time we add an item to the numbers array.

The .navigationBarTitle("Number List", displayMode: .inline) modifier adds a title to the list using the .inline display mode parameter.

The .navigationBarItems(trailing: Button(…)…) modifier adds a button to the trailing end of the display, which triggers the addItemToRow function when clicked.

The addItemToRow function generates a random number in the range 0–99 and appends it to the numbers array.

 

Deleting rows from a list

In this section, we will display a list of countries and use a swipe motion to delete items from the list one at a time.

Getting ready

Let's start by creating a SwiftUI app called DeleteRowFromList.

How to do it…

We use the List view's .onDelete(perform: …) modifier to implement list deletion. The process is as follows:

  1. Add a state variable to the ContentView struct called countries. The variable should contain an array of countries:
    @State var countries = ["USA", "Canada",  "England","Cameroon", "South Africa", "Mexico" ,     "Japan", "South Korea"]
  2. Create a List view in the ContentView body that uses a ForEach loop to display the contents of the countries array:
    	 List {
              ForEach(countries, id: \.self) { country in
                  Text(country)
               	}
      	}
  3. Add a .onDelete(perform: self.deleteItem) modifier to the ForEach loop.
  4. Implement the deleteItem() function. The function should be placed below the body variable's closing brace:
    	private func deleteItem(at indexSet: IndexSet){
            self.countries.remove(atOffsets: indexSet)
        }
  5. Optionally, enclose the List view in a navigation view and add a .navigationBarTitle("Countries", displayMode: .inline) modifier to the list. The resulting ContentView struct should be as follows:
    struct ContentView: View {
        @State var countries = ["USA", "Canada",     "England","Cameroon", "South Africa", "Mexico" ,        "Japan", "South Korea"]
        var body: some View {
            NavigationView{
                List {
                    ForEach(countries, id: \.self) {                  country in
                        Text(country)
                    }
                    .onDelete(perform: self.deleteItem)
                }
                .navigationBarTitle("Countries",                displayMode: .inline)
            }
        }
        private func deleteItem(at indexSet: IndexSet){
            self.countries.remove(atOffsets: indexSet)
        }

    Run the canvas review by clicking the play button next to the canvas preview. A swipe from right to left on a row causes a Delete button to appear. Click on the button to delete the row:

Figure 2.6 – DeleteRowFromList preview execution

Figure 2.6 – DeleteRowFromList preview execution

Run the app live preview and admire the work of your own hands!

How it works…

Navigation views and list views were discussed earlier. Only the .onDelete(…) modifier is new. The .onDelete(perform: self.deleteItem) modifier triggers the deleteItem() function when the user swipes from right to left.

The deleteItem(at indexSet: IndexSet) function takes a single parameter, IndexSet, which represents the index of the row to be removed/deleted. The .onDelete() modifier automatically knows to pass the IndexSet parameter to deleteItem(…).

There's more…

Deleting an item from a list view can also be performed by embedding the list in a navigation view and adding an EditButton component.

 

Editing a list

Implementing the EditButton component in a list is very similar to implementing the delete button in the previous recipe. An edit button offers the user the option to quickly delete items by clicking a minus sign to the left of each list row.

Getting ready

Let's start by creating a SwiftUI app called EditListApp.

How to do it…

We will reuse some of the code from the previous recipe to complete this project. Take the following steps:

  1. Replace the EditListApp app's ContentView struct with the following content from the DeleteRowFromList app:
    struct ContentView: View {
        @State var countries = ["USA", "Canada",     "England","Cameroon", "South Africa", "Mexico" ,        "Japan", "South Korea"]
        var body: some View {
            NavigationView{
                List {
                    ForEach(countries, id: \.self) {                  country in
                        Text(country)
                    }
                    .onDelete(perform: self.deleteItem)
                }
                .navigationBarTitle("Countries",                displayMode: .inline)
            }
        }
        private func deleteItem(at indexSet: IndexSet){
            self.countries.remove(atOffsets: indexSet)
        }
    }
  2. Add a .navigationBarItems(trailing: EditButton()) modifier to the List view, just below the .navigationBarTitle("Countries", displayMode:.inline) modifier.

    Run the preview and click the Edit button at the top-right corner of the screen. Minus (-) signs enclosed in red circles appear to the left of each row:

Figure 2.7 – EditListApp preview execution

Figure 2.7 – EditListApp preview execution

Run the app live preview and admire the work of your own hands!

How it works…

The .navigationBarItems(trailing: EditButton()) modifier adds an edit button to the right corner of the display that, once clicked, creates the view shown in the preceding screenshot. A click on the delete button executes the .onDelete(perform: self.deleteItem) modifier. The modifier then executes the deleteItem function, which removes/deletes the selected row.

There's more…

To move the edit button to the right of the navigation bar, change the modifier to .navigationBarItems(leading: EditButton()).

 

Moving rows in a list

In this section, we will create an app that implements a list view and allows the user to move/reorganize rows.

Getting ready

Let's start by creating a new SwiftUI app in Xcode called MovingListRows.

How to do it…

To allow users to move rows, we need to add a .onMove(..) modifier to the end of the list view's ForEach loop. We also need to embed the list in a navigation view and add a navigationBarItems modifier that implements an EditButton component. The steps are as follows:

  1. Open ContentView.swift. Within the ContentView struct, add a @State variable in Content called countries that contains an array of countries:
    @State var countries = ["USA", "Canada",   "England","Cameroon", "South Africa", "Mexico" ,    "Japan", "South Korea"]
  2. Replace the Text view in the body with a navigation view:
    NavigationView{
            }
  3. Add a list and a ForEach loop within NavigationView that displays the content of the countries variable:
    List {
         	ForEach(countries, id: \.self) { country in
       		Text(country)
           	}.onMove(perform: moveRow)
          }
  4. Add a .onMove(…) modifier to the ForEach loop that calls the moveRow function:
    	ForEach(countries, id: \.self) { country in
       		Text(country)
           	}.onMove(perform: moveRow)
  5. Add the .navigationBarTitle("Countries", displayMode: .inline) and .navigationBarItems(trailing: EditButton()) modifiers to the end of the List view:
    List {
                    .
    			.
    			.
          }
          .navigationBarTitle("Countries",          isplayMode: .inline)
          .navigationBarItems(trailing: EditButton())
  6. Implement the moveRow function at the end of the body variable's closing brace:
    private func moveRow(source: IndexSet, destination: Int){
            countries.move(fromOffsets: source, toOffset:           destination)
        }
  7. The completed ContentView struct should be as follows:
    struct ContentView: View {
        @State var countries = ["USA", "Canada",      "England","Cameroon", "South Africa", "Mexico" ,         "Japan", "South Korea"]
        var body: some View {
            NavigationView{
                List {
                    ForEach(countries, id: \.self) {                   country in
                        Text(country)
                    }
                    .onMove(perform: moveRow)
                }
                .navigationBarTitle("Countries",                displayMode: .inline)
                .navigationBarItems(trailing: EditButton())
            }
        }
        private func moveRow(source: IndexSet,       destination: Int){
            countries.move(fromOffsets: source,           toOffset: destination)
        }
    }

    Run the application in the canvas, on a simulator, or on a physical device. A click on the Edit button at the top-right corner of the screen displays a hamburger symbol to the right of each row. Click and drag the symbol to move the row on which the country is displayed:

Figure 2.8 – MovingListRows preview when running

Figure 2.8 – MovingListRows preview when running

Run the app live preview and move the rows up or down. Nice work!

How it works…

To move list rows, you need to add the list to a navigation view, add the .onMove(perform:) modifier to the ForEach loop, and add a .navigationBarItems(trailing: EditButton()) modifier to the list.

The moveRow(source: IndexSet, destination: Int) function takes two parameters: the source, an IndexSet argument representing the current index of the item to be moved, and the destination, an integer representing the destination of the row. The .onMove(perform:) modifier automatically passes those arguments to the function.

 

Adding sections to a list

In this section, we will create an app that implements a static list with sections. The app will display a partial list of countries grouped by continent.

Getting ready

Let's start by creating a new SwiftUI app in Xcode and name it ListWithSections.

How to do it…

We will add section views to our list view. We will then add a few countries to each of the sections. Proceed as follows:

  1. (Optional) open the ContentView.swift file and replace the Text view in the body with a navigation view. Wrapping the list in a navigation view allows you to add a title and navigation items to the view:
    NavigationView {
    }
  2. Add a list and section to the navigation view:
    List {
      	Section(header: Text("North America")){
        		Text("USA")
               Text("Canada")
               Text("Mexico")
               Text("Panama")
               Text("Anguilla")
          }
    }
  3. Add a listStyle(..) modifier to the end of the list to change its style from the default plain style to GroupedListStyle():
    List {
      	.
    	.
    	.
     }
    .listStyle(GroupedListStyle())
    .navigationBarTitle("Continents and Countries",   displayMode: .inline)
  4. (Optional) add a navigationBarTitle(..) modifier to the list, just below the list style. Add this section if you included the navigation view in step 1.
  5. Add more sections representing various continents to the navigation view. The resulting ContentView struct should look as follows:
    struct ContentView: View {
        var body: some View {
            NavigationView{
                List {
                    Section(header: Text("North America")){
                        Text("USA")
                        Text("Canada")
                        Text("Mexico")
                        Text("Panama")
                        Text("Anguilla")
                    }
                    Section(header: Text("Africa")){
                        Text("Nigeria")
                        Text("Ghana")
                        Text("Kenya")
                        Text("Senegal")
                    }
                    Section(header: Text("Europe")){
                        Text("Spain")
                        Text("France")
                        Text("Sweden")
                        Text("Finland")
                        Text("UK")
                    }
                }
            .listStyle(GroupedListStyle())
                .navigationBarTitle("Continents and                Countries", displayMode: .inline)
            }
        }
    }

    The canvas preview should now show a list with sections, as follows:

Figure 2.9 – ListWithSections preview

Figure 2.9 – ListWithSections preview

Good work! Now, run the app live preview and admire the work of your own hands.

How it works…

SwiftUI's sections are used to separate items into groups. In this recipe, we used sections to visually group countries into different continents. Sections can be used with a header, as follows:

Section(header: Text("Europe")){
                    Text("Spain")
                    Text("France")
                    Text("Sweden")
                    Text("Finland")
                    Text("UK")
                }

They can also be used without a header:

Section {
               Text("USA")
               Text("Canada")
               Text("Mexico")
               Text("Panama")
               Text("Anguilla")
         }

When using a section embedded in a list, we can add a .listStyle() modifier to change the styling of the whole list.

 

Using LazyHStack and LazyVStack (iOS 14+)

SwiftUI 2.0 introduced the LazyHStack and LazyVStack components. These components are used in a similar way to regular HStack and VStack components but offer the advantage of lazy loading. Lazy components are loaded just before the item becomes visible on the device's screen during a device scroll, therefore reducing latency.

We will create an app that uses LazyHStack and LazyVStack and observe how it works.

Getting ready

Let's create a new SwiftUI app called LazyStacks:

  1. Open Xcode and click Create New Project.
  2. In the Choose template window, select iOS and then App.
  3. Click Next.
  4. Enter LazyStacks in the Product Name field and select SwiftUI App from the Life Cycle field.
  5. Click Next and select a location on your computer where the project should be stored.

How to do it…

We will implement a LazyHStack and LazyVStack view within a single SwiftUI view file by embedding both in a VStack component. The steps are as follows:

  1. Click on the ContentView.swift file to view its content in Xcode's editor pane.
  2. Let's create a ListRow SwiftUI view that will have two properties: an ID and a type. ListRow should also print a statement showing what item is currently being initialized:
    struct ListRow: View {
        let id: Int
        let type: String
        init(id: Int, type: String){
            print("Loading \(type) item \(id)")
            self.id = id
            self.type = type
        }
        var body: some View {
            Text("\(type) \(id)").padding()
        }
    }
  3. Replace the initial Text view with VStack:
    VStack {
    }
  4. Add a horizontal scroll view inside the VStack component and use a .frame() modifier to limit the view's height:
    ScrollView(.horizontal){
    }.frame(height: 100, alignment: .center)
  5. Add LazyHStack inside the scroll view with a ForEach view that iterates through the numbers 110000 and displays them using our ListRow view:
    LazyHStack {
         ForEach(1...10000, id:\.self){ item in
           ListRow(id: item, type: "Horizontal")
               }
                    }
  6. Add a second vertical scroll view to VStack with a LazyVStack struct that loops through numbers 110000:
    ScrollView {
      LazyVStack {
        ForEach(1...10000, id:\.self){ item in
          ListRow(id: item, type: "Vertical")
                 }
            }
        }
  7. Now, let's observe lazy loading in action. If the Xcode debug area is not visible, click on View | Debug Area | Show Debug Area:
    Figure 2.10 – Show Debug Area

    Figure 2.10 – Show Debug Area

  8. Select a simulator to use for running the app. The app should look as follows:
    Figure 2.11 – Selecting the simulator from Xcode

    Figure 2.11 – Selecting the simulator from Xcode

  9. Click the play button to run the code in the simulator:
    Figure 2.12 – The LazyStacks app running on the simulator

    Figure 2.12 – The LazyStacks app running on the simulator

  10. Scroll through the items in LazyHStack (located at the top). Observe how the print statements appear in the debug area just before an item is displayed on the screen. Each item is initialized just before it is displayed.
  11. Scroll through the items in LazyVStack. Observe how the print statements appear in the debug area just before an item is displayed.

How it works…

We started this recipe by creating the ListRow view because we wanted to clearly demonstrate the advantage of lazy loading over the regular method where all items get loaded at once. The ListRow view has two properties: an ID and a string. We add a print statement to the init() function so that we can observe when each item gets initialized:

    init(id: Int, type: String){
        print("Loading \(type) item \(id)")
        self.id = id
        self.type = type
    }

The ListRow view body presents a Text view with the ID and type parameters passed to it.

Moving up to the ContentView struct, we replace the initial Text view in the body variable with a VStack component. This allows us to implement both LazyHStack and LazyVStack within the same SwiftUI view.

We implement LazyHStack by first wrapping it in a scroll view, then using a ForEach struct to iterate over the range of values we want to display. For each of those values, a new ListRow view is initialized just before it becomes visible when the user scrolls down:

ScrollView {
  LazyVStack {
   ForEach(1...10000, id:\.self){ item in
    ListRow(id: item, type: "Vertical")
             }
          }
       }

Run the app using a device emulator to view the print statements before each item is initialized. Nothing will be printed if the app is run in live preview mode on Xcode.

There's more…

Try implementing the preceding app using a regular HStack or VStack component and observe the performance difference. The app will be significantly slower since all the rows are initialized at once.

 

Using LazyHGrid and LazyVGrid (iOS 14+)

Just like lazy stacks, lazy grids use lazy loading to display content on the screen. They initialize only the subset of the items that would soon be displayed on the screen as the user scrolls. SwiftUI 2 introduced the LazyVGrid and LazyHGrid views. Let's implement a lazy grid that displays some text in this recipe.

Getting ready

Create a new SwiftUI iOS project and name it UsingLazyGrids.

How to do it…

We'll implement a LazyVGrid and LazyHGrid view inside a Stack view so as to observe both views in action on a single page. The steps are as follows:

  1. Above the ContentView struct's body variable, let's create an array of GridItem columns. The GridItem struct is used to configure the layout of LazyVGrid:
      let columns = [
        GridItem(.adaptive(minimum: 100))
      ]
  2. Create an array of GridItem rows that define the layout that would be used in LazyHGrid:
      let rows = [
           GridItem(.flexible()),
           GridItem(.flexible()),
           GridItem(.flexible())
      ]
  3. Create an array of colors. This will be used for styling some items in our view:
    let colors: [Color] = [.green,.red, .yellow,.blue]
  4. Replace the initial Text view in the body view with a VStack component and a scroll view.
  5. Within the scroll view, add a LazyVGrid view and a ForEach struct that iterates over the numbers 1999 and displays the number in a Text view:
     VStack {
       ScrollView {
        LazyVGrid(columns: columns, spacing:20) {
         ForEach(1...999, id:\.self){ index in
          Text("Item \(index)")
         }
        }
       }
     }
  6. The resulting view displays the numbers in a grid, but the content looks very bland. Let's add some zest by styling the text.
  7. Add a padding() modifier, a background modifier that uses the value of the current index to pick a color from our color array, and the clipshape() modifier to give each Text view a capsule shape:
    …
    ForEach(1...999, id:\.self){ index in
             Text("Item \(index)")
             .padding(EdgeInsets(top: 30, leading: 15,             bottom: 30, trailing: 15))
             .background(colors[index % colors.count])
              .clipShape(Capsule())
              }
    …
  8. Now, let's add a scroll view and LazyHStack. You will notice that everything else is the same as the LazyVGrid view except for the column parameter that's changed to a row parameter:
    VStack {
             …
              ScrollView(.horizontal) {
               LazyHGrid(rows: rows, spacing:20) {
                ForEach(1...999, id:\.self){ index in
                 Text("Item \(index)")
                      .foregroundColor(.white)
                      .padding(EdgeInsets(top: 30, leading:                     15, bottom: 30, trailing: 15))
                      .background(colors[index % colors.                     count])
                      .clipShape(Capsule())
                        }
                    }
                }
            }

    The resulting preview should look as follows:

Figure 2.13 – The UsingLazyStacks app

Figure 2.13 – The UsingLazyStacks app

Run the app in the preview and vertically scroll through LazyVGrid and horizontally scroll through LazyHGrid. Observe how smoothly the scrolling occurs despite the fact that our data source contains close to 2,000 elements. Scrolling stays responsive because all our content is lazily loaded.

How it works…

One of the fundamental concepts of using lazy grids involves understanding how to define the LazyVGrid columns or the LazyHGrid rows. The LazyVGrid column variable was defined as an array containing a single GridItem component but causes three rows to be displayed. GridItem(.adaptive(minimum: 80)) tells SwiftUI to use at least 80 units of width for each item and place as many items as it can along the same row. Thus, the number of items displayed on a row may increase when the device is changed from portrait to landscape orientation, and vice versa.

GridItem(.flexible()), on the other hand, fills up each row as much as possible:

let rows = [
        GridItem(.flexible()),
        GridItem(.flexible()),
        GridItem(.flexible())
    ]

Adding three GridItem(.flexible()) components divides the available space into three equal rows for data display. Remove or add more GridItem(.flexible()) components to the row array and observe how the number of rows decreases or increases.

We provide two parameters to our lazy grids: a columns/rows parameter that specifies the layout of the grid and a spacing parameter that defines the spacing between the grid and the next item in the parent view.

 

Using ScrollViewReader (iOS 14+)

ScrollViewReader can be used to programmatically scroll to a different section of a list that might not be currently visible. In this recipe, we will create an app that displays a list of characters from A to L. The app will also have a button at the top for programmatically scrolling to the last element in the list and a button at the bottom for programmatically scrolling to an element in the middle of the list.

Getting ready

Create a new SwiftUI app using the UIKit App Delegate life cycle:

Figure 2.14 – UIKit App Delegate in Xcode

Figure 2.14 – UIKit App Delegate in Xcode

Name the app UsingScrollViewReader.

How to do it…

We will start by creating an array of structs with a name and an ID. The array will be used to display SF symbols for the characters A–L. We will then proceed to implement ScrollViewReader and programmatically move to the top or the bottom of the list.

The steps are as follows:

  1. Create a struct called ImageStore, just above the ContentView_Previews struct. The struct should implement the Identifiable protocol:
    struct ImageStore: Identifiable {
        var name: String
        var id: Int
    }
  2. Within the ContentView struct, just before the body variable, declare an array called imageNames. Initialize the array with ImageStore structs whose name parameters represent the letters A–Q from SF Symbols:
        let imageNames = [
            ImageStore(name:"a.circle.fill",id:0),
            ImageStore(name:"b.circle.fill",id:1),
            ImageStore(name:"c.circle.fill",id:2),
            ImageStore(name:"d.circle.fill",id:3),
            ImageStore(name:"e.circle.fill",id:4),
            ImageStore(name:"f.circle.fill",id:5),
            ImageStore(name:"g.circle.fill",id:6),
            ImageStore(name:"h.circle.fill",id:7),
            ImageStore(name:"i.circle.fill",id:8),
            ImageStore(name:"j.circle.fill",id:9),
            ImageStore(name:"k.circle.fill",id:10),
            ImageStore(name:"l.circle.fill",id:11),
            ImageStore(name:"m.circle.fill",id:12),
            ImageStore(name:"n.circle.fill",id:13),
            ImageStore(name:"o.circle.fill",id:14),
            ImageStore(name:"p.circle.fill",id:15),
            ImageStore(name:"q.circle.fill",id:16),
        ]
  3. Replace the TextView component in the body variable with a ScrollView component, ScrollViewReader, and a Button component to navigate to the letter Q in the list:
    ScrollView {
         ScrollViewReader { value in
         Button("Go to letter Q") {
         value.scrollTo(16)
               }
            }         
        }
  4. Now use a ForEach struct to iterate over the imageNames array and display its content using the SwiftUI's Image struct:
     ForEach(imageNames){ image in
       Image(systemName: image.name)
         .id(image.id)
         .font(.largeTitle)
         .foregroundColor(Color.yellow)
         .frame(width: 90, height: 90)
         .background(Color.blue)
         .padding()
       }
  5. Now, add another button at the bottom that causes the list to scroll back to the top:
    Button("Go back to A") {
            value.scrollTo(0)
       }
  6. The app should be able to run now, but let's add some padding and background to the bottom and top buttons to improve their appearance:
    Button("Go to letter Q") {
           value.scrollTo(0)
          }
          .padding()
          .background(Color.yellow)
    	Button("Go to G") {
            value.scrollTo(6, anchor: .bottom)
          }
           .padding()
           .background(Color.yellow)

    The resulting app preview should look as follows:

Figure 2.15 – The UsingScrollViewReader app

Figure 2.15 – The UsingScrollViewReader app

Run the app in Xcode live preview and tap the button at the top to programmatically scroll down to the letter Q.

Scroll to the bottom of the view and tap the button to scroll up to the view where the letter G is at the bottom of the visible area.

How it works…

We start this recipe by creating the ImageStore struct that defines the properties of each image we want to display:

struct ImageStore: Identifiable {
    var name: String
    var id: Int
}

The id parameter is required for ScrollViewReader as a reference for the location to scroll to, just like a house address provides the final destination for driving. By making the struct extend the Identifiable protocol, we are able to iterate over the imageNames array without specifying an id parameter to the ForEach struct:

ForEach(imageNames){ image in
                    Image(systemName: image.name)
				…
                }

ScrollViewReader should be embedded inside a scroll view. This provides a scrollTo() method that can be used to programmatically scroll to the item whose index is specified in the method:

ScrollViewReader { value in
                Button("Go to letter Q") {
                    value.scrollTo(16)
                }
			…
}

The scrollTo() method also has an anchor parameter that is used to specify the position of the item we are scrolling to; for example, scrollTo(6, anchor: .top), in this case, causes the app to scroll until the ImageStore item with ID 6 at the bottom of the view.

 

Using expanding lists (iOS 14+)

Expanding lists can be used to display hierarchical structures within a list. Each list item can be expanded to view its contents. The expanding ability is achieved by creating a struct that holds some information and an optional array of items of the same type as the struct itself. Let's examine how expanding lists work by creating an app that displays the contents of a backpack.

Getting ready

Create a new SwiftUI app and name it UsingExpandingLists.

How to do it…

We start by creating the Backpack struct that describes the properties of the data we want to display. We'll then create a number of Backpack constants and display the hierarchical information using a List view containing a children property.

The steps are as follows:

  1. At the bottom of the ContentView.swift file, define a Backpack struct that has an id, name, icon, and content property. The content property's type should be an optional array of Backpack structs:
    struct Backpack: Identifiable {
        let id = UUID()
        let name: String
        let icon: String
        var content: [Backpack]?
    }
  2. Below the Backpack struct declaration, create three variables: two currency types and an array of currencies:
    let dollar = Backpack(name: "Dollar", icon: "dollarsign.   circle")
    let yen = Backpack(name: "Yen",icon: "yensign.circle")
    let currencies = Backpack(name: "Currencies", icon:    "coloncurrencysign.circle", content: [dollar, yen])
  3. Create a pencil, hammer, paperclip, and glass constant:
    let pencil = Backpack(name: "Pencil",icon: "pencil.  circle")
    let hammer = Backpack(name: "Hammer",icon: "hammer")
    let paperClip = Backpack(name: "Paperclip",icon:   "paperclip")
    let glass = Backpack(name: "Magnifying glass",   icon: "magnifyingglass")
  4. Create a bin Backpack constant that contains paperclip and glass, as well as a tool constant that holds pencil, hammer, and bin:
    let bin  = Backpack(name: "Bin", icon: "arrow.up.bin",  content: [paperClip, glass])
    let tools = Backpack(name: "Tools", icon: "folder",   content: [pencil, hammer,bin])
  5. Going back to the ContentView struct, add an instance property; items, which contains an array of two items, the currencies and tools constants defined earlier:
    struct ContentView: View {
        let items = [currencies,tools]
        …
    }
  6. Within the body variable, replace the Text view with a List view that displays the content of the items array:
        var body: some View {
            List(items, children: \.content){ row in
                Image(systemName: row.icon)
                Text(row.name)
            }
        }

    The resulting Xcode live preview should look as follows when expanded:

Figure 2.16 – UsingExpandingLists

Figure 2.16 – Using expanding lists

Run the Xcode live preview and click on the arrows to expand and collapse the views.

How it works…

We start by creating a struct of the proper format for expanding lists. Expanding lists require one of the struct properties to be an optional array of the same type as the struct itself. Our Backpack content property holds an optional array of Backpack items:

struct Backpack: Identifiable {
    let id = UUID()
    let name: String
    let icon: String
    var content: [Backpack]?
}

The UUID() function generates a random identifier and stores it in the id property. We can manually provide the name, icon, and content variables for the items we add to the backpack.

We create our first hierarchical structure by creating the Dollar and Yen variables:

let dollar = Backpack(name: "Dollar", icon: "dollarsign.  circle")
let yen = Backpack(name: "Yen",icon: "yensign.circle")

Observe that no content property has been provided for both variables. This is possible because the content is an optional parameter.

We then create a currencies constant that that has name and icon. The Dollar and Yen variables created earlier are added to its content array.

We use a similar composition to add elements to our tool constant.

After setting up the data we want to display, we create an item property in our ContentView struct that holds an array of Backpack constants created earlier:

let items = [currencies,tools]

Finally, we replace the Text view in the ContentView body with a List view that iterates over the items. The list is made expandable by adding the children parameter, which expects an array of the same time as the struct passed to the List view.

There's more…

The tree structure used for expandable lists can also be generated on a single line, as follows:

let tools = Backpack(name: "Tools", icon: "folder", content:
   [Backpack(name: "Pencil",icon: "pencil.circle"),
    Backpack(name: "Hammer",icon: "hammer"),
    Backpack(name: "Bin", icon: "arrow.up.bin", content:
        [Backpack(name: "Paperclip",icon: "paperclip"),
         Backpack(name: "Magnifying glass", icon:           "magnifyingglass")
           ])
       ])

The SwiftUI List view provides out-of-the-box support for OutlineGroup in iOS 14. OutlineGroup computes views and disclosure groups on demand from an underlying collection.

See also

Refer to the WWDC20 video on stacks, grids, and outlines at https://developer.apple.com/videos/play/wwdc2020/10031/.

Refer to the Apple documentation on OutlineGroup at https://developer.apple.com/documentation/swiftui/outlinegroup.

 

Using disclosure groups to hide and show content (iOS 14+)

DisclosureGroup is a view used to show or hide content based on the state of a disclosure control. It takes two parameters: a label to identify its content and a binding to control whether its content is shown or hidden. Let's take a closer look at how it works by creating an app that shows and hides content in a disclosure group.

Getting ready

Create a new SwiftUI project and name it UsingDisclosureGroup.

How to do it…

We will create an app the uses DisclosureGroup views to reveal some planets in our solar system, continents on the Earth, and some surprise text. The steps are as follows:

  1. Below the ContentView struct, add a state property called showplanets:
    @State private var showplanets = true
  2. Replace the Text view in the body variable with a DisclosureGroup view that displays some planets:
     DisclosureGroup("Planets", isExpanded: $showplanets){
         Text("Mercury")
         Text("Venus") 
    }
  3. Review the result in Xcode's live preview, then nest another DisclosureGroup view for planet Earth. The group should contain the list of Earth's continents:
      DisclosureGroup("Planets", isExpanded: $showplanets){
    	…
           DisclosureGroup("Earth"){
                    Text("North America")
                    Text("South America")
                    Text("Europe")
                    Text("Africa")
                    Text("Asia")
                    Text("Antarctica")
                    Text("Oceania")
                }
            }
  4. Let's add another DisclosureGroup view using a different way of defining them. Since the body variable can't display two views, let's embed our parent DisclosureGroup view in a VStack component. Hold down the key and click on the parent DisclosureGroup view and select Embed in VStack from the list of actions.
  5. Below our parent DisclosureGroup view, add another one that reveals surprise text when clicked:
    VStack {
        DisclosureGroup("Planets", isExpanded: $showplanets){
             Text("Mercury")
             Text("Venus")
                    
             DisclosureGroup("Earth"){
                Text("North America")
                Text("South America")
                Text("Europe")
                Text("Africa")
                Text("Asia")
                Text("Antarctica")
                Text("Oceania")
             }
          }
          DisclosureGroup{
            Text("Surprise! This is an alternative                  way of using DisclosureGroup")
          } label : {
              Label("Tap to reveal", systemImage: "cube.box")
                    .font(.system(size:25, design: .rounded))
                    .foregroundColor(.blue)
                }
          }

    The resulting preview should be as follows:

Figure 2.17 – The UsingDisclosureGroup app

Figure 2.17 – The UsingDisclosureGroup app

Run the Xcode live preview and click on the arrows to expand and collapse the views.

How it works…

Our initial DisclosureGroup view presents a list of planets. By passing in a binding, we are able to read the state change and know whether the DisclosureGroup view is in an open or closed state:

DisclosureGroup("Planets", isExpanded: $showplanets){
     Text("Mercury")
     Text("Venus") 
} 

We can also use DisclosureGroup views without bindings, depending solely on the default UI:

DisclosureGroup("Earth"){
    Text("North America")
    Text("South America")
     …
 }

Lastly, we used the new Swift 5.3 closure syntax to separate the Text and Label portions into two separate views. This allows more customization of the Label portion:

DisclosureGroup{
  Text("Surprise! This is an alternative way of using      DisclosureGroup")
  } label : {
      Label("Tap to reveal", systemImage: "cube.box")
           .font(.system(size:25, design: .rounded))
           .foregroundColor(.blue)
  }

DisclosureGroup views are very versatile as they can be nested and used to display content in a hierarchical way.

About the Authors

  • Giordano Scalzo

    Giordano Scalzo is a developer with 20 years of programming experience, since the days of ZX Spectrum. He has worked in Swift, Objective-C, C/C++, Java, .Net, Ruby, Python, and in a ton of other languages that he has forgotten the names of. After years of backend development, over the past 10 years, Giordano has developed extensively for iOS, releasing more than 20 apps—apps that he wrote for clients, enterprise applications, or on his own. He is currently a Tech Lead Consultant in London, where he leads mobile digital transformations through his company, Effective Code Ltd.

    Browse publications by this author
  • Edgar Nzokwe

    Edgar Nzokwe is a software engineer with 6+ years of experience building web and mobile applications. His areas of expertise include SwiftUI, UIKit, and Kotlin, Python, and a few Javascript frameworks. Edgar is dedicated to advancing the knowledge base of SwiftUI because he believes it empowers developers to design and build dazzling cross-platform apps with minimal development time.

    Browse publications by this author

Latest Reviews

(2 reviews total)
more infomore infomore infomore info
I ordered this book two months ago but Packt has not published it yet.

Recommended For You

Book Title
Unlock this book and the full library for FREE
Start free trial