87 lines
2.7 KiB
Markdown
87 lines
2.7 KiB
Markdown
# List and Section
|
|
|
|
## Intent
|
|
|
|
Use `List` for feed-style content and settings-style rows where built-in row reuse, selection, and accessibility matter.
|
|
|
|
## Core patterns
|
|
|
|
- Prefer `List` for long, vertically scrolling content with repeated rows.
|
|
- Use `Section` headers to group related rows.
|
|
- Pair with `ScrollViewReader` when you need scroll-to-top or jump-to-id.
|
|
- Use `.listStyle(.plain)` for modern feed layouts.
|
|
- Use `.listStyle(.grouped)` for multi-section discovery/search pages where section grouping helps.
|
|
- Apply `.scrollContentBackground(.hidden)` + a custom background when you need a themed surface.
|
|
- Use `.listRowInsets(...)` and `.listRowSeparator(.hidden)` to tune row spacing and separators.
|
|
- Use `.environment(\\.defaultMinListRowHeight, ...)` to control dense list layouts.
|
|
|
|
## Example: feed list with scroll-to-top
|
|
|
|
```swift
|
|
@MainActor
|
|
struct TimelineListView: View {
|
|
@Environment(\.selectedTabScrollToTop) private var selectedTabScrollToTop
|
|
@State private var scrollToId: String?
|
|
|
|
var body: some View {
|
|
ScrollViewReader { proxy in
|
|
List {
|
|
ForEach(items) { item in
|
|
TimelineRow(item: item)
|
|
.id(item.id)
|
|
.listRowInsets(.init(top: 12, leading: 16, bottom: 6, trailing: 16))
|
|
.listRowSeparator(.hidden)
|
|
}
|
|
}
|
|
.listStyle(.plain)
|
|
.environment(\\.defaultMinListRowHeight, 1)
|
|
.onChange(of: scrollToId) { _, newValue in
|
|
if let newValue {
|
|
proxy.scrollTo(newValue, anchor: .top)
|
|
scrollToId = nil
|
|
}
|
|
}
|
|
.onChange(of: selectedTabScrollToTop) { _, newValue in
|
|
if newValue == 0 {
|
|
withAnimation {
|
|
proxy.scrollTo(ScrollToView.Constants.scrollToTop, anchor: .top)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Example: settings-style list
|
|
|
|
```swift
|
|
@MainActor
|
|
struct SettingsView: View {
|
|
var body: some View {
|
|
List {
|
|
Section("General") {
|
|
NavigationLink("Display") { DisplaySettingsView() }
|
|
NavigationLink("Haptics") { HapticsSettingsView() }
|
|
}
|
|
Section("Account") {
|
|
Button("Sign Out", role: .destructive) {}
|
|
}
|
|
}
|
|
.listStyle(.insetGrouped)
|
|
}
|
|
}
|
|
```
|
|
|
|
## Design choices to keep
|
|
|
|
- Use `List` for dynamic feeds, settings, and any UI where row semantics help.
|
|
- Use stable IDs for rows to keep animations and scroll positioning reliable.
|
|
- Prefer `.contentShape(Rectangle())` on rows that should be tappable end-to-end.
|
|
- Use `.refreshable` for pull-to-refresh feeds when the data source supports it.
|
|
|
|
## Pitfalls
|
|
|
|
- Avoid heavy custom layouts inside a `List` row; use `ScrollView` + `LazyVStack` instead.
|
|
- Be careful mixing `List` and nested `ScrollView`; it can cause gesture conflicts.
|