PythonIDE AppUI
June 19, 2026 · View on GitHub
Execution rulebook for native AppUI MiniApps. Pair with pythonide-native whenever the workflow touches iOS permissions, sensors, media capture, networking side effects, or secure storage.
Read Before Coding
- appui quickstart
- AppUI module
- UI patterns
- AppUI schema and appui.pyi for signatures
- llms-full.txt when unsure about exported symbols
- Load
pythonide-nativebefore importing anyphotos,location,permission,notification, etc.
Chinese module pages on pythonide.xin are valid deep references for scenarios and examples.
Workflow
- Confirm AppUI is the right runtime (
pythoniderouter). Do not use AppUI for widgets or frame-loop games. - Choose structure first:
Form,List,TabView,NavigationStack, dashboardScrollView. - Define module-level
appui.StateorReactiveState. - Write named zero-argument callbacks; callbacks mutate state or call native APIs.
- Keep
body()pure: return a view tree only. No permissions, network, storage writes, notifications, timers started, or navigation side effects insidebody(). - End with exactly one
appui.run(body, state=state, presentation=...). - For editable business data, use
storage.get_json(..., default)only for small settings; usedatabasefor queryable records, caches, favorites, history, playlists, or large lists.
Entry Pattern
import appui
state = appui.State(count=0, status="Ready")
def increment():
state.count += 1
state.status = "Updated"
def body():
return appui.NavigationStack(
appui.Form([
appui.Section("Counter", [
appui.LabeledContent("Value", value=str(state.count)),
appui.Button("+1", action=increment).button_style("bordered_prominent"),
], footer=state.status),
]).navigation_title("Counter")
)
appui.run(body, state=state, presentation="fullscreen_with_close")
Presentation
| Use case | presentation |
|---|---|
| Full app / tool with close affordance | fullscreen_with_close |
| Lightweight utility | sheet |
| Default when unsure for a real app | fullscreen_with_close |
Structure Rules
- Settings, account, checkout, profile:
Form + Section - Dynamic rows:
List + ForEachwith stablekey - Master-detail:
NavigationStack + NavigationLink - Product areas:
TabView + Tab, each tab owns aNavigationStack - Continuing bottom tasks:
TabView + .tab_view_bottom_accessory(...) + .sheet(...) - Use
.tab_view_bottom_accessoryfor the compact persistent row and.sheet(detents=..., drag_indicator=...)for the full panel. Do not invent an expandable bottom accessory API. - Search: native
.searchable(...); do not hand-roll search bars - Toolbar actions in fullscreen apps;
fullscreen_with_closealready provides close - Stable item IDs; mutate by id, never by filtered index
- Do not nest
ListinsideScrollView,Section, or another scrolling container
Layout And Styling
- Target iPhone width ~390 pt; avoid fixed widths above 360 pt
- Padding usually 8–20 pt
- Use semantic system colors:
systemBackground,label,secondaryLabel,systemBlue, etc. - Do not hard-code hex/RGB unless the API requires it
- Ordinary rows/settings/todos should stay
List/Formnative-first; reserve custom dashboard layouts for true dashboard/media surfaces - Use
.tab_view_bottom_accessoryplus.sheetfor persistent playback, podcast, recording, download, timer, or navigation state instead of making that ongoing task a dedicated tab
Native Integration
Also load pythonide-native for any capability below.
Hard Rules
nativeCallsStayOutOfAppUIBody: permission prompts, GPS, capture, notifications, and network side effects belong in named callbacks,.refreshable, or.task— never inbody()- Prefer AppUI bridge components before imperative modules when the control is inline in the UI
- AppUI embedded video:
PlayerController+VideoPlayer; do not useimport avplayerunless the user explicitly wants script-level playback - Secrets →
keychain; small settings →storage; queryable records →database - Do not import CPython
sqlite3directly in MiniApps; usedatabase
AppUI Bridge Components
| Bridge | AppUI component | Related modules | Doc |
|---|---|---|---|
camera_picker | appui.CameraPicker | photos | camera-module |
file_importer | appui.FileImporter | - | file-picker-module |
map_view | appui.MapView | location, permission | location-module |
photo_picker | appui.PhotoPicker | photos | photos-module |
player_controller | appui.PlayerController | avplayer | appui-ref-media |
share_link | appui.ShareLink | - | share-module |
video_player | appui.VideoPlayer | avplayer | appui-ref-media |
web_view | appui.WebView | - | appui-ref-media |
Bridge Vs Module
| Need | Prefer |
|---|---|
| Inline photo pick in a form | appui.PhotoPicker |
| Inline camera capture button | appui.CameraPicker |
| Map preview in a screen | appui.MapView + pythonide-native for location callbacks |
| Share a file or text | appui.ShareLink |
| Batch asset processing, save to library, background fetch | import photos in callback |
| GPS updates, geocoding, heading | import location in callback |
| Local notifications | import notification in callback |
Callbacks And Runtime
- Prefer named zero-argument callbacks
- Wrong:
Button(action=open(item))orrow.on_tap(save(i))— runs during view construction - Row callbacks should capture stable item IDs
- Timers at module scope with
action=at construction; neverTimer(action=None)then assign later - High-frequency sensors, camera streams, waveforms → consider Aurora realtime docs; do not rebuild the whole tree every frame in a tight loop
Hard Stops
- No HTML/WebView unless the user explicitly needs web content
- No
lambdacallbacks in deliverable code - No
children=forVStack/HStack; pass child lists positionally - No
NavigationStack([...]); one root view only - No
ScrollView(VStack(...))for ordinary lists → useList(ForEach(...)) - No dedicated playback/download/recording tab when the expected product pattern is a compact bottom bar that expands into a full panel
- No
Image(...).on_tap(...)as a button → useButton - No guessed SwiftUI-only APIs; verify against schema/stubs
- No tkinter, PyQt, Flask, Streamlit, or browser frameworks
Common Mistakes
| Symptom | Fix |
|---|---|
| Dead button | Named zero-arg callback instead of eager invocation |
| Wrong row after search/filter | Stable ids + ForEach(..., key=...) |
| Permission dialog on launch | Move native call into button callback |
| Blank preview | Ensure body() returns a View and appui.run(...) is present |
| Shared search across tabs | Separate per-tab query state |
Examples
examples/counter/— minimalForm+ persistence-ready structureexamples/photo_notes/— AppUI shell +PhotoPicker+storage
Validation
Done means: runnable AppUI code, documented APIs only, appui.run(...) present, named callbacks for side effects, and honest reporting if editor-side preview cannot be executed.