Learn · 04 of 09
Lists and selection.
Select-from-a-list-and-do-something-with-it is the
core interaction loop in terminal software — see
Lazygit, K9s, Yazi, fzf, every editor's buffer picker. TerminalUI
ships three collection views with first-class selection bindings:
List — flat collection, single or multi-selection. OutlineGroup — hierarchical rows with disclosure. Table — multi-column, sortable, multi-selection.
All three take a selection binding. The shape of the
binding decides the selection mode:
Binding<ID?> — single selection. Binding<Set<ID>> — multi-selection.
Anatomy
Rows must be Identifiable
Selection is keyed by ID. Rows are
Identifiable either explicitly (the model conforms) or
implicitly via a key path
(List(deploys, id: \.id)). Stable IDs are the same
requirement that @State persistence depends on
elsewhere in the framework — see
State keying.
Selection mode is the binding shape
Single: Binding<ID?>. Multi:
Binding<Set<ID>>. There is no separate
"select mode" property — the type system decides.
Hierarchical with OutlineGroup
OutlineGroup takes a children key path
that returns [Element]?. Leaves return
nil; non-leaves return their children.
Returning an empty array makes the row expandable but empty —
almost certainly not what you want. The
updatesIfNonEmpty pattern in the example above is the
idiomatic shape.
Laziness
List placement is lazy by default — only visible rows
are placed and drawn. LazyVStack /
LazyHStack let you opt into the same behaviour for
ordinary stacks. The single-ForEach case has a
full-lazy fast path; mixed children fall back to viewport-lazy
placement.
Mouse and pointer
On terminals that report mouse, list rows are clickable for
selection. Tutorial 07
covers the pointer model in depth — for now, treat clicks as
automatic when the host supports them.
Where to go next