堆叠工具栏

学习如何在使用 Expo Router 的堆栈导航中使用 iOS 工具栏。


重要 Stack.Toolbar 是一个仅在 iOS 上可用的 alpha API,自 Expo SDK 55 及更高版本支持。该 API 可能会有重大修改。

Stack.Toolbar 允许你在 Stack 屏幕上添加原生 iOS 工具栏项目。你可以在顶部栏(左侧或右侧)或底部工具栏区域放置按钮、菜单和自定义视图。

添加头部按钮

🌐 Adding header buttons

Stack.Toolbar 中使用 Stack.Toolbar.Button 并搭配 placement="right"placement="left",即可在导航头添加按钮。这对于执行收藏、分享或编辑内容的操作非常有用。

🌐 Use Stack.Toolbar.Button within Stack.Toolbar with placement="right" or placement="left" to add buttons to the navigation header. This is useful for actions like favoriting, sharing, or editing content.

app/notes/[id].tsx
import { useState } from 'react'; import { Stack } from 'expo-router'; import { View, Text, Alert } from 'react-native'; export default function NoteScreen() { const [isFavorite, setIsFavorite] = useState(false); return ( <> <Stack.Toolbar placement="right"> <Stack.Toolbar.Button icon={isFavorite ? 'star.fill' : 'star'} onPress={() => setIsFavorite(!isFavorite)} /> <Stack.Toolbar.Button icon="square.and.arrow.up" onPress={() => Alert.alert('Share')} /> </Stack.Toolbar> <Stack.Toolbar placement="left"> <Stack.Toolbar.Button icon="sidebar.left" onPress={() => Alert.alert('Sidebar')} /> </Stack.Toolbar> <View style={{ flex: 1, padding: 16 }}> <Text>Note content...</Text> </View> </> ); }

图标

🌐 Icons

工具栏按钮支持两种类型的图标:SF 符号和自定义图片。

🌐 Toolbar buttons support two types of icons: SF Symbols and custom images.

SF 符号

🌐 SF Symbols

添加图标最简单的方法是使用 SF Symbols,苹果内置的图标库。将符号名称直接传递给 icon 属性即可:

🌐 The easiest way to add icons is using SF Symbols, Apple's built-in icon library. Pass the symbol name directly to the icon prop:

<Stack.Toolbar.Button icon="star.fill" onPress={() => {}} /> <Stack.Toolbar.Button icon="square.and.arrow.up" onPress={() => {}} /> <Stack.Toolbar.Menu icon="ellipsis.circle">{/* ... */}</Stack.Toolbar.Menu>

你可以在 Apple 的 SF Symbols 应用中浏览可用的符号。

🌐 You can browse available symbols in Apple's SF Symbols app.

自定义图片

🌐 Custom images

你也可以使用自定义图片。在头部工具栏(placement="left"placement="right")中,直接将图片源传递给 icon 属性:

🌐 You can also use custom images. In header toolbars (placement="left" or placement="right"), pass an image source directly to the icon prop:

import { Stack } from 'expo-router'; export default function Page() { return ( <> <Stack.Toolbar placement="right"> <Stack.Toolbar.Button icon={require('./assets/expo.png')} onPress={() => {}} /> </Stack.Toolbar> {/* Screen content */} </> ); }

在底部工具栏中,使用来自 expo-imageuseImage Hook,并将结果传递给 image 属性:

🌐 In the bottom toolbar, use the useImage hook from expo-image and pass the result to the image prop:

import { Stack } from 'expo-router'; import { useImage } from 'expo-image'; export default function Page() { const customIcon = useImage('https://simpleicons.org/icons/expo.svg', { maxWidth: 24, maxHeight: 24, }); return ( <> <Stack.Toolbar> <Stack.Toolbar.Button image={customIcon} onPress={() => {}} /> </Stack.Toolbar> {/* Screen content */} </> ); }

信息 用于底部工具栏自定义图片的 useImageimage 属性模式是临时 API,未来版本可能会发生变化。

构建操作菜单

🌐 Building action menus

对于具有多个操作的屏幕,请使用 Stack.Toolbar.Menu 将它们分组到下拉菜单中:

🌐 For screens with multiple actions, use Stack.Toolbar.Menu to group them into a dropdown menu:

app/mail/[id].tsx
import { useState } from 'react'; import { Stack } from 'expo-router'; import { Alert } from 'react-native'; export default function EmailScreen() { const [isArchived, setIsArchived] = useState(false); return ( <> <Stack.Toolbar placement="right"> <Stack.Toolbar.Menu icon="ellipsis.circle"> <Stack.Toolbar.MenuAction icon="arrowshape.turn.up.left" onPress={() => Alert.alert('Reply')}> Reply </Stack.Toolbar.MenuAction> <Stack.Toolbar.MenuAction icon="arrowshape.turn.up.right" onPress={() => Alert.alert('Forward')}> Forward </Stack.Toolbar.MenuAction> <Stack.Toolbar.MenuAction icon={isArchived ? 'tray.full' : 'archivebox'} isOn={isArchived} onPress={() => setIsArchived(!isArchived)}> {isArchived ? 'Unarchive' : 'Archive'} </Stack.Toolbar.MenuAction> <Stack.Toolbar.MenuAction icon="trash" destructive onPress={() => Alert.alert('Delete')}> Delete </Stack.Toolbar.MenuAction> </Stack.Toolbar.Menu> </Stack.Toolbar> {/* Email content */} </> ); }

Stack.Toolbar.MenuAction 上的 isOn 属性会在操作旁显示勾选标记,对于切换状态很有用。destructive 属性将操作样式设置为红色,以表示危险操作。

🌐 The isOn prop on Stack.Toolbar.MenuAction shows a checkmark next to the action, useful for toggle states. The destructive prop styles the action in red to indicate a dangerous operation.

嵌套子菜单

🌐 Nested submenus

对于更复杂的菜单,可以将 Stack.Toolbar.Menu 嵌套在另一个菜单中。使用 inline 属性可以直接显示子菜单项,而不折叠:

🌐 For more complex menus, nest Stack.Toolbar.Menu inside another menu. Use the inline prop to display submenu items directly without collapsing:

import { useState } from 'react'; import { Stack } from 'expo-router'; export default function EmailScreen() { const [sortBy, setSortBy] = useState<'name' | 'date' | 'size'>('name'); const [showHiddenFiles, setShowHiddenFiles] = useState(false); return ( <> <Stack.Toolbar> <Stack.Toolbar.Menu icon="ellipsis.circle"> {/* Inline submenu - options appear directly in the menu */} <Stack.Toolbar.Menu inline title="Sort By"> <Stack.Toolbar.MenuAction isOn={sortBy === 'name'} onPress={() => setSortBy('name')}> Name </Stack.Toolbar.MenuAction> <Stack.Toolbar.MenuAction isOn={sortBy === 'date'} onPress={() => setSortBy('date')}> Date </Stack.Toolbar.MenuAction> <Stack.Toolbar.MenuAction isOn={sortBy === 'size'} onPress={() => setSortBy('size')}> Size </Stack.Toolbar.MenuAction> </Stack.Toolbar.Menu> {/* Nested submenu - opens as a separate menu */} <Stack.Toolbar.Menu title="Preferences"> <Stack.Toolbar.MenuAction isOn={showHiddenFiles} onPress={() => setShowHiddenFiles(!showHiddenFiles)}> Show Hidden Files </Stack.Toolbar.MenuAction> </Stack.Toolbar.Menu> </Stack.Toolbar.Menu> </Stack.Toolbar> {/* Email content */} </> ); }

使用底部工具栏

🌐 Using the bottom toolbar

iOS 应用通常在底部有一个用于主要操作的工具栏。要添加一个,可以使用 Stack.Toolbar,不需要指定 placement 属性(它默认为 "bottom"):

🌐 iOS apps commonly have a bottom toolbar for primary actions. To add one, use Stack.Toolbar without a placement prop (it defaults to "bottom"):

app/photos/index.tsx
import { Stack } from 'expo-router'; import { Alert } from 'react-native'; export default function PhotosScreen() { return ( <> <Stack.Toolbar> <Stack.Toolbar.Button icon="photo.on.rectangle" onPress={() => Alert.alert('Select')}> Select </Stack.Toolbar.Button> <Stack.Toolbar.Spacer /> <Stack.Toolbar.Button icon="plus" onPress={() => Alert.alert('Add')}> Add </Stack.Toolbar.Button> </Stack.Toolbar> </> ); }

Stack.Toolbar.Spacer 在项目之间创建灵活的空间,将它们推到相对的两端。这就是如何实现像在工具栏两端都有按钮的布局的方式。

信息 底部工具栏只能在页面组件内使用,不能在布局文件中使用。

在按钮上添加徽章

🌐 Adding badges to buttons

在标题工具栏中,你可以添加徽章以显示计数或状态。使用 Stack.Toolbar.IconStack.Toolbar.LabelStack.Toolbar.Badge 来组合按钮内容:

🌐 In header toolbars, you can add badges to indicate counts or status. Use Stack.Toolbar.Icon, Stack.Toolbar.Label, and Stack.Toolbar.Badge to compose the button content:

app/inbox.tsx
import { Stack } from 'expo-router'; export default function InboxScreen() { const unreadCount = 5; return ( <> <Stack.Toolbar placement="right"> <Stack.Toolbar.Button onPress={() => {}}> <Stack.Toolbar.Icon sf="bell" /> <Stack.Toolbar.Label>Notifications</Stack.Toolbar.Label> {unreadCount > 0 && <Stack.Toolbar.Badge>{String(unreadCount)}</Stack.Toolbar.Badge>} </Stack.Toolbar.Button> </Stack.Toolbar> {/* Screen content */} </> ); }

信息 徽章仅在标题位置(leftright)有效,底部工具栏不起作用。

嵌入自定义视图

🌐 Embedding custom views

当你需要超越按钮和菜单的功能时,请使用 Stack.Toolbar.View 来嵌入任意 React Native 组件:

🌐 When you need something beyond buttons and menus, use Stack.Toolbar.View to embed any React Native component:

app/search.tsx
import { Stack } from 'expo-router'; import { Pressable, Alert } from 'react-native'; import { SymbolView } from 'expo-symbols'; export default function SearchScreen() { return ( <> <Stack.Toolbar> <Stack.Toolbar.View> <Pressable style={{ width: 32, height: 32, justifyContent: 'center', alignItems: 'center' }} onPress={() => { Alert.alert('Filter pressed'); }}> <SymbolView name="line.3.horizontal.decrease.circle" size={24} /> </Pressable> </Stack.Toolbar.View> </Stack.Toolbar> {/* Screen content */} </> ); }

动态显示和隐藏项目

🌐 Showing and hiding items dynamically

使用 hidden 属性根据状态切换工具栏项:

🌐 Use the hidden prop to toggle toolbar items based on state:

app/document.tsx
import { useState } from 'react'; import { Stack } from 'expo-router'; export default function DocumentScreen() { const [isEditing, setIsEditing] = useState(false); return ( <> <Stack.Toolbar placement="right"> <Stack.Toolbar.Button hidden={isEditing} icon="pencil" onPress={() => setIsEditing(true)} /> <Stack.Toolbar.Button hidden={!isEditing} onPress={() => setIsEditing(false)}> Done </Stack.Toolbar.Button> </Stack.Toolbar> {/* Document content */} </> ); }

常见问题

🌐 Common problems

在 iOS 26 的夜间模式下,液体玻璃工具栏按钮会闪烁

在 iOS 26 的夜间模式下,当在屏幕之间切换时,带有液态玻璃样式的工具栏按钮的背景可能会闪烁或闪现。这是因为 React Navigation 的默认主题与系统夜间模式不匹配,导致液态玻璃渲染出现视觉瑕疵。

🌐 Toolbar buttons with liquid glass styling may flicker or flash their background when navigating between screens in dark mode on iOS 26. This happens because React Navigation's default theme doesn't match the system dark mode, causing visual artifacts in the liquid glass rendering.

