Documentation

Views

The Ui module is Walnut’s view vocabulary: a small set of elements and attributes that map directly onto UIKit. It is always importable; most apps start with import Ui exposing (..).

Two ideas to hold on to:

  1. Everything returns data. column [ spacing 8 ] [ text "hi" ] evaluates to VColumn [Spacing 8] [VText "hi"] — a value you can print, test, and diff. The renderer interprets it.
  2. Every element names its UIKit counterpart. Walnut doesn’t hide the platform; it curates it.

Elements

FunctionTypeRendered as
textString -> View msgUILabel
elList (Attr msg) -> View msg -> View msgcontainer UIView (padding/centering/styling wrapper)
columnList (Attr msg) -> List (View msg) -> View msgvertical UIStackView
rowList (Attr msg) -> List (View msg) -> View msghorizontal UIStackView
buttonList (Attr msg) -> List (View msg) -> View msgUIButton (configuration-based)
textFieldList (Attr msg) -> View msgUITextField
iconList (Attr msg) -> String -> View msgUIImageView + SF Symbol (e.g. icon [] "trash")
imageList (Attr msg) -> String -> View msgUIImageView from asset catalog name (scaleAspectFill)
scrollList (Attr msg) -> List (View msg) -> View msgUIScrollView wrapping a column
tableList (Attr msg) -> List (View msg) -> View msgUITableView (sections as children)
sectionList (Attr msg) -> String -> List (View msg) -> View msgtable section + header title
cellList (Attr msg) -> List (View msg) -> View msgtable row; children are the cell content
collectionList (Attr msg) -> List (View msg) -> View msgUICollectionView (items as children)
itemList (Attr msg) -> List (View msg) -> View msgcollection cell or map marker
mapList (Attr msg) -> List (View msg) -> View msgMKMapView; marker children are items
sheetList (Attr msg) -> msg -> View msg -> View msgsystem sheet (UISheetPresentationController)
navStackList (Attr msg) -> msg -> List (View msg) -> View msgUINavigationController (children = root…top)
tabBarList (Attr msg) -> (Int -> msg) -> List (View msg) -> View msgUITabBarController (children = tab roots)
spacerView msggreedy flexible space inside row/column
nativeString -> List (Attr msg) -> View msgany UIKit class — see UIKit interop
noneView msgrenders nothing (handy in conditionals)

Notes:

  • text takes no attributes. Style text by wrapping it: el [ fontSize 32, bold ] (text "Title"). Buttons style their own label: button [ fontSize 24 ] [ text "+" ].
  • button children: a text and/or an icon become the button’s configuration title/image; anything richer is rendered inside the button.
  • textField is controlled via attributes: content (current text), placeholder, onInput, secure. Fields keep focus across re-renders (they are reused by position, or by key if you provide one).
  • table / section / cell map onto a hosted UITableView. Put section nodes as table children; put cell nodes inside each section. Cell bodies are normal Walnut views (rows, text, icons). Use onTap on a cell for selection; give the table a stable key so scroll position survives re-renders.
  • collection / item map onto a hosted UICollectionView (flow layout). Collection children must be item nodes; item children are the cell content. onTap + key behave like table cells.
  • map hosts MKMapView. Set center with propString / propFloat keys "latitude", "longitude", "span" (degrees). Optional children are item markers with the same lat/lon props, optional content title, and onTap.
  • sheet attrs onDismiss body presents a real system sheet. Swipe/grabber dismiss dispatches onDismiss. Detents: propString "detent" "medium" | "large" | "mediumLarge" (default starts at medium). Bodies with height fill or scroll stretch to the detent; intrinsic columns stay top-aligned. The sheet host insets for the keyboard. Keep custom dimmed cards as overlay / dialog. Todo compose uses sheet; Surfaces has a smoke demo. Sibling overlay / dialog nodes pin onto the topmost presented sheet (not under it). Multiple sheet nodes stack: later sheets present from the earlier sheet VC (Todo’s day/time/list picker sits on compose).
  • navStack attrs onPop pages hosts a UINavigationController. Children are pages from root to top; give each page a stable key. Interactive pop / edge swipe dispatches onPop. Navigation bar is hidden — put your own back control in the page (or rely on the gesture). Textbook multi-tab apps: put tabBar at the root and give each tab its own navStack (Todo, Ledger). Do not wrap a tabBar inside an outer navStack — that fights UIKit containment. Surfaces still demos a lone navStack push.
  • tabBar attrs onSelect tabs hosts a UITabBarController (system insets / Liquid Glass). Children are tab roots (index 0…). Title from each child’s content; icon from propString "systemImage" "house". Drive selection with propInt "selected" i; taps call onSelect i. Prefer each child to be a navStack with those attrs on the stack itself (wrapping navStack in el used to leave an empty tab — the nav never embedded).
  • native "ClassName" hosts any allowlisted UIView subclass by ObjC name (prop* / onNative*). Prefer this when you want to drop curated helpers (switch, button, textField, …). Details: uikit-interop.md.
  • overlay remains for hand-rolled dimmed panels (pickers, custom cards). Prefer sheet when you want system detents and swipe-to-dismiss. When a sheet is up, overlays render on that sheet’s view.

