Complete Guide to Lists in SwiftUI

Afsanafarheen
8 min readMar 10, 2023

--

In SwiftUI, displaying and interacting with data in the form of a list is simple. By using the pre-built List control, creating a list is straightforward. The approach you choose for creating the list depends on the nature of your data, as we’ll discuss further in this post. The key takeaway is that lists offer a convenient way to present data in your view.

We are going to create a Grocery listing application to understand about List and its usage.

Create a new project in xcode, select “SwiftUI” as interface and name the application as “GroceryList”.

STATIC LIST:

Static Lists are lists that are made with predefined SwiftUI controls. You can use views such as shapes, images, sliders, and etc. When implementing a static list, you must add every row manually into the list.

var body: some View {
List {
Text("A List Item")
Text("A Second List Item")
Text("A Third List Item")
}
}

Output of the above code should look like this :

Dynamic List:

Dynamic Lists consist of more complex data.It takes in same type of data and can be more efficient since it iterates through its values.

struct DynamicList: View {
var body: some View {
List(1..<10){
Text("\($0)")
}
}
}

Creating a datasource and constructing the list:

As we now know how static and dynamic list works, lets jump on to our grocery app. First, lets create a struct which will be our datasource.

Create a new swift file and name it as GroceryModel.swift and add the below code.

struct GroceryModel:Identifiable{
let id = UUID()
var categories:[GroceryCategory]
}

enum GroceryCategory:String,CaseIterable{
case fruits = "Fruits"
case vegetables = "Vegetables"
case dairyProducts = "Dairy Products"
case snacks = "Snacks"
case instantFood = "Instant Food"
}

Why Identifiable?
SwiftUI needs to know how it can identify each item uniquely otherwise it will struggle to compare view hierarchies to figure out what has changed.
By confirming to IDENTIFIABLE it is guaranteed to remain unique for the lifetime of an object.

Now, lets create the datasource using the GroceryModel and feed it to the list. Replace the below code in ContentView.swift file.

struct GroceryListView: View {
@State var model:GroceryModel = GroceryModel(categories: GroceryCategory.allCases)
var body: some View {
NavigationView {
List{
ForEach(model.categories,id: \.self){ data in
Text(data.rawValue)
}
}.navigationTitle("Grocery List")
}
}
}

why “\.self?”
In English, that specifies “create a new row for every item in the model items, identified uniquely by its id

Output:

Sections in List:

Creating section is much easier and simpler.

  var body: some View {
NavigationView {
List{
ForEach(model.categories,id: \.self){ data in
Section{
Text(data.rawValue)
}
}
}.navigationTitle("Grocery List")
}
}

The above code adds section after each data. We can also add header and footer to the section. For that, first lets add section name for each grocery category.
Replace ‘GroceryCategory’ enum with the below code.

enum GroceryCategory:String,CaseIterable{
case fruits = "Fruits"
case vegetables = "Vegetables"
case dairyProducts = "Dairy Products"
case snacks = "Snacks"
case instantFood = "Instant Food"

var sectionName:String{
switch self {
case .fruits:
return "Fruits"
case .vegetables:
return "Vegetables"
case .dairyProducts:
return "Dairy Products "
case .snacks:
return "Snacks"
case .instantFood:
return "Instant food"
}
}

}

Great, now it can be easily accessed from the model we have already used in the ‘GroceryListView’.

struct GroceryListView: View {
@State var model:GroceryModel = GroceryModel(categories: GroceryCategory.allCases)
var body: some View {
NavigationView {
List{
ForEach(model.categories,id: \.self){ data in
Section(data.sectionName){
Text(data.rawValue)
}
}
}.navigationTitle("Grocery List")
}
}
}

After adding this code, your output will be like this.

Listing Styles:

There are 6 list styles available which is easier to use and understand.

FORMAT:

List{
...
}
.listStyle(.insetGrouped)
  • DefaultListStyle( ): shows footer
  • GroupedListStyle( ): shows footer
  • InsetGroupedListStyle( ): shows footer
  • PlainListStyle( ): does not shows footer
  • SidebarListStyle( ): does not shows footer
  • InsetListStyle( ): does not shows footer

