SwiftUI Form, Section, List, ScrollView, lazy stacks, and grids under One.iOS

One.iOS lists, forms, and scrolling containers are SwiftUI’s own: Form,
List, OutlineGroup, ScrollView, ScrollViewReader, LazyVStack, LazyHStack,
LazyVGrid, LazyHGrid, Grid, and GridRow, each hosted in a Fabric view
with children composed into one SwiftUI tree. These are greedy containers: they
take the box they are given instead of reporting an ideal height, so give each
its own box rather than nesting one inside a stack.
Reach for Form for grouped settings screens, List for row lists with
selection and swipe actions, and ScrollView with lazy stacks or grids for
long or two-dimensional content. Row modifiers, swipe actions, pull to refresh,
and search arrive through swiftStyle.
import { One, useNativeState } from 'one'
function Settings() { const airplane = useNativeState(false)
return ( <One.iOS.Form> <One.iOS.Section title="General"> <One.iOS.Toggle label="Airplane Mode" isOn={airplane} onIsOnChange={(value) => airplane.set(value)} /> </One.iOS.Section> </One.iOS.Form> )}The form fills its box and groups its rows into a titled section. The toggle
binds to a NativeState handle, so flips converge without a React render.
List-family containers fill their parent’s remaining space unless style gives
them sizing of their own. They compose their children into the parent’s hosting
controller, and when a row’s props change the container republishes just that
row’s composed content, keeping its identity and state. The listStyle name is
checked against the device’s iOS version and unknown names throw.
Selection is tag based. Give each selectable row a unique string tag through
its swiftStyle; SwiftUI reports the selected tags and One parses the proposal
back in JavaScript. Omit selection and SwiftUI manages selection itself. When
supplied, it is controlled: onSelectionChange reports the native proposal and
the list moves only when the prop updates.
Search and pull to refresh are swiftStyle modifiers. Put searchable on a
NavigationStack around the list so iOS presents its native search field, keep
the text in React state, and filter the rows yourself. refreshable takes an
async callback that the bridge awaits before the indicator finishes.
Form is a grouped settings form. sizing 'fill' flexes to its box;
'content' wraps its rows. Switching sizing remounts the form, so a fill form
never inherits a content height Yoga pinned, or the reverse. Form also takes
the environment props from iOS Stacks.
import { One } from 'one'
function Profile() { return ( <One.iOS.Form sizing="content"> <One.iOS.Section title="Profile" footer="Shown on your public page."> <One.iOS.Text text="Ada Lovelace" /> </One.iOS.Section> </One.iOS.Form> )}A content-sized form wraps its rows, so it fits naturally above other content instead of filling the screen.
Its swiftStyle takes SwiftUI’s scroll and form modifiers. Inside a sheet,
scrollContentBackground: 'hidden' drops the grouped panel so the sheet’s own
glass shows through, as Apple’s apps do:
<One.iOS.Form swiftStyle={{ scrollContentBackground: 'hidden' }}> <One.iOS.Section title="Room"> <One.iOS.Text text="Kitchen" /> </One.iOS.Section></One.iOS.Form>| Props | Type | Description |
|---|---|---|
sizingFormSizing = "fill"fill flexes to its box; content wraps its rows. | FormSizing = "fill" | fill flexes to its box; content wraps its rows. |
swiftStyleOneNativeStyleForm modifiers such as scrollContentBackground. | OneNativeStyle | Form modifiers such as scrollContentBackground. |
children (required)ReactNodeSections and rows. | ReactNode | Sections and rows. |
Section groups rows under a title header with a footer string beneath
them. Its swiftStyle takes SwiftUI section modifiers such as
headerProminence and listSectionMargins; React updates to them refresh that
section.
import { One } from 'one'
function Mailbox() { return ( <One.iOS.List listStyle="insetGrouped" style={{ height: 320 }}> <One.iOS.Section title="Mailboxes" swiftStyle={{ headerProminence: 'increased' }} > <One.iOS.Label label="Inbox" systemImage="tray" /> </One.iOS.Section> </One.iOS.List> )}| Props | Type | Description |
|---|---|---|
titlestring = ""The header. | string = "" | The header. |
footerstring = ""Text under the rows. | string = "" | Text under the rows. |
swiftStyleOneNativeStyleSection modifiers such as headerProminence. | OneNativeStyle | Section modifiers such as headerProminence. |
children (required)ReactNodeThe rows. | ReactNode | The rows. |
List is a SwiftUI list. listStyle is 'automatic', 'sidebar',
'inset', 'grouped', 'insetGrouped', or 'plain'.
import { useState } from 'react'import { One } from 'one'
function Mailboxes({ mailboxes }: { mailboxes: { id: string; name: string }[] }) { const [selected, setSelected] = useState<string[]>([])
return ( <One.iOS.List selection={selected} onSelectionChange={setSelected}> {mailboxes.map((mailbox) => ( <One.iOS.Label key={mailbox.id} label={mailbox.name} systemImage="tray" swiftStyle={{ tag: mailbox.id }} /> ))} </One.iOS.List> )}Each row’s tag identifies it in the selection array. The state update accepts
the native proposal, moving the list to match.
| Props | Type | Description |
|---|---|---|
listStyleListStyle = "automatic"automatic, sidebar, inset, grouped, insetGrouped, or plain. | ListStyle = "automatic" | automatic, sidebar, inset, grouped, insetGrouped, or plain. |
selectionstring[]Selected row tags. Supplying it makes selection controlled. | string[] | Selected row tags. Supplying it makes selection controlled. |
onSelectionChange(selection: string[]) => voidReceives the native selection proposal. | (selection: string[]) => void | Receives the native selection proposal. |
swiftStyleOneNativeStyleList modifiers, scrollContentBackground, refreshable, and section spacing. | OneNativeStyle | List modifiers, scrollContentBackground, refreshable, and section spacing. |
children (required)ReactNodeSections and rows. | ReactNode | Sections and rows. |
Put searchable on the NavigationStack around the list and refreshable on
the list itself:
import { useState } from 'react'import { One } from 'one'
function SearchableList({ items, reload }: { items: string[]; reload: () => Promise<void> }) { const [query, setQuery] = useState('')
return ( <One.iOS.NavigationStack style={{ flex: 1 }} swiftStyle={{ searchable: { value: query, onChange: setQuery } }} > <One.iOS.List listStyle="plain" style={{ flex: 1 }} swiftStyle={{ refreshable: reload }} > {items .filter((item) => item.includes(query)) .map((item) => ( <One.iOS.Text key={item} text={item} /> ))} </One.iOS.List> </One.iOS.NavigationStack> )}The search field lives in the stack’s toolbar, so a standalone list shows none. The query stays in React state and the list renders the filtered rows.
Use a row’s swiftStyle for SwiftUI row modifiers such as listRowInsets,
listRowSeparator, and listRowSeparatorTint. Modifiers apply in object
order, so put listRowSeparatorTint before listRowSeparator for the tint to
take effect with the visibility.
import { One } from 'one'
function InboxRow({ unread }: { unread: boolean }) { return ( <One.iOS.List listStyle="plain" style={{ height: 240 }}> <One.iOS.Text text="Inbox" swiftStyle={{ listRowInsets: { edges: 'leading', length: unread ? 96 : 16 }, listRowSeparatorTint: { color: 'red', edges: 'bottom' }, listRowSeparator: { visibility: unread ? 'hidden' : 'visible', edges: 'bottom' }, }} /> </One.iOS.List> )}Rows also support swipe actions through
One.iOS.SwipeActions, and an
One.iOS.EditButton composed inside the list toggles its native label between
Edit and Done. Row deletion and move actions are not exposed.
Use ViewSlot with name="listRowBackground" to draw a SwiftUI view behind
one row. The base child is the row; ViewSlot.Content is the background.
import { One } from 'one'
function HighlightedRow() { return ( <One.iOS.List style={{ flex: 1 }}> <One.iOS.ViewSlot name="listRowBackground"> <One.iOS.Text text="Apple" /> <One.iOS.ViewSlot.Content> <One.iOS.Rectangle fill="#B1DAFD" /> </One.iOS.ViewSlot.Content> </One.iOS.ViewSlot> </One.iOS.List> )}ScrollView scrolls its children. axes is 'vertical', 'horizontal', or
'both'; showsIndicators toggles the scroll indicators.
import type { ReactNode } from 'react'import { One } from 'one'
function Gallery({ reload, children }: { reload: () => Promise<void>; children: ReactNode }) { return ( <One.iOS.ScrollView axes="vertical" swiftStyle={{ refreshable: reload }}> {children} </One.iOS.ScrollView> )}A vertical scroll view takes refreshable for pull to refresh, and shows the
native search field when a surrounding NavigationStack carries searchable,
as with List.
| Props | Type | Description |
|---|---|---|
axesScrollViewAxes = "vertical"vertical, horizontal, or both. | ScrollViewAxes = "vertical" | vertical, horizontal, or both. |
showsIndicatorsboolean = trueShows the scroll indicators. | boolean = true | Shows the scroll indicators. |
swiftStyleOneNativeStyleScroll modifiers and refreshable. | OneNativeStyle | Scroll modifiers and refreshable. |
children (required)ReactNodeThe content. | ReactNode | The content. |
ScrollViewReader adapts SwiftUI’s ScrollViewReader content builder to a
JavaScript function receiving a ScrollViewProxy. Give a view a swiftStyle.id,
then call proxy.scrollTo(id, anchor) to scroll the scroll view inside the
reader that contains it. The call runs on the proxy SwiftUI gave the native
reader.
import { One } from 'one'
function JumpToLatest({ rows }: { rows: number[] }) { return ( <One.iOS.ScrollViewReader> {(proxy) => ( <> <One.iOS.Button label="Latest" onPress={() => proxy.scrollTo('row-29', 'bottom')} /> <One.iOS.ScrollView> {rows.map((row) => ( <One.iOS.Text key={row} text={`Row ${row}`} swiftStyle={{ id: `row-${row}` }} /> ))} </One.iOS.ScrollView> </> )} </One.iOS.ScrollViewReader> )}anchor is a SwiftUI UnitPoint: 'zero', 'center', 'leading',
'trailing', 'top', 'bottom', 'topLeading', 'topTrailing',
'bottomLeading', 'bottomTrailing', or { x, y } in unit coordinates.
Omitted or null scrolls the least distance that reveals the view, and an ID
no view carries scrolls nothing, as in SwiftUI.
| Props | Type | Description |
|---|---|---|
swiftStyleOneNativeStyleSwiftUI modifiers for this view, such as padding, background, and frame. | OneNativeStyle | SwiftUI modifiers for this view, such as padding, background, and frame. |
children (required)(proxy: ScrollViewProxy) => ReactNodeThe content closure, run with the native reader proxy. | (proxy: ScrollViewProxy) => ReactNode | The content closure, run with the native reader proxy. |
| ScrollViewProxy | Type | Description |
|---|---|---|
scrollTo(id: string, anchor?: UnitPoint | null) => voidScrolls to the view whose swiftStyle.id is id. | (id: string, anchor?: UnitPoint | null) => void | Scrolls to the view whose swiftStyle.id is id. |
OutlineGroup draws a tree as rows that expand and collapse, SwiftUI’s
disclosure rows. Place it inside a List, Form, or Section. Give it the
tree as data, idKey for the property holding each item’s id (default
'id'), and childrenKey for the property holding its child items, the way
SwiftUI’s children: key path does. The function child returns the row for one
item and runs once per item in the tree. Native owns which rows are open; the
group has no expansion state to control.
import { One } from 'one'
type Folder = { id: string; name: string; folders?: Folder[] }
function Folders({ roots }: { roots: Folder[] }) { return ( <One.iOS.List listStyle="sidebar"> <One.iOS.OutlineGroup data={roots} childrenKey="folders"> {(folder) => <One.iOS.Label label={folder.name} systemImage="folder" />} </One.iOS.OutlineGroup> </One.iOS.List> )}An item whose children property is missing or null is a leaf with no
disclosure indicator; an empty array is a branch with nothing in it. Ids must
be strings or numbers and unique across the whole tree, and a repeat throws.
SwiftUI’s List(data, children:) is not a separate view here; use
OutlineGroup inside a List.
| Props | Type | Description |
|---|---|---|
data (required)readonly Item[]The root items of the tree. | readonly Item[] | The root items of the tree. |
idKeykeyof Item = 'id'The property holding each item id: a string or number. | keyof Item = 'id' | The property holding each item id: a string or number. |
childrenKey (required)keyof ItemThe property holding each item child items, or null for a leaf. | keyof Item | The property holding each item child items, or null for a leaf. |
swiftStyleOneNativeStyleSwiftUI modifiers for this view, such as padding, background, and frame. | OneNativeStyle | SwiftUI modifiers for this view, such as padding, background, and frame. |
children (required)(item: Item) => ReactNodeThe row content for one item. | (item: Item) => ReactNode | The row content for one item. |
Table draws rows with several columns, SwiftUI’s Table. On iPhone SwiftUI
shows only the first column; on iPad it shows every column with a header row.
Give it the rows as data, idKey for the property holding each row’s id
(default 'id'), and columns, each with an id and a title. The function
child receives a row and a column and returns the cell content, and it runs once
per row and column.
import { useState } from 'react'import { One, type TableSort } from 'one'
type Person = { id: string; name: string; age: number }
const columns = [ { id: 'name', title: 'Name' }, { id: 'age', title: 'Age' },]
function People({ people }: { people: Person[] }) { const [sort, setSort] = useState<TableSort | null>(null) const sorted = sort ? [...people].sort((a, b) => { const order = sort.column === 'age' ? a.age - b.age : a.name.localeCompare(b.name) return sort.direction === 'ascending' ? order : -order }) : people return ( <One.iOS.Table style={{ flex: 1 }} data={sorted} columns={columns} sort={sort} onSortChange={setSort} > {(person, column) => ( <One.iOS.Text text={column.id === 'age' ? String(person.age) : person.name} /> )} </One.iOS.Table> )}A table is sortable when you pass onSortChange: every header then sorts, sort
names the sorted column (or null for none), and a header tap calls
onSortChange with the column and direction. The table never reorders rows
itself; sort data and pass it back, and bump revision to take the sort prop
over a tap you did not accept. sort without onSortChange, or the reverse,
throws. Ids must be strings or numbers and unique among rows and among columns,
and a repeat throws. Every cell is mounted, so keep tables to the rows a screen
needs. Give the table a height, as you would a List. Table needs iOS 17.4 or newer and draws nothing on earlier systems.
| Props | Type | Description |
|---|---|---|
data (required)readonly Item[]The rows. | readonly Item[] | The rows. |
idKeykeyof Item = 'id'The property holding each row id: a string or number. | keyof Item = 'id' | The property holding each row id: a string or number. |
columns (required)readonly TableColumn[]The columns, each with an id and a header title. | readonly TableColumn[] | The columns, each with an id and a header title. |
sortTableSort | nullThe sorted column and its direction, or null for none. Needs onSortChange. | TableSort | null | The sorted column and its direction, or null for none. Needs onSortChange. |
onSortChange(sort: TableSort) => voidCalled when a header is tapped. Makes every header sort. | (sort: TableSort) => void | Called when a header is tapped. Makes every header sort. |
revisionnumber = 0Bump to take the sort prop over a header tap you did not accept. | number = 0 | Bump to take the sort prop over a header tap you did not accept. |
swiftStyleOneNativeStyleSwiftUI modifiers for this view, such as padding, background, and frame. | OneNativeStyle | SwiftUI modifiers for this view, such as padding, background, and frame. |
children (required)(item: Item, column: TableColumn) => ReactNodeThe cell content for one row and column. | (item: Item, column: TableColumn) => ReactNode | The cell content for one row and column. |
LazyVStack and LazyHStack lay children out lazily along one axis.
LazyVStack aligns with 'leading', 'center', or 'trailing';
LazyHStack aligns with 'top', 'center', 'bottom',
'firstTextBaseline', or 'lastTextBaseline'. spacing must be zero or more
when given; omitted, SwiftUI uses its default.
import { One } from 'one'
function Feed({ rows }: { rows: string[] }) { return ( <One.iOS.ScrollView style={{ flex: 1 }}> <One.iOS.LazyVStack spacing={8}> {rows.map((row) => ( <One.iOS.Text key={row} text={row} /> ))} </One.iOS.LazyVStack> </One.iOS.ScrollView> )}| LazyVStack | Type | Description |
|---|---|---|
alignmentLazyVStackAlignment = "center"leading, center, or trailing. | LazyVStackAlignment = "center" | leading, center, or trailing. |
spacingnumberThe gap between children. | number | The gap between children. |
swiftStyleOneNativeStyleSwiftUI modifiers for this view, such as padding, background, and frame. | OneNativeStyle | SwiftUI modifiers for this view, such as padding, background, and frame. |
children (required)ReactNodeThe rows. | ReactNode | The rows. |
| LazyHStack | Type | Description |
|---|---|---|
alignmentLazyHStackAlignment = "center"top, center, bottom, firstTextBaseline, or lastTextBaseline. | LazyHStackAlignment = "center" | top, center, bottom, firstTextBaseline, or lastTextBaseline. |
spacingnumberThe gap between children. | number | The gap between children. |
swiftStyleOneNativeStyleSwiftUI modifiers for this view, such as padding, background, and frame. | OneNativeStyle | SwiftUI modifiers for this view, such as padding, background, and frame. |
children (required)ReactNodeThe columns. | ReactNode | The columns. |
LazyVGrid lays cells out in columns and LazyHGrid in rows. Put a lazy grid
inside a scroll view along its lazy axis. columns or rows is an array of
GridItem values: fixed needs value, flexible accepts minimum and
maximum, and adaptive needs minimum and accepts maximum. Items can also
set spacing and a two-axis alignment. Bounds must be zero or more, and
maximum must be at least minimum.
import { One } from 'one'
function PhotoGrid({ items }: { items: { id: string; title: string }[] }) { return ( <One.iOS.ScrollView style={{ height: 200 }}> <One.iOS.LazyVGrid columns={[{ size: 'fixed', value: 80 }, { size: 'flexible', minimum: 80 }]} spacing={12} > {items.map((item) => ( <One.iOS.Text key={item.id} text={item.title} /> ))} </One.iOS.LazyVGrid> </One.iOS.ScrollView> )}| LazyVGrid | Type | Description |
|---|---|---|
columns (required)readonly GridItem[]One item per column. | readonly GridItem[] | One item per column. |
alignmentGridHorizontalAlignmentleading, center, or trailing. | GridHorizontalAlignment | leading, center, or trailing. |
spacingnumberThe gap between rows. | number | The gap between rows. |
swiftStyleOneNativeStyleSwiftUI modifiers for this view, such as padding, background, and frame. | OneNativeStyle | SwiftUI modifiers for this view, such as padding, background, and frame. |
children (required)ReactNodeThe cells. | ReactNode | The cells. |
| LazyHGrid | Type | Description |
|---|---|---|
rows (required)readonly GridItem[]One item per row. | readonly GridItem[] | One item per row. |
alignmentGridVerticalAlignmenttop, center, bottom, firstTextBaseline, or lastTextBaseline. | GridVerticalAlignment | top, center, bottom, firstTextBaseline, or lastTextBaseline. |
spacingnumberThe gap between columns. | number | The gap between columns. |
swiftStyleOneNativeStyleSwiftUI modifiers for this view, such as padding, background, and frame. | OneNativeStyle | SwiftUI modifiers for this view, such as padding, background, and frame. |
children (required)ReactNodeThe cells. | ReactNode | The cells. |
Grid builds a non-lazy two-dimensional layout. Put cells in GridRow; a
child directly under Grid spans every column, following SwiftUI.
Like ZStack, a standalone Grid reports the height SwiftUI measured to React Native.
import { One } from 'one'
function Specs() { return ( <One.iOS.Grid horizontalSpacing={16} verticalSpacing={8}> <One.iOS.GridRow> <One.iOS.Text text="Name" /> <One.iOS.Text text="Value" /> </One.iOS.GridRow> <One.iOS.GridRow> <One.iOS.Text text="Width" /> <One.iOS.Text text="80" /> </One.iOS.GridRow> </One.iOS.Grid> )}| Grid | Type | Description |
|---|---|---|
alignmentGridAlignment = "center"Cell alignment, such as topLeading or center. | GridAlignment = "center" | Cell alignment, such as topLeading or center. |
horizontalSpacingnumberThe gap between columns. | number | The gap between columns. |
verticalSpacingnumberThe gap between rows. | number | The gap between rows. |
swiftStyleOneNativeStyleSwiftUI modifiers for this view, such as padding, background, and frame. | OneNativeStyle | SwiftUI modifiers for this view, such as padding, background, and frame. |
children (required)ReactNodeGridRow children and full-width views. | ReactNode | GridRow children and full-width views. |
| GridRow | Type | Description |
|---|---|---|
alignmentGridVerticalAlignmentVertical alignment of the row's cells. | GridVerticalAlignment | Vertical alignment of the row's cells. |
swiftStyleOneNativeStyleSwiftUI modifiers for this view, such as padding, background, and frame. | OneNativeStyle | SwiftUI modifiers for this view, such as padding, background, and frame. |
children (required)ReactNodeOne cell per column. | ReactNode | One cell per column. |
As in SwiftUI, a modifier on a GridRow turns the row into a single cell that
spans every column, so style the cells instead unless that is what you want.
| iOS | Android | Web | |
|---|---|---|---|
| Renders | SwiftUI lists | Throws | Throws |
| Sizing | Fills its box | None | None |
| Selection | Native tags | None | None |
| Search | Native field | None | None |
Form, List, ScrollView, and
ScrollViewReader fill the space they are given; nesting one inside a stack
throws.selection array and the
native proposal are both checked; anything else throws.scrollTo cannot run while the reader builds. Call it from an event
handler or an effect. A call from a mount effect runs when the reader
appears; a call after unmount, a non-string ID, or an invalid anchor throws,
as does a non-function child.fixed needs value, adaptive needs
minimum, every bound must be zero or more, and maximum must be at least
minimum.Edit this page on GitHub.