In an earlier post, we went over what a ForEach loop is used for. In this tutorial, we’ll look at the List.
A List is designed to show a collection of views in a table format, much like how you see your emails, contacts, music and so on. This is similar to the UITableView in concept, but how it is implemented is very different.
If you have read the ForEach loop tutorial, the last example showed how to put four capsules or pills horizontally on the screen. Here is the code we used for this:
struct TagListView: View {
let discussionPoints = [
DiscussionPoint(title: "Meeting Agenda", tag: "Important"),
DiscussionPoint(title: "Budget Review", tag: "Finance"),
DiscussionPoint(title: "Project Updates", tag: "Development"),
DiscussionPoint(title: "Team Building", tag: "HR")
]
var body: some View {
HStack {
ForEach(discussionPoints) { point in
Text(point.tag)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(Capsule().fill(Color.blue.opacity(0.2)))
.foregroundColor(.blue)
}
}
}
}
struct DiscussionPoint: Identifiable {
let id = UUID()
let title: String
let tag: String
}
A quick overview if you are new to this. We declared a struct called DiscussionPoint and conformed it to Identifiable. This allows this struct to be used in a ForEach loop because items need to have an ID.
We then initialised the data at the top with the object which had a title and a tag for each object.
In the body, we have a horizontal stack with a ForEach nested within. This iterates over the discussionPoints and the Text view within grabs the tag as it loops around each point.
In my use case, this only forms part of what I need. Lets take a look at how we can include the title and how we can add multiple tags to each discussion point. We will do this with a List.
struct DiscussionPoint: Identifiable {
let id = UUID()
let title: String
let tags: [String]
}
First, adjust DiscussionPoint so that tag becomes tags and we expect an array of String.
let discussionPoints = [
DiscussionPoint(title: "Meeting Agenda", tags: ["Important", "Urgent", "Team"]),
DiscussionPoint(title: "Budget Review", tags: ["Finance", "Annual", "Review"]),
DiscussionPoint(title: "Project Updates", tags: ["Development", "Deadline", "Priority"]),
DiscussionPoint(title: "Team Building", tags: ["HR", "Culture", "Wellness"])
]
Next, fix up discussionPoints so that tag becomes tags and change from a string to an array of strings as seen above.
var body: some View {
NavigationView {
List(discussionPoints) { point in
VStack(alignment: .leading) {
Text(point.title)
.font(.headline)
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(point.tags, id: \.self) { tag in
Text(tag)
.padding(.horizontal, 10)
.padding(.vertical, 3)
.background(Capsule().fill(Color.blue.opacity(0.2)))
.foregroundColor(.blue)
}
}
}
.padding(.top, 4)
}
.padding(.vertical, 8)
}
.navigationTitle("Discussion Points")
}
}
We now nest the ForEach within a List. We have made changes on the ForEach line so that it takes point.tags (point being from the List), and then it iterates over each tag for each discussion point. We added id: \.self back in because, in the ForEach, we are not iterating over the discussionPoints, but instead, we are simply iterating over an array of strings.
We put the ForEach in a horizontal scroll view in case we add more tags that take it off-screen.
We also embed the whole view in a NavigationView to give it a title.
When previewing this, you will now get a table with some horizontal pills under each title within a cell.
By using a combination of List and ForEach, we can create some nice-looking table cells.
Lets take a look at other functionality of the List view. We can do this by adding swipe gestures to each of the rows in List. We do this as follows:
Text(point.title)
.font(.headline)
.swipeActions {
Button(role: .destructive) {
print("Delete \(point)")
} label: {
Label("Delete", systemImage: "trash")
}
Button {
print("Pin \(point)")
} label: {
Label("Pin", systemImage: "pin")
}
.tint(.yellow)
}
In the List view, where we declare the Text view, you can add the swipeActions modifier and add buttons to this. You would typically replace the print(“Delete \(point)”) with a call to a method that would be used to manipulate the object.
If you use this code and modify an object, you will probably run into problems because discussionPoints will need to be declared with a property wrapper so that if you do delete an item, SwiftUI will update the view.
import SwiftUI
class DiscussionPointsViewModel: ObservableObject {
@Published var discussionPoints: [DiscussionPoint]
init() {
self.discussionPoints = [
DiscussionPoint(title: "Meeting Agenda", tags: ["Important", "Urgent", "Team"]),
DiscussionPoint(title: "Budget Review", tags: ["Finance", "Annual", "Review"]),
DiscussionPoint(title: "Project Updates", tags: ["Development", "Deadline", "Priority"]),
DiscussionPoint(title: "Team Building", tags: ["HR", "Culture", "Wellness"])
]
}
func deleteDiscussionPoint(_ point: DiscussionPoint) {
discussionPoints.removeAll { $0.id == point.id }
}
func pinDiscussionPoint(_ point: DiscussionPoint) {
print("Pin \(point.title)")
}
}
struct TagListView: View {
@StateObject private var viewModel = DiscussionPointsViewModel()
var body: some View {
List {
ForEach(viewModel.discussionPoints) { point in
VStack(alignment: .leading) {
Text(point.title)
.font(.headline)
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(point.tags, id: \.self) { tag in
Text(tag)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(Capsule().fill(Color.blue.opacity(0.2)))
.foregroundColor(.blue)
}
}
}
.padding(.top, 4)
}
.swipeActions {
Button(role: .destructive) {
viewModel.deleteDiscussionPoint(point)
} label: {
Label("Delete", systemImage: "trash")
}
Button {
viewModel.pinDiscussionPoint(point)
} label: {
Label("Pin", systemImage: "pin")
}
.tint(.yellow)
}
.padding(.vertical, 8)
}
}
}
}
struct DiscussionPoint: Identifiable {
let id = UUID()
let title: String
let tags: [String]
}
struct TagListView_Previews: PreviewProvider {
static var previews: some View {
TagListView()
}
}
First, we create the DiscussionPointsViewModel class. You will notice that it is an ObservableObject, allowing for changes to be observed so that views can be updated. When a change occurs to the discussionPoints array within, SwiftUI will recognise this and redraw it as needed.
On line 4, we declare discussionPoints as being an array of DiscussionPoint. We also add the @Published property wrapper needed to detect changes to the data.
We then initialise the data on lines 6 through 13.
Lines 15 to 21 contain the functions needed to either delete or pin a discussion, although we haven’t implemented pinDiscussionPoint other than printing what was “pinned”.
Line 26, we use the @StateObject property wrapper to grab our DiscussionPointsViewModel and initialise it.
In the body, we can adapt the code to use viewModel (declared on line 26) and access the discussionPoints property.
This layout is a little different than the one before, but either way can be worked with. The major difference to show you is using a ViewModel to store the data in.
On Line 50, when a swipeAction is performed, we call the deleteDiscussionPoint method in the viewModel which removes the item from the array. At this point, the property wrapper notifies the view that it needs to be redrawn and the item is removed from the list.
List provides a simple way of working with data in a far more easier way than UITableView in UIKit. Although UITableView has some advantages, I personally find it quicker to work with List where I can.
Play around with the swipe actions and see what you can implement. Also, take a look at adding more swipe actions. You can add more than just one swipeActions modifier, and on the second one, you can tell it to be the leading edge so that you can swipe both left and right:
.swipeActions(edge: .leading) {
Button {
print("Favorite \(point)")
} label: {
Label("Favorite", systemImage: "star")
}
.tint(.blue)
}
.swipeActions {
Button(role: .destructive) {
viewModel.deleteDiscussionPoint(point)
} label: {
Label("Delete", systemImage: "trash")
}
Button {
viewModel.pinDiscussionPoint(point)
} label: {
Label("Pin", systemImage: "pin")
}
.tint(.yellow)
}
Leave a Reply
You must be logged in to post a comment.