Lets create an detail page for each grocery category and add listing style for it. We need to add grocery list for each grocery category we have. Replace the ‘GroceryCategory’ enum with the below code.

enum GroceryCategory:String,CaseIterable{
case fruits = "Fruits"
case vegetables = "Vegetables"
case dairyProducts = "Dairy Products"
case snacks = "Snacks"
case instantFood = "Instant Food"

var categoryList:[String]{
switch self {
case .fruits:
return ["Apples","Oranges","Guava","Pineapple","Grapes"]
case .vegetables:
return ["Onion","Tomato","Brinjal","Potato","Beans"]
case .dairyProducts:
return ["Milk","Bread","Paneer","Wheat Bread","Eggs"]
case .snacks:
return ["Biscuits","chips","French fries","Nuggets","Puffs"]
case .instantFood:
return ["Noodles","Maggi","Peanut butter","Pizza","Burger"]
}
}
var sectionName:String{
switch self {
case .fruits:
return "Fruits"
case .vegetables:
return "Vegetables"
case .dairyProducts:
return "Dairy Products "
case .snacks:
return "Snacks"
case .instantFood:
return "Instant food"
}
}

}

Great, now replace the GroceryListView with the below code.

struct GroceryListView: View {
@State var model:GroceryModel = GroceryModel(categories: GroceryCategory.allCases)
var body: some View {
NavigationView {
List{
ForEach(model.categories,id: \.self){ data in
NavigationLink(destination: GroceryDetailView(categoryName: data)){
Section{
Text(data.rawValue)
}
}
}
}.navigationTitle("Grocery List")
}
}
}

We now have to create ‘GroceryDetailView’:

struct GroceryDetailView:View{
@State var categoryName:GroceryCategory
var body: some View {
List{
ForEach(categoryName.categoryList,id: \.self){ data in
Text(data)
}
}
}
}

We are using the categoryList we added earlier to show as the list for each groceryCategory. We are going to add different listingStyle to each list of the ‘GroceryDetailView’. Let’s create a function called ‘groceryListStyle’ with ‘GroceryCategory’ as param and add it as extension to the ‘View’.

extension View {
@ViewBuilder
func groceryListStyle(catName:GroceryCategory) -> some View {
switch catName{
case .fruits:
self.listStyle(.insetGrouped)
case .dairyProducts:
self.listStyle(.automatic)
case .vegetables:
self.listStyle(.grouped)
case .instantFood:
self.listStyle(.sidebar)
case .snacks:
self.listStyle(.inset)
}
}
}

Now, lets add it to the GroceryDetailView’views list. I’ve added Section to ‘instantFood’ GroceryCategory for one of the listStyle.

struct GroceryDetailView:View{
@State var categoryName:GroceryCategory
var body: some View {
List{
ForEach(categoryName.categoryList,id: \.self){ data in
if categoryName == .instantFood{
Section{
Text(data)
}
}else{
Text(data)
}
}
}
.groceryListStyle(catName: categoryName)
}
}

After all the changes we have done till now, the output should be like this:

Creating Hierarchial lists:

We can also create a hierarchical list of arbitrary depth by providing tree-structured data and a children parameter that provides a key path to get the child nodes at any level.

We need create a datasource in which each category will have children and those children will also have children nodes. Lets create a struct called ‘GroceryItem’. In this struct, we are going to have name and children values and each children can either have children nodes or can be empty.

struct GroceryItem: Hashable, Identifiable, CustomStringConvertible {
var id: Self { self }
var name: String
var children: [GroceryItem]? = nil
var description: String {
switch children {
case nil:
return "\(name)"
case .some(let children):
return children.isEmpty ? "\(name)" : "\(name)"
}
}
}

Now, we will create datasource for ‘GroceryItem’.

