List & ForEach
Basic List
List is a container that displays rows of data in a scrollable, platform-styled layout. It provides built-in separation, insets, and swipe behavior.
struct FruitList: View {
let fruits = ["Apple", "Banana", "Cherry", "Date", "Elderberry"]
var body: some View {
List(fruits, id: \.self) { fruit in
Text(fruit)
}
}
}
List with Identifiable Data
For custom types, conform to Identifiable or provide an id key path:
struct Fruit: Identifiable {
let id = UUID()
let name: String
let color: Color
}
struct FruitList: View {
let fruits = [
Fruit(name: "Apple", color: .red),
Fruit(name: "Banana", color: .yellow),
Fruit(name: "Cherry", color: .red)
]
var body: some View {
List(fruits) { fruit in
HStack {
Circle()
.fill(fruit.color)
.frame(width: 20, height: 20)
Text(fruit.name)
}
}
}
}
ForEach for Repeated Views
ForEach generates views from a collection. It works inside List, VStack, or any container:
VStack {
ForEach(0..<5) { index in
Text("Row \(index)")
}
}
// With identifiable data
ForEach(fruits) { fruit in
Text(fruit.name)
}
ForEach is not a view itself -- it generates content within a parent container. It requires stable identifiers to correctly track items across updates.
List with Sections
Organize content into sections with headers and footers:
List {
Section("Fruits") {
ForEach(fruits) { fruit in
Text(fruit.name)
}
}
Section("Vegetables") {
ForEach(vegetables) { veg in
Text(veg.name)
}
}
}
.listStyle(.insetGrouped)
List Styles
List { /* content */ }
.listStyle(.plain) // minimal separators
.listStyle(.insetGrouped) // grouped with insets (default on iOS)
.listStyle(.inset) // inset without grouping
.listStyle(.grouped) // full-width grouped
.listStyle(.sidebar) // for sidebar navigation
Dynamic deletion
struct EditableList: View {
@State private var items = ["Item 1", "Item 2", "Item 3"]
var body: some View {
List {
ForEach(items, id: \.self) { item in
Text(item)
}
.onDelete(perform: deleteItems)
}
.toolbar {
EditButton()
}
}
func deleteItems(at offsets: IndexSet) {
items.remove(atOffsets: offsets)
}
}
List Row Actions
List {
ForEach(messages) { message in
Text(message.text)
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
delete(message)
} label: {
Label("Delete", systemImage: "trash")
}
Button {
archive(message)
} label: {
Label("Archive", systemImage: "archivebox")
}
.tint(.blue)
}
.swipeActions(edge: .leading) {
Button {
toggleRead(message)
} label: {
Label(
message.isRead ? "Unread" : "Read",
systemImage: message.isRead ? "envelope.open" : "envelope"
)
}
.tint(.orange)
}
}
}
Swipe Actions & Search
Swipe Actions
The .swipeActions modifier adds trailing or leading actions when the user swipes on a row:
List(tasks) { task in
Text(task.title)
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
delete(task)
} label: {
Label("Delete", systemImage: "trash")
}
.tint(.red)
Button {
pin(task)
} label: {
Label("Pin", systemImage: "pin")
}
.tint(.orange)
}
.swipeActions(edge: .leading) {
Button {
complete(task)
} label: {
Label("Done", systemImage: "checkmark")
}
.tint(.green)
}
}
.trailingis the default swipe direction (left to right).leadingrequires an explicit right-to-left swipe.tint()sets the background color of the swipe buttonrole: .destructiveapplies standard destructive styling
Searchable Modifier
Add a search bar to filter list content:
struct SearchableList: View {
let items = ["Apple", "Banana", "Cherry", "Date", "Elderberry"]
@State private var searchText = ""
var filteredItems: [String] {
if searchText.isEmpty {
return items
}
return items.filter { $0.localizedCaseInsensitiveContains(searchText) }
}
var body: some View {
NavigationStack {
List(filteredItems, id: \.self) { item in
Text(item)
}
.searchable(text: $searchText, prompt: "Search fruits")
.navigationTitle("Fruits")
}
}
}
Advanced Search with Suggestions
.searchable(
text: $searchText,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "Search"
) {
// Search suggestions appear when search field is active
ForEach(suggestions, id: \.self) { suggestion in
Text(suggestion)
.searchCompletion(suggestion)
}
}
Pull-to-Refresh
List(items) { item in
Text(item.name)
}
.refreshable {
await loadItems()
}
The refreshable modifier adds pull-to-refresh and works with both sync and async functions.
Search with Async Filtering
.searchable(text: $searchText)
.task(id: searchText) {
// Debounced search
try? await Task.sleep(for: .milliseconds(300))
results = await searchService.search(query: searchText)
}
List Insets and Separators
List {
ForEach(items) { item in
Text(item.name)
}
.listRowSeparator(.hidden) // hide separators
.listRowSeparatorTint(.blue) // color separators
.listRowInsets(.init(top: 8, leading: 20, bottom: 8, trailing: 20))
}
Quiz
1. What must data conform to for use in ForEach?
2. What is the difference between .sheet() and .fullScreenCover()?
3. How do you add a search bar to a list?
4. What replaced NavigationView in modern SwiftUI?
5. How do you add swipe actions to a list row?
Flashcards
Question
What is List in SwiftUI?
Click to reveal answer
Answer
A scrollable container that displays rows with built-in platform styling, separators, and swipe actions.
Question
What replaced NavigationView?
Click to reveal answer
Answer
NavigationStack, which supports programmatic navigation via NavigationPath and type-safe destinations.
Question
How do you add search functionality?
Click to reveal answer
Answer
Use .searchable(text:prompt:) modifier on a list or navigation container.
Question
What is NavigationPath?
Click to reveal answer
Answer
A type-erased stack of navigation destinations that supports programmatic push/pop and state restoration.
Question
What is the difference between .onDelete and .swipeActions?
Click to reveal answer
Answer
.onDelete provides a simple delete button. .swipeActions allows multiple custom actions with different colors and icons.
Revision Notes
Key Takeaways
- 1. List provides built-in styling, separators, and scroll behavior
- 2. ForEach requires stable identifiers via Identifiable or id key path
- 3. NavigationStack replaces NavigationView with programmatic navigation support
- 4. .searchable() adds integrated search with suggestions and keyboard handling
- 5. .swipeActions() provides customizable trailing and leading row actions
Interview Tips
- • Explain the difference between List and ForEach (container vs generator)
- • Know when to use .sheet vs .fullScreenCover
- • Be ready to discuss programmatic navigation with NavigationPath
- • Understand how to add search, swipe actions, and pull-to-refresh to lists
Cheat Sheet
List: scrollable rows with platform styling.
ForEach: generates views from collections, requires stable IDs.
Swipe actions: .swipeActions(edge:) with .tint() for colors.
Search: .searchable(text:prompt:) for integrated search bars.
NavigationStack: modern navigation with NavigationPath.
NavigationLink: pushes views; .navigationDestination for programmatic.
Sheets: .sheet() partial, .fullScreenCover() full screen.