交互与回调
按钮、输入、on_change、sheet 和行级回调的可靠写法。
触摸、长按、拖拽与捏合等交互通过视图修饰符绑定到 Python 回调。键盘与焦点用 .focused 与 .keyboard_dismiss 控制;上下文菜单用 .context_menu;需要与系统手势共存时用 .simultaneous_gesture 或 .high_priority_gesture。
#预期效果
示例会展示点击、长按、焦点、键盘、拖拽和行级动作的反馈路径。
#概念速览
| 能力 | API |
|---|---|
| 点击 / 长按 | .on_tap(action)、.on_long_press(...) |
| 拖移 / 捏合 / 旋转 | .on_drag、.on_magnification、.on_rotation |
| 组合手势 | .simultaneous_gesture(...)、.high_priority_gesture(...) |
| 菜单 | .context_menu(content=[...]) |
| 焦点与键盘 | .focused(...)、.keyboard_dismiss(mode) |
| 禁用 | .disabled(value) |
| 气泡菜单 | .popover(is_presented, content, on_dismiss) |
| 搜索和刷新 | .searchable(...)、.refreshable(...) |
| 列表动作 | .swipe_actions(...) |
#手势优先级
- 普通子视图手势:默认由子控件(如
Button)消费点击。 simultaneous_gesture:与视图自身手势并行,适合在可滚动区域附加轻量检测。high_priority_gesture:优先于子视图;仅在确有需要时使用,否则容易导致按钮点不动。
context_menu 的 content 是 Button 视图列表,不要用任意字符串代替 Button。
#基础示例
点击计数、长按提示、context_menu 弹出操作。示例使用命名回调,预览里每个动作都有状态反馈。
已复制
import appui
state = appui.State(count=0, hint="")
def add_count():
state.count += 1
def reset_count():
state.batch_update(count=0, hint="已归零")
def double_count():
state.batch_update(count=state.count * 2, hint="已加倍")
def show_long_press_hint():
state.hint = "长按已触发"
def body():
block = (
appui.RoundedRectangle(corner_radius=16)
.fill("systemBlue")
.frame(width=140, height=90)
.on_tap(add_count)
.on_long_press(action=show_long_press_hint, min_duration=0.4)
.context_menu(content=[
appui.Button("归零", action=reset_count),
appui.Button("加倍", action=double_count),
])
)
return appui.NavigationStack(
appui.ScrollView(
appui.VStack([
appui.Text(f"计数: {state.count}").font("title"),
appui.Text(state.hint or "点击或长按蓝色块")
.font("footnote")
.foreground_color("secondaryLabel"),
block,
], spacing=20).padding()
)
.keyboard_dismiss("interactive")
.navigation_title("交互示例")
)
appui.run(body, state=state)
#焦点、键盘和 Popover
focused(key=..., equals=...) 适合多个输入字段之间切换焦点。keyboard_dismiss("interactive") 通常加在 ScrollView 或列表外层。
已复制
import appui
state = appui.State(
active_field="name",
name="Ada",
city="上海",
show_pop=False,
lock_inputs=False,
gesture_log="",
)
def set_name(value):
state.name = value
def set_city(value):
state.city = value
def focus_name():
state.active_field = "name"
def focus_city():
state.active_field = "city"
def set_lock_inputs(value):
state.lock_inputs = bool(value)
def open_popover():
state.show_pop = True
def close_popover():
state.show_pop = False
def log_long_press():
state.gesture_log = "叠加长按"
def popover_content():
return appui.VStack([
appui.Text("Popover / Sheet 内容").font("headline"),
appui.Button("关闭", action=close_popover),
], spacing=12).padding()
def body():
popover_trigger = (
appui.Text("点我打开 Popover")
.padding()
.background("systemGray5", corner_radius=8)
.popover(is_presented=state.show_pop, on_dismiss=close_popover, content=popover_content)
.on_tap(open_popover)
.simultaneous_gesture(
gesture="long_press",
callback=log_long_press,
min_duration=0.35,
)
.disabled(state.lock_inputs)
)
return appui.NavigationStack(
appui.ScrollView(
appui.VStack([
appui.TextField("姓名", text=state.name, on_change=set_name)
.text_field_style("rounded_border")
.focused(key="name", equals=state.active_field)
.on_submit(focus_city),
appui.TextField("城市", text=state.city, on_change=set_city)
.text_field_style("rounded_border")
.focused(key="city", equals=state.active_field),
appui.HStack([
appui.Button("编辑姓名", action=focus_name).button_style("bordered"),
appui.Button("编辑城市", action=focus_city).button_style("bordered"),
], spacing=12),
appui.Toggle("锁定输入", is_on=state.lock_inputs, on_change=set_lock_inputs),
appui.Text(state.gesture_log or "在灰色区域长按可触发 simultaneous_gesture")
.font("caption")
.foreground_color("secondaryLabel"),
popover_trigger,
], spacing=16).padding()
)
.keyboard_dismiss("interactive")
.navigation_title("焦点与手势")
)
appui.run(body, state=state)
#拖拽、缩放和旋转
on_drag、on_magnification、on_rotation 的回调由运行时传入测量值或字典。实际项目里先把返回值显示出来,再按需要解析字段。
已复制
def drag_changed(value):
state.gesture_log = str(value)
def drag_ended(value):
state.gesture_log = f"ended: {value}"
view.on_drag(on_changed=drag_changed, on_ended=drag_ended)
如果子控件本身需要点击,谨慎使用 high_priority_gesture,它会优先于子视图手势。
#行级动作和刷新
列表行的交互要绑定稳定 id,不要依赖过滤后的下标。下拉刷新使用 .refreshable(action=...)。
已复制
import appui
state = appui.State(
query="",
selected="None",
refresh_count=0,
rows=[
{"id": "api", "title": "API reference", "done": False},
{"id": "ui", "title": "Preview UI", "done": True},
{"id": "docs", "title": "Docs guide", "done": False},
],
)
def row_key(row):
return row["id"]
def set_query(value):
state.query = value
def filtered_rows():
keyword = state.query.strip().lower()
if not keyword:
return state.rows
return [row for row in state.rows if keyword in row["title"].lower()]
def refresh_rows():
state.refresh_count += 1
def select_row(row_id):
for row in state.rows:
if row["id"] == row_id:
state.selected = row["title"]
break
def toggle_row(row_id):
state.rows = [
{**row, "done": not row["done"]} if row["id"] == row_id else row
for row in state.rows
]
def row_view(row):
def select_current():
select_row(row["id"])
def toggle_current():
toggle_row(row["id"])
icon = "checkmark.circle.fill" if row["done"] else "circle"
return (
appui.Button(
action=select_current,
content=appui.HStack([
appui.Label(row["title"], system_image=icon),
appui.Spacer(),
appui.Text("Open").foreground_color("secondaryLabel"),
]),
)
.button_style("plain")
.swipe_actions(actions=[
appui.Button("Toggle", action=toggle_current),
])
)
def body():
return appui.NavigationStack(
appui.List([
appui.Section("Rows", [
appui.ForEach(filtered_rows(), row_builder=row_view, key=row_key)
]),
appui.Section("State", [
appui.LabeledContent("Selected", value=state.selected),
appui.LabeledContent("Refresh count", value=str(state.refresh_count)),
]),
])
.searchable(text=state.query, on_change=set_query)
.refreshable(action=refresh_rows)
.navigation_title("Callbacks")
)
appui.run(body, state=state)
#常见误区
on_long_press与context_menu都使用长按语义;同时启用时要实测是否冲突。.focused(key=..., equals=...)的equals应绑定到State中的当前字段名。simultaneous_gesture的gesture常用'tap'、'long_press'、'magnification'。high_priority_gesture过度使用会导致子按钮点击失效。keyboard_dismiss通常加在ScrollView或列表外层。- 行级按钮要操作稳定 id,不要操作过滤后的下标。