struct GroceryListView: View {
@State var model:GroceryModel = GroceryModel(categories: GroceryCategory.allCases)
@State var treeModel:[GroceryItem] = [
GroceryItem(name: "Fruits", children:
[GroceryItem(name: "Apple", children:
[GroceryItem(name: "Green apple", children:nil),
GroceryItem(name: "Kashmir apple", children:nil)])
]),
GroceryItem(name: "Vegetables", children:
[GroceryItem(name: "Tomato", children:
[GroceryItem(name: "Red Tomato", children:nil),
GroceryItem(name: "Green Tomato", children:nil)])
]),
]
@State var isHierarchy:Bool = false

var body: some View {
NavigationView {
if isHierarychy{
List(treeModel, children: \.children) { item in
Text(item.description)
}.listRowInsets(EdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10))
}else{
List{
ForEach(model.categories,id: \.self){ data in
if isHierarychy{

}else{
NavigationLink(destination: GroceryDetailView(categoryName: data)){
Section{
Text(data.rawValue)
}
}
}
}
}.navigationTitle("Grocery List")
.toolbar {
Toggle(isOn: $isHierarchy) {
Text("Show as Hierarchy")
}
}
}
}
}
}

Lets break down the code:

  • treeModel: it is the new datasource we have created using ‘GroceryItem’.
  • List(treeModel, children: \.children) : we are going to use List which takes ‘children’ to provide the hierarchial view.
  • .toolbar: it adds toolbar to the list. We are going to have a toggle button to toggle between normal list and hierarchial list.
  • isHierarchy: we are going to use this @state variable store the state of the button and to show the relevant list.

To know more about @State check out my blog Binding in swift .

Replace the above code in ‘GroceryListView’ and our output should be like this:

Adding swipe action:

Adding swipe action to the row is much easier and straightforward. You should use ‘.swipeActions(edge: .trailing)’ where you configure the rows. In our case we will be adding swipe action inside. First lets add the function to which is going to update our model.

func removeVal(cat:GroceryCategory){
self.model.categories.removeAll { xcat in
xcat == cat
}
}

Lets add swipe action to the row and add the above function. Our final code of GroceryListView should be like this.

struct GroceryListView: View {
@State var model:GroceryModel = GroceryModel(categories: GroceryCategory.allCases)
@State var cats:[GroceryItem] = [
GroceryItem(name: "Fruits", children:
[GroceryItem(name: "Apple", children:
[GroceryItem(name: "Green apple", children:nil),
GroceryItem(name: "Kashmir apple", children:nil)])
]),
GroceryItem(name: "Vegetables", children:
[GroceryItem(name: "Tomato", children:
[GroceryItem(name: "Red Tomato", children:nil),
GroceryItem(name: "Green Tomato", children:nil)])
]),
]
@State var isHierarychy:Bool = false

var body: some View {
NavigationView {
if isHierarychy{
List(cats, children: \.children) { item in
Text(item.description)
}.listRowInsets(EdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10))
}else{
List{
ForEach(model.categories,id: \.self){ data in
if isHierarychy{

}else{
NavigationLink(destination: GroceryDetailView(categoryName: data)){
Section{
Text(data.rawValue)
}
}
.swipeActions(edge: .trailing) {
Button {
self.removeVal(cat: data)
} label: {
Label("Delete", systemImage: "trash")
}
.tint(.red)
}
}

}
}.navigationTitle("Grocery List")
.toolbar {
Toggle(isOn: $isHierarychy) {
Text("Show as Hierarychy")
}
}
}
}
}

func removeVal(cat:GroceryCategory){
self.model.categories.removeAll { xcat in
xcat == cat
}
}
}

Output:

Similary, we can move, select, multi-select rows and also we can add more than one button in the swipe action and also specify from which edge the swipe should happen. In our example, we have used ‘.trailing’ edge.

Conclusion:

List in swiftui are a powerful way to display collections of data in a visually appealing and interactive way. SwiftUI provides a lot of built-in functionality and allowing for customization of cell content and appearance.

Overall, the combination of built-in functionality and customisation options make lists in SwiftUI a powerful tool for creating dynamic and engaging interfaces for displaying collections of data.

For more reference, I’ve provided the project here.

Hope this article was helpful 😄.

Follow me on twitter : https://twitter.com/iamafsana_

Sayonara!! 😃

--

--

Afsanafarheen

I'm Afsana Farheen, an experienced iOS developer with a passion for creating innovative and user-friendly mobile applications.