List is a container View in SwiftUI for displaying data as a vertically scrollable list, analogous to UITableView in UIKit. According to Apple Developer Documentation, 2024, List supports static and dynamic sections, swipe actions, row reordering and pull-to-refresh. Unlike UITableView, List uses a declarative API based on SwiftUI and ForEach, automatically managing cell reuse and performance with a large number of rows.
Key Takeaways
List is a View that displays a sequence of items in a vertically scrollable list. It was introduced in iOS 13 alongside SwiftUI and is the primary way to display lists of data, replacing UITableView from UIKit. List automatically manages cell reuse, scrolling, and performance.
List uses lazy-loading: cells are created as you scroll, not all at once. This sets it apart from VStack with ForEach inside ScrollView, where all cells are created at render time. List also provides built-in support for swipe actions, pull-to-refresh, editing (delete/move), and row selection.
According to Apple WWDC 2021 (Session 10072), List in iOS 15+ received significant performance improvements thanks to a new diffing mechanism at the collection level. This made List more efficient when updating data, especially for lists with hundreds of rows.
Developers often choose between List and ScrollView with VStack for displaying a set of Views. The key difference: List uses cell reuse (like UITableView), while ScrollView + VStack creates all Views immediately. For fixed-size lists (up to 20 items), the difference is negligible. For dynamic lists with 50+ rows, List is preferable for performance.
Static List is a list with a fixed number of rows specified directly in the List body. It is used for menus, settings, and forms with a known set of items. Each row is declared explicitly, without loops or ForEach.
// Static list (for menus and settings)
List {
Text("Profile")
Text("Settings")
Text("About")
}
// Dynamic list (for data)
struct UserList: View {
let users: [User]
var body: some View {
List(users) { user in
HStack {
Text(user.name)
Text(user.role)
.foregroundColor(.secondary)
}
}
}
}
Dynamic List uses the List(data:rowContent:) initializer or ForEach inside the List body. The first approach is convenient when each row corresponds to one data item. The second is useful when there are sections or additional items between data.
Identification (Identifiable): For dynamic lists, data items must conform to the Identifiable protocol, or you must specify a KeyPath to a unique identifier in the data:id tuple. SwiftUI uses identifiers to track changes: additions, deletions, and row moves.
Section is a View for grouping rows in a List with a header and an optional footer. Section accepts header and footer as ViewBuilder, allowing you to use not only text but also custom Views for section headers.
struct SettingsView: View {
var body: some View {
List {
Section(header: Text("Account")) {
Text("Name")
Text("Email")
}
Section(header: Text("Notifications")) {
Toggle("Push", isOn: $pushEnabled)
Toggle("Email", isOn: $emailEnabled)
}
}
.listStyle(.insetGrouped)
}
}
// Dynamic sections with ForEach
List {
ForEach(groupedData.keys.sorted(), id: \.self) { key in
Section(header: Text(key)) {
ForEach(groupedData[key]!) { item in
Text(item.title)
}
}
}
}
List Styles: SwiftUI provides several built-in styles through the .listStyle() modifier. .insetGrouped — standard for iOS Settings, .plain — minimalistic, .inset — with indentation, .sidebar — for Sidebar on iPad.
According to SwiftUI Cookbook (2024), Section with dynamic sections and ForEach inside is a standard pattern for grouping data in applications with complex structure. The key rule: do not nest Section inside Section, and do not use the List(data:) initializer together with Section — use ForEach inside the List body.
.swipeActions(edge:allowsFullSwipe:content:) — a modifier for iOS 15+ that adds swipe actions to List rows. It allows displaying buttons when swiping left (default) or right, with different colors and roles (destructive, cancel).
struct TaskList: View {
@Binding var tasks: [Task]
var body: some View {
List {
ForEach($tasks) { $task in
Text(task.title)
.swipeActions(edge: .trailing) {
Button("Delete", role: .destructive) {
tasks.removeAll { $0.id == task.id }
}
}
.swipeActions(edge: .leading) {
Button(task.isDone ? "Undo" : "Done") {
task.isDone.toggle()
}
.tint(.green)
}
}
}
.refreshable {
// Async data loading
await loadTasks()
}
}
}
.refreshable — a modifier for iOS 15+ that adds pull-to-refresh. It accepts an async closure that executes when the user pulls the list down. SwiftUI automatically displays a loading indicator. After the operation completes, the indicator is hidden.
.onDelete and .onMove — modifiers for iOS 13+ that add support for row deletion and reordering. To use them, wrap the data in ForEach with Binding or pass closures through .onDelete(perform:) on List or ForEach.
List performance depends on the number of rows, the complexity of each cell, and the frequency of data updates. SwiftUI uses lazy-loading and cell reuse (similar to UITableView.dequeueReusableCell), but additional optimizations may be required for lists with 500+ rows.
| Optimization | Description | iOS version |
|---|---|---|
| Identifiable | Unique IDs for each item | iOS 13+ |
| EquatableView | Avoids redrawing when data is equal | iOS 13+ |
| id(_:) | Forces View recreation when ID changes | iOS 13+ |
| .equatable() | Strict Equatable comparison | iOS 15+ |
| Diffable data | Automatic diff on changes | iOS 15+ |
Issue 1: Frequent updates. If the data in the list updates frequently (e.g., every second), List may redraw visible cells on each state change. Solution: use structures (value types) for data — SwiftUI compares them by value and redraws only changed rows.
Issue 2: Heavy cells. If each row contains a complex View hierarchy, images, and animations, scrolling may lag. Solution: extract cells into separate Views, use EquatableView to prevent unnecessary redraws. According to SwiftUI Lab (2024), splitting a complex row into subcomponents reduces rendering time by 30–50%.
Issue 3: Large number of rows. With 1000+ rows, List still works efficiently thanks to lazy-loading, but initial loading may slow down due to layout computation. Solution: use LazyVStack only for lists with uniform rows where List features (swipe, sections) are not needed. For full-featured lists, List remains the best choice.
Frequently Asked Questions
List is a container View for displaying a scrollable data list in SwiftUI. It is the equivalent of UITableView in UIKit with a declarative API. Supports sections, swipe actions, pull-to-refresh, editing, and customization via .listStyle().
List uses lazy-loading and cell reuse — cells are created as you scroll. ScrollView + VStack creates all Views at once. For lists with 50+ rows, List is preferable. For fixed small sets (up to 20 items), the difference is negligible.
Use the .refreshable modifier (iOS 15+). Pass an async closure with data update logic. SwiftUI automatically displays a loading indicator and hides it after the async operation completes.
Use Section View with a header and optional footer. Place list rows inside Section. For dynamic sections, use ForEach with groupedData. The list style is configured via .listStyle(.insetGrouped) for an iOS-like appearance.
Use structures (value types) for data, extract complex cells into separate Views with EquatableView, avoid frequent state updates in each row. For lists with 1000+ rows, consider LazyVStack if List features are not needed.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also