SwiftUI Lists & Grids - Display Collections Efficiently
Lists and grids are essential for displaying collections of data efficiently. SwiftUI provides powerful, flexible components that handle scrolling, reordering, and lazy loading automatically.
Lists in SwiftUI
The List view is perfect for displaying scrollable, vertical collections of data.
Basic List
struct SimpleList: View { let items = ["Apple", "Banana", "Cherry", "Date", "Elderberry"]
var body: some View { List(items, id: \.self) { item in Text(item) } }}List with Custom Rows
struct Task: Identifiable { let id = UUID() let title: String let isCompleted: Bool}
struct TaskListView: View { let tasks = [ Task(title: "Learn SwiftUI", isCompleted: true), Task(title: "Build an app", isCompleted: false), Task(title: "Deploy to App Store", isCompleted: false) ]
var body: some View { List(tasks) { task in HStack { Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle") .foregroundColor(task.isCompleted ? .green : .gray)
Text(task.title) .strikethrough(task.isCompleted)
Spacer() } } }}List Sections
struct SectionedListView: View { var body: some View { List { Section("Fruits") { Text("Apple") Text("Banana") Text("Orange") }
Section("Vegetables") { Text("Carrot") Text("Broccoli") Text("Spinach") }
Section { Text("Chicken") Text("Beef") Text("Fish") } header: { Text("Proteins") } footer: { Text("High protein foods") } } }}List Styles
struct ListStylesExample: View { var body: some View { NavigationStack { List { Text("Item 1") Text("Item 2") Text("Item 3") } .listStyle(.insetGrouped) // .plain, .grouped, .inset, .sidebar .navigationTitle("List Styles") } }}Interactive Lists
Swipe Actions
struct SwipeActionsExample: View { @State private var items = ["Email 1", "Email 2", "Email 3", "Email 4"]
var body: some View { List { ForEach(items, id: \.self) { item in Text(item) .swipeActions(edge: .trailing) { Button(role: .destructive) { if let index = items.firstIndex(of: item) { items.remove(at: index) } } label: { Label("Delete", systemImage: "trash") }
Button { print("Archive \(item)") } label: { Label("Archive", systemImage: "archivebox") } .tint(.blue) } .swipeActions(edge: .leading) { Button { print("Mark as read") } label: { Label("Read", systemImage: "envelope.open") } .tint(.green) } } } }}Delete and Move
struct EditableListView: View { @State private var tasks = ["Task 1", "Task 2", "Task 3", "Task 4"]
var body: some View { NavigationStack { List { ForEach(tasks, id: \.self) { task in Text(task) } .onDelete(perform: deleteTask) .onMove(perform: moveTask) } .navigationTitle("Tasks") .toolbar { EditButton() } } }
func deleteTask(at offsets: IndexSet) { tasks.remove(atOffsets: offsets) }
func moveTask(from source: IndexSet, to destination: Int) { tasks.move(fromOffsets: source, toOffset: destination) }}Pull to Refresh
struct RefreshableListView: View { @State private var items = ["Item 1", "Item 2", "Item 3"]
var body: some View { List(items, id: \.self) { item in Text(item) } .refreshable { await loadData() } }
func loadData() async { // Simulate network call try? await Task.sleep(nanoseconds: 2_000_000_000) items.append("Item \(items.count + 1)") }}Search in Lists
Add search functionality:
struct SearchableListView: View { @State private var searchText = ""
let allFruits = ["Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig", "Grape", "Honeydew", "Indian Fig", "Jackfruit"]
var filteredFruits: [String] { if searchText.isEmpty { return allFruits } else { return allFruits.filter { $0.localizedCaseInsensitiveContains(searchText) } } }
var body: some View { NavigationStack { List(filteredFruits, id: \.self) { fruit in Text(fruit) } .searchable(text: $searchText, prompt: "Search fruits") .navigationTitle("Fruits") } }}Lazy Stacks
For custom scrolling without List styling:
LazyVStack
struct LazyVStackExample: View { var body: some View { ScrollView { LazyVStack(spacing: 20) { ForEach(1...100, id: \.self) { number in CardView(number: number) } } .padding() } }}
struct CardView: View { let number: Int
var body: some View { RoundedRectangle(cornerRadius: 15) .fill(Color.blue.opacity(0.3)) .frame(height: 100) .overlay( Text("Card \(number)") .font(.title) ) }}LazyHStack
Horizontal lazy loading:
struct LazyHStackExample: View { var body: some View { ScrollView(.horizontal, showsIndicators: false) { LazyHStack(spacing: 15) { ForEach(1...20, id: \.self) { number in RoundedRectangle(cornerRadius: 15) .fill(Color.purple.opacity(0.3)) .frame(width: 150, height: 200) .overlay(Text("Item \(number)")) } } .padding() } }}Grids
Display items in a grid layout:
LazyVGrid - Vertical Grid
struct PhotoGridView: View { let columns = [ GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()) ]
var body: some View { ScrollView { LazyVGrid(columns: columns, spacing: 15) { ForEach(1...50, id: \.self) { number in RoundedRectangle(cornerRadius: 10) .fill(Color.blue.opacity(0.3)) .frame(height: 100) .overlay( Image(systemName: "photo") .font(.largeTitle) ) } } .padding() } }}Adaptive Grid
Automatically adjusts columns based on available space:
struct AdaptiveGridView: View { let adaptiveColumns = [ GridItem(.adaptive(minimum: 100)) ]
var body: some View { ScrollView { LazyVGrid(columns: adaptiveColumns, spacing: 15) { ForEach(1...30, id: \.self) { number in RoundedRectangle(cornerRadius: 10) .fill(Color.green.opacity(0.3)) .frame(height: 100) .overlay(Text("\(number)")) } } .padding() } }}Fixed and Flexible Columns
Mix different column types:
struct MixedGridView: View { let columns = [ GridItem(.fixed(100)), // Fixed width GridItem(.flexible()), // Takes remaining space GridItem(.flexible()) // Shares remaining space ]
var body: some View { ScrollView { LazyVGrid(columns: columns, spacing: 15) { ForEach(1...20, id: \.self) { number in Color.random .frame(height: 80) .cornerRadius(10) .overlay(Text("\(number)")) } } .padding() } }}
extension Color { static var random: Color { Color( red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1) ) }}LazyHGrid - Horizontal Grid
struct HorizontalGridView: View { let rows = [ GridItem(.fixed(100)), GridItem(.fixed(100)), GridItem(.fixed(100)) ]
var body: some View { ScrollView(.horizontal) { LazyHGrid(rows: rows, spacing: 15) { ForEach(1...30, id: \.self) { number in RoundedRectangle(cornerRadius: 10) .fill(Color.orange.opacity(0.3)) .frame(width: 100) .overlay(Text("\(number)")) } } .padding() } }}Complex Grid Layouts
Pinterest-Style Grid
struct PinterestGridView: View { let columns = [ GridItem(.flexible()), GridItem(.flexible()) ]
var body: some View { ScrollView { LazyVGrid(columns: columns, spacing: 15) { ForEach(1...20, id: \.self) { number in ImageCard(number: number) } } .padding() } }}
struct ImageCard: View { let number: Int
var randomHeight: CGFloat { CGFloat.random(in: 150...300) }
var body: some View { VStack(alignment: .leading) { RoundedRectangle(cornerRadius: 15) .fill(Color.blue.opacity(0.3)) .frame(height: randomHeight) .overlay( Image(systemName: "photo") .font(.largeTitle) )
Text("Photo \(number)") .font(.caption) .foregroundColor(.gray) } }}Practical Example - Photo Gallery App
struct Photo: Identifiable { let id = UUID() let title: String let category: String}
struct PhotoGalleryApp: View { @State private var searchText = "" @State private var selectedCategory = "All"
let categories = ["All", "Nature", "Architecture", "People", "Animals"]
let photos = [ Photo(title: "Sunset", category: "Nature"), Photo(title: "Building", category: "Architecture"), Photo(title: "Portrait", category: "People"), Photo(title: "Dog", category: "Animals"), Photo(title: "Mountain", category: "Nature"), Photo(title: "Tower", category: "Architecture"), // Add more photos... ]
var filteredPhotos: [Photo] { photos.filter { photo in let matchesCategory = selectedCategory == "All" || photo.category == selectedCategory let matchesSearch = searchText.isEmpty || photo.title.localizedCaseInsensitiveContains(searchText) return matchesCategory && matchesSearch } }
let columns = [ GridItem(.adaptive(minimum: 150), spacing: 15) ]
var body: some View { NavigationStack { VStack(spacing: 0) { // Category filter ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 10) { ForEach(categories, id: \.self) { category in CategoryButton( title: category, isSelected: selectedCategory == category ) { selectedCategory = category } } } .padding() }
// Photo grid ScrollView { LazyVGrid(columns: columns, spacing: 15) { ForEach(filteredPhotos) { photo in PhotoGridItem(photo: photo) } } .padding() } } .navigationTitle("Gallery") .searchable(text: $searchText, prompt: "Search photos") } }}
struct CategoryButton: View { let title: String let isSelected: Bool let action: () -> Void
var body: some View { Button(action: action) { Text(title) .padding(.horizontal, 16) .padding(.vertical, 8) .background(isSelected ? Color.blue : Color.gray.opacity(0.2)) .foregroundColor(isSelected ? .white : .primary) .cornerRadius(20) } }}
struct PhotoGridItem: View { let photo: Photo
var body: some View { VStack(alignment: .leading, spacing: 8) { RoundedRectangle(cornerRadius: 15) .fill(Color.blue.opacity(0.2)) .aspectRatio(1, contentMode: .fit) .overlay( Image(systemName: "photo") .font(.largeTitle) .foregroundColor(.gray) )
VStack(alignment: .leading, spacing: 4) { Text(photo.title) .font(.headline) Text(photo.category) .font(.caption) .foregroundColor(.gray) } } }}Performance Optimization
Use Lazy Loading
// ✅ Good - Lazy loadingScrollView { LazyVStack { ForEach(1...1000, id: \.self) { number in HeavyView(number: number) } }}
// ❌ Avoid - Loads all at onceScrollView { VStack { ForEach(1...1000, id: \.self) { number in HeavyView(number: number) // All 1000 created immediately! } }}Identifiable for Performance
// ✅ Good - Identifiablestruct Item: Identifiable { let id = UUID() let name: String}
List(items) { item in Text(item.name)}
// ⚠️ Less efficientList(items, id: \.name) { item in Text(item.name)}Avoid Heavy Computations in Body
// ✅ Good - Computed oncestruct OptimizedView: View { let data: [Item]
var sortedData: [Item] { data.sorted { $0.name < $1.name } }
var body: some View { List(sortedData) { item in Text(item.name) } }}
// ❌ Avoid - Computed on every renderstruct SlowView: View { let data: [Item]
var body: some View { List(data.sorted { $0.name < $1.name }) { item in Text(item.name) } }}Best Practices
1. Choose the Right Component
// List - For simple, styled listsList(items) { item in Text(item.name) }
// LazyVStack - For custom scrollingScrollView { LazyVStack { ForEach(items) { ... } }}
// LazyVGrid - For grid layoutsScrollView { LazyVGrid(columns: columns) { ... }}2. Use Lazy Loading
// ✅ Always use Lazy variants for large datasetsLazyVStack { }LazyHStack { }LazyVGrid(columns:) { }LazyHGrid(rows:) { }3. Identifiable Protocol
// ✅ Good - Conforms to Identifiablestruct Item: Identifiable { let id = UUID() let name: String}4. Efficient Updates
// ✅ Good - SwiftUI knows what changed@State private var items: [Item] = []
// Changes to specific items update efficientlyitems[0].isCompleted = trueSummary
- ✅ List - Styled, scrollable collections with built-in features
- ✅ LazyVStack/HStack - Custom scrolling with lazy loading
- ✅ LazyVGrid/HGrid - Efficient grid layouts
- ✅ Swipe Actions - Delete, archive, mark as read
- ✅ Search - Filter content with
.searchable() - ✅ Pull to Refresh - Update data with
.refreshable() - ✅ Performance - Use lazy loading for large datasets
- ✅ Adaptive Grids - Automatically adjust to available space
Master lists and grids to display any amount of data efficiently! 📱
Next: Learn SwiftUI Data Flow for advanced state management across your app.