Attributes

Layout

AttributeMeaning
spacing ngap between children of column/row
padding ninner margin on all sides
paddingXY x yhorizontal / vertical inner margins
width fill / width shrink / width (px n)width behavior
height fill / height shrink / height (px n)height behavior
centerX, centerYcenter content on that axis
alignLeft, alignRight, alignTop, alignBottomcross-axis alignment for stack children
safeAreainset layout margins from the safe area guide
scrollBottomInset nextra bottom contentInset on scroll (clear floatBottom chrome)
onRefresh msgpull-to-refresh on scroll (UIRefreshControl); fires msg on pull

Layout model, day 1: column/row are stack views; alignment attributes set the stack’s cross-axis alignment; spacer absorbs main-axis space; px sizes pin exact dimensions; fill lowers hugging so the element takes what’s left. The root view is edge-to-edge by default (so glass chrome can sample the home-indicator region); opt into safeArea on page roots when you want guide insets.

Walnut does not auto-theme for system Dark Mode. Explicit bg / fontColor values are fixed RGB. Host chrome that used adaptive UIKit colors (systemBackground, label, placeholderText) is kept clear / non-adaptive so a cream design system is not painted over black. Apps with a light-only palette should also set UIUserInterfaceStyle to Light (and/or window.overrideUserInterfaceStyle = .light).

Appearance

AttributeMeaning
bg colorbackground color
fontColor colortext color
fontSize nsystem font, size n
fontWeight n100…900 (CSS-style); bold = 700, semibold = 600
textCentercenter-align label text
rounded ncontinuous-curve corner radius
border w colorborder width and color
opacity x0.0 – 1.0
tint colorUIKit tint (buttons, icons)

Liquid Glass

AttributeMeaning
glassglass material (iOS 26 UIGlassEffect / .glass() button; blur fallback earlier)
glassProminentprominent/interactive glass (.prominentGlass() button)

Full story in Liquid Glass.

Events

AttributeTypeFires
onTap msgmsg -> Attr msgbutton tap (touchUpInside) or tap gesture on any element
onInput toMsg(String -> msg) -> Attr msgevery text change in a textField
onNative event msgString -> msg -> Attr msga named UIControl event on a native element

Text field state

AttributeMeaning
content sthe field’s current text (keep it in your model)
placeholder splaceholder text
securepassword entry
key sstable identity across renders (also useful for future keyed diffing)

Colors

Color values come from:

rgb 1.0 0.5 0.0          -- components 0.0–1.0
rgba 0.0 0.0 0.0 0.5     -- with alpha
rgb255 255 149 0         -- 8-bit components

plus named system-ish colors: white black clear gray lightGray darkGray blue green red orange yellow purple pink teal indigo.

Putting it together

view : Model -> View Msg
view model =
    scroll [ padding 20, spacing 14 ]
        [ el [ fontSize 34, bold ] (text "Settings")
        , column [ spacing 1, rounded 16, glass ]
            [ settingRow "airplane" "Airplane Mode" model.airplane ToggleAirplane
            , settingRow "wifi" "Wi-Fi" model.wifi ToggleWifi
            ]
        , button [ onTap SignOut, glassProminent, tint red, centerX ]
            [ text "Sign Out" ]
        ]

settingRow : String -> String -> Bool -> Msg -> View Msg
settingRow symbol label isOn msg =
    row [ padding 14, spacing 12 ]
        [ icon [ tint blue ] symbol
        , text label
        , spacer
        , native "UISwitch"
            [ propBool "on" isOn
            , onNative "valueChanged" msg
            ]
        ]

Conditional rendering

Views are expressions, so use the language:

column [ spacing 8 ]
    [ if model.hasError then
        el [ fontColor red ] (text model.errorMessage)
      else
        none
    , viewList model.items
    ]

Testing views

Because views are data, tests assert on structure without UIKit:

view { count = 3 }
--> VColumn [CenterX,CenterY,Spacing 16] [VText "3", VRow ...]

The Swift test suite does exactly this against whole programs (the toolchain), and the same property will power snapshot tooling later.