Documentation

UIKit interop

Walnut does not pretend UIKit isn’t there. Interop has three layers:

  1. Platform list — generated names from allowlisted iOS SDK frameworks (uiSwitch, mkMapView, languageModelSession, …)
  2. Curated shortsswitch, spinner (ergonomic aliases)
  3. App natives.json — your renames / custom @objc views
  4. Escape hatchnative "ClassName" for one-offs

Platform list

The toolchain ships a catalog of allowlisted app-facing frameworks (UIKit, HealthKit, PassKit, CloudKit, MapKit, WebKit, FoundationModels, and similar) — not every class in the SDK.

Naming: leading acronym lowercased (UISwitchuiSwitch, HKHealthStorehkHealthStore); plain PascalCase decapitalized (LanguageModelSessionlanguageModelSession).

-- Typed payload (reads UISwitch.isOn → Bool -> msg)
uiSwitch [ propBool "on" model.lit, onNativeBool "valueChanged" SetLit ]
uiActivityIndicatorView [ propBool "animating" True ]

Hosting limit: the view renderer only instantiates UIView subclasses. Domain types (hkHealthStore, ckContainer, …) still typecheck so you can discover them, but they need ports/effects — not native — to be useful.

Curated helpers

switch [ propBool "on" model.lit, onNativeBool "valueChanged" SetLit ]
spinner [ propBool "animating" model.lit, propInt "activityIndicatorViewStyle" 100 ]

App aliases (natives.json)

At the project root (next to walnut.json):

{
  "aliases": {
    "switch2": "UISwitch",
    "beamGauge": "MyBeamGauge"
  }
}

Then switch2 [ … ] is a real helper. A Swift typealias UISwitch2 = UISwitch alone is not enough — NSClassFromString never sees typealiases; the JSON bridge is the source of truth.

Escape hatch: native

native : String -> List (Attr msg) -> View msg

native "UISwitch" [...] instantiates via NSClassFromString, applies KVC prop*, and wires typed onNative* control events:

row [ spacing 12 ]
    [ text "Notifications"
    , spacer
    , native "UISwitch"
        [ propBool "on" model.notificationsEnabled
        , onNativeBool "valueChanged" SetNotifications
        ]
    ]

The same knowledge — class names, property names, control events, straight from Apple’s docs — expresses as data in your view, and the runtime handles allocation, targets, and lifecycle. In imperative UIKit you would write:

let switch = UISwitch()
switch.isOn = true
switch.addTarget(self, action: #selector(toggled), for: .valueChanged)

Properties via KVC

Four typed helpers set properties. Most keys are plain KVC (including nested paths like layer.borderWidth). A few UIKit write paths that are not KVC are special-cased in the host:

KeyControlBehavior
titleUIButtonsetTitle(_:for: .normal) (+ configuration.title when set)
segmentsUISegmentedControlCSV titles → insertSegment(withTitle:at:)
font.pointSizelabel / field / buttonRebuild UIFont with withSize (pointSize is read-only)
propString : String -> String -> Attr msg
propInt    : String -> Int    -> Attr msg
propFloat  : String -> Float  -> Attr msg
propBool   : String -> Bool   -> Attr msg

Examples:

native "UIButton"
    [ propString "title" "Save"
    , onNative "touchUpInside" Save
    ]

native "UISegmentedControl"
    [ propString "segments" "Day,Week,Month"
    , propInt "selectedSegmentIndex" model.tab
    , onNativeInt "valueChanged" "selectedSegmentIndex" SetTab
    ]

native "UILabel"
    [ propString "text" "Hello"
    , propFloat "font.pointSize" 22.0
    ]

native "UIActivityIndicatorView"
    [ propBool "animating" True
    , propInt "activityIndicatorViewStyle" 100   -- .large
    ]

prop* also works on built-in elements when the host applies them — reach past curated attributes on a button, textField, etc. where wired:

textField
    [ onInput EmailChanged
    , propInt "keyboardType" 7          -- .emailAddress
    , propBool "autocorrectionType" False
    ]

Events via target-action

onNative : String -> msg -> Attr msg

Attaches a UIControl target for the named event and dispatches a fixed Msg:

NameUIControl.Event
"touchUpInside".touchUpInside
"touchDown".touchDown
"valueChanged".valueChanged
"editingChanged".editingChanged
"editingDidBegin".editingDidBegin
"editingDidEnd".editingDidEnd
"primaryActionTriggered".primaryActionTriggered

Typed payloads

When you need the control’s current value, use a typed helper. Each reads a value, applies (payload -> msg) via wn_apply1 (same pattern as onInput), then dispatches:

HelperPayloadReadback
onNativeBool event toMsgBoolUISwitch.isOn, else KVC "on" / "isOn"
onNativeInt event keyPath toMsgIntvalue(forKeyPath:)
onNativeFloat event keyPath toMsgFloatvalue(forKeyPath:)
onNativeString event keyPath toMsgStringvalue(forKeyPath:)
onNativePosix event keyPath toMsgInt (ms since epoch)Date / NSDate via keyPath
switch [ propBool "on" model.lit, onNativeBool "valueChanged" SetLit ]

native "UIDatePicker"
    [ propInt "datePickerMode" 1
    , onNativePosix "valueChanged" "date" DatePicked
    ]

onNative (fixed msg, no readback) remains available for pure signals.

Layout integration

A native element participates in Auto Layout like everything else: it lives in its parent stack view and honors width (px n) / height (px n), padding on its container, opacity, rounded, border, bg, and onTap.

el [ padding 16, glass, rounded 20 ]
    (native "UISegmentedControl" [ height (px 32) ])

When to use which

SituationReach for
Labels, stacks, buttons, fields, images, scrolling, tables, curated natives (switch)Ui elements
App-specific class rename / custom @objc view with a stable Walnut namenatives.json alias
A property the curated attributes don’t coverprop* on the built-in element
A whole control Walnut doesn’t wrap yet (one-off)native "UIClassName"
Complex custom views, delegates, data sourcesWrite Swift in the shell app and expose it — or better, open an issue; the goal is to grow Ui deliberately

Safety notes

  • A misspelled class name renders an inline red diagnostic (Unknown UIKit class: UISwtich) instead of crashing.
  • A misspelled property name will raise UIKit’s setValue:forUndefinedKey: exception at render time — KVC is stringly-typed; that’s the price of the hatch. Keep native usage wrapped in small helper functions (switch : Bool -> Msg -> View Msg) so each stringly boundary appears once in your codebase.