One Logo Pool Ball
wwwwwwwwwwwwwwwwwww

iOS Lists and Forms

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

One.iOS List in the inset grouped style with section headers and symbol rows

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.

Usage

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.

How it works

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

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>
PropsTypeDescription
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.
OneNativeStyleForm modifiers such as scrollContentBackground.
children (required)ReactNodeSections and rows.
ReactNodeSections and rows.

Section

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>
)
}
PropsTypeDescription
titlestring = ""The header.
string = ""The header.
footerstring = ""Text under the rows.
string = ""Text under the rows.
swiftStyleOneNativeStyleSection modifiers such as headerProminence.
OneNativeStyleSection modifiers such as headerProminence.
children (required)ReactNodeThe rows.
ReactNodeThe rows.

List

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.

PropsTypeDescription
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[]) => voidReceives the native selection proposal.
swiftStyleOneNativeStyleList modifiers, scrollContentBackground, refreshable, and section spacing.
OneNativeStyleList modifiers, scrollContentBackground, refreshable, and section spacing.
children (required)ReactNodeSections and rows.
ReactNodeSections and rows.

Search and refresh

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.

Row modifiers

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.

Row background

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

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.

PropsTypeDescription
axesScrollViewAxes = "vertical"vertical, horizontal, or both.
ScrollViewAxes = "vertical"vertical, horizontal, or both.
showsIndicatorsboolean = trueShows the scroll indicators.
boolean = trueShows the scroll indicators.
swiftStyleOneNativeStyleScroll modifiers and refreshable.
OneNativeStyleScroll modifiers and refreshable.
children (required)ReactNodeThe content.
ReactNodeThe content.

ScrollViewReader

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.

PropsTypeDescription
swiftStyleOneNativeStyleSwiftUI modifiers for this view, such as padding, background, and frame.
OneNativeStyleSwiftUI 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) => ReactNodeThe content closure, run with the native reader proxy.
ScrollViewProxyTypeDescription
scrollTo(id: string, anchor?: UnitPoint | null) => voidScrolls to the view whose swiftStyle.id is id.
(id: string, anchor?: UnitPoint | null) => voidScrolls to the view whose swiftStyle.id is id.

OutlineGroup

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.

PropsTypeDescription
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 ItemThe property holding each item child items, or null for a leaf.
swiftStyleOneNativeStyleSwiftUI modifiers for this view, such as padding, background, and frame.
OneNativeStyleSwiftUI modifiers for this view, such as padding, background, and frame.
children (required)(item: Item) => ReactNodeThe row content for one item.
(item: Item) => ReactNodeThe row content for one item.

Table

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.

PropsTypeDescription
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 | nullThe 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) => voidCalled 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 = 0Bump to take the sort prop over a header tap you did not accept.
swiftStyleOneNativeStyleSwiftUI modifiers for this view, such as padding, background, and frame.
OneNativeStyleSwiftUI 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) => ReactNodeThe cell content for one row and column.

LazyVStack, LazyHStack

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>
)
}
LazyVStackTypeDescription
alignmentLazyVStackAlignment = "center"leading, center, or trailing.
LazyVStackAlignment = "center"leading, center, or trailing.
spacingnumberThe gap between children.
numberThe gap between children.
swiftStyleOneNativeStyleSwiftUI modifiers for this view, such as padding, background, and frame.
OneNativeStyleSwiftUI modifiers for this view, such as padding, background, and frame.
children (required)ReactNodeThe rows.
ReactNodeThe rows.
LazyHStackTypeDescription
alignmentLazyHStackAlignment = "center"top, center, bottom, firstTextBaseline, or lastTextBaseline.
LazyHStackAlignment = "center"top, center, bottom, firstTextBaseline, or lastTextBaseline.
spacingnumberThe gap between children.
numberThe gap between children.
swiftStyleOneNativeStyleSwiftUI modifiers for this view, such as padding, background, and frame.
OneNativeStyleSwiftUI modifiers for this view, such as padding, background, and frame.
children (required)ReactNodeThe columns.
ReactNodeThe columns.

Grids

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>
)
}
LazyVGridTypeDescription
columns (required)readonly GridItem[]One item per column.
readonly GridItem[]One item per column.
alignmentGridHorizontalAlignmentleading, center, or trailing.
GridHorizontalAlignmentleading, center, or trailing.
spacingnumberThe gap between rows.
numberThe gap between rows.
swiftStyleOneNativeStyleSwiftUI modifiers for this view, such as padding, background, and frame.
OneNativeStyleSwiftUI modifiers for this view, such as padding, background, and frame.
children (required)ReactNodeThe cells.
ReactNodeThe cells.
LazyHGridTypeDescription
rows (required)readonly GridItem[]One item per row.
readonly GridItem[]One item per row.
alignmentGridVerticalAlignmenttop, center, bottom, firstTextBaseline, or lastTextBaseline.
GridVerticalAlignmenttop, center, bottom, firstTextBaseline, or lastTextBaseline.
spacingnumberThe gap between columns.
numberThe gap between columns.
swiftStyleOneNativeStyleSwiftUI modifiers for this view, such as padding, background, and frame.
OneNativeStyleSwiftUI modifiers for this view, such as padding, background, and frame.
children (required)ReactNodeThe cells.
ReactNodeThe 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>
)
}
GridTypeDescription
alignmentGridAlignment = "center"Cell alignment, such as topLeading or center.
GridAlignment = "center"Cell alignment, such as topLeading or center.
horizontalSpacingnumberThe gap between columns.
numberThe gap between columns.
verticalSpacingnumberThe gap between rows.
numberThe gap between rows.
swiftStyleOneNativeStyleSwiftUI modifiers for this view, such as padding, background, and frame.
OneNativeStyleSwiftUI modifiers for this view, such as padding, background, and frame.
children (required)ReactNodeGridRow children and full-width views.
ReactNodeGridRow children and full-width views.
GridRowTypeDescription
alignmentGridVerticalAlignmentVertical alignment of the row's cells.
GridVerticalAlignmentVertical alignment of the row's cells.
swiftStyleOneNativeStyleSwiftUI modifiers for this view, such as padding, background, and frame.
OneNativeStyleSwiftUI modifiers for this view, such as padding, background, and frame.
children (required)ReactNodeOne cell per column.
ReactNodeOne 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.

Platform differences

iOSAndroidWeb
RendersSwiftUI listsThrowsThrows
SizingFills its boxNoneNone
SelectionNative tagsNoneNone
SearchNative fieldNoneNone

Limits and gotchas

Edit this page on GitHub.