要解决此问题,请使用适当的主题,将你的根布局用 @react-navigation/native<ThemeProvider> 封装起来:

🌐 To fix this, wrap your root layout with <ThemeProvider> from @react-navigation/native using the appropriate theme:

app/_layout.tsx
import { ThemeProvider, DarkTheme, DefaultTheme } from '@react-navigation/native'; import { Stack } from 'expo-router'; import { useColorScheme } from 'react-native'; export default function RootLayout() { const colorScheme = useColorScheme(); return ( <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}> <Stack /> </ThemeProvider> ); }
在屏幕之间切换时背景会闪白

屏幕切换之间出现白色闪光通常意味着导航栈使用的是浅色背景,而你的应用使用的是深色主题。当屏幕包含工具栏项目时,这种闪光尤为明显,因为它与工具栏的样式形成了对比。

🌐 A white flash between screen transitions usually means the navigation stack is using a light background while your app uses a dark theme. This is especially noticeable when screens contain toolbar items, as the flash contrasts with the toolbar styling.

要解决此问题,请使用 React Navigation 的 <ThemeProvider> 封装你的根布局,并传入适当的主题:

🌐 To fix this, wrap your root layout with React Navigation's <ThemeProvider> and pass the appropriate theme:

app/_layout.tsx
import { ThemeProvider, DarkTheme, DefaultTheme } from '@react-navigation/native'; import { Stack } from 'expo-router'; import { useColorScheme } from 'react-native'; export default function RootLayout() { const colorScheme = useColorScheme(); return ( <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}> <Stack /> </ThemeProvider> ); }
大标题在滚动时不会收缩

当在 Stack.Toolbar 旁使用 headerLargeTitle: true(或 <Stack.Screen.Title large>)时,大标题在滚动时可能不会收缩。这种情况发生在可滚动视图不是屏幕组件的直接第一个子组件时。

🌐 When using headerLargeTitle: true (or <Stack.Screen.Title large>) alongside Stack.Toolbar, the large title may not collapse on scroll. This happens when the scrollable view is not the direct first child of the screen component.

为了解决此问题,请确保 ScrollViewFlatList 是你的屏幕组件渲染的第一个子元素。如果需要一个封装器,请在其上设置 collapsable={false}

🌐 To fix this, ensure ScrollView or FlatList is the first child rendered by your screen component. If you need a wrapper, set collapsable={false} on it:

app/index.tsx
import { Stack } from 'expo-router'; import { ScrollView, View, Text } from 'react-native'; export default function Home() { return ( <ScrollView> <Stack.Screen.Title large>Home</Stack.Screen.Title> <Text>Content here</Text> </ScrollView> ); }

如果你需要封装 ScrollView,请在封装器上设置 collapsable={false}

🌐 If you need to wrap the ScrollView, set collapsable={false} on the wrapper:

app/index.tsx
import { Stack } from 'expo-router'; import { ScrollView, View, Text } from 'react-native'; export default function Home() { return ( <View collapsable={false}> <ScrollView> <Stack.Screen.Title large>Home</Stack.Screen.Title> <Text>Content here</Text> </ScrollView> </View> ); }

已知的限制

🌐 Known limitations

仅限 iOS

Stack.Toolbar 仅在 iOS 上可用。在 Android 和网页上,该组件不会显示。

仅在页面组件中显示底部工具栏

底部工具栏只能在页面组件中使用,不能在布局文件中使用。这是因为底部工具栏需要与特定屏幕的内容关联。

🌐 The bottom toolbar can only be used inside page components, not in layout files. This is because the bottom toolbar needs to be associated with a specific screen's content.

无法嵌套工具栏

你不能将 Stack.Toolbar 组件嵌套在彼此内部。

🌐 You cannot nest Stack.Toolbar components inside each other.

仅在页眉位置显示徽章

Stack.Toolbar.Badge 仅在使用 placement="left"placement="right" 时受支持。徽章不会显示在底部工具栏中。

了解更多

🌐 Learn more

有关完整的 API 文档,包括所有可用属性,请参见 Stack.Toolbar API 参考

🌐 For complete API documentation, including all available props, see the Stack.Toolbar API reference.