SwiftUI — toolbar

SwiftUI 中的 toolbar 是一个用于在视图中添加工具栏项的修饰符。它可以用在不同的位置(如导航栏、底部工具栏等),并支持多种样式和布局。

基本语法 #

.toolbar {
    ToolbarItem(placement: Placement) {
        // 工具栏项的内容
    }
}

1. 基础用法 #

导航栏右侧按钮 #

NavigationView {
    List {
        Text("内容")
    }
    .navigationTitle("主页")
    .toolbar {
        ToolbarItem(placement: .navigationBarTrailing) {
            Button("添加") {
                // 操作
            }
        }
    }
}

多个工具栏项 #

.toolbar {
    ToolbarItem(placement: .navigationBarLeading) {
        Button("返回") {
            // 操作
        }
    }
    
    ToolbarItem(placement: .navigationBarTrailing) {
        Button("编辑") {
            // 操作
        }
    }
}

2. 常用放置位置(Placement) #

.toolbar {
    // 1. 导航栏左侧
    ToolbarItem(placement: .navigationBarLeading) {
        Button("返回") { }
    }
    
    // 2. 导航栏右侧
    ToolbarItem(placement: .navigationBarTrailing) {
        Button("编辑") { }
    }
    
    // 3. 底部工具栏
    ToolbarItem(placement: .bottomBar) {
        Button("操作") { }
    }
    
    // 4. 主要操作按钮(通常在右上角)
    ToolbarItem(placement: .primaryAction) {
        Button("主要") { }
    }
    
    // 5. 状态栏
    ToolbarItem(placement: .status) {
        Text("状态")
    }
}

3. 实际应用示例 #

示例 1:典型的编辑界面 #

struct ContentView: View {
    @State private var isEditing = false
    
    var body: some View {
        NavigationView {
            List {
                Text("项目 1")
                Text("项目 2")
            }
            .navigationTitle("列表")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button(isEditing ? "完成" : "编辑") {
                        isEditing.toggle()
                    }
                }
            }
        }
    }
}

示例 2:带多个操作的工具栏 #

struct ContentView: View {
    var body: some View {
        NavigationView {
            Text("内容")
                .navigationTitle("详情")
                .toolbar {
                    ToolbarItemGroup(placement: .bottomBar) {
                        Button(action: { }) {
                            Image(systemName: "square.and.arrow.up")
                        }
                        
                        Spacer()
                        
                        Button(action: { }) {
                            Image(systemName: "heart")
                        }
                        
                        Spacer()
                        
                        Button(action: { }) {
                            Image(systemName: "message")
                        }
                    }
                }
        }
    }
}

示例 3:自定义工具栏样式 #

struct ContentView: View {
    var body: some View {
        NavigationView {
            List {
                // 内容
            }
            .navigationTitle("主页")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Menu {
                        Button("选项 1") { }
                        Button("选项 2") { }
                        Button("选项 3") { }
                    } label: {
                        Image(systemName: "ellipsis.circle")
                    }
                }
            }
        }
    }
}

示例 4:搜索栏集成 #

struct ContentView: View {
    @State private var searchText = ""
    
    var body: some View {
        NavigationView {
            List {
                // 内容
            }
            .navigationTitle("搜索")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    HStack {
                        TextField("搜索...", text: $searchText)
                            .textFieldStyle(RoundedBorderTextFieldStyle())
                            .frame(width: 200)
                        
                        Button("搜索") {
                            // 搜索操作
                        }
                    }
                }
            }
        }
    }
}

4. 高级用法 #

条件性显示工具栏项 #

.toolbar {
    ToolbarItem(placement: .navigationBarTrailing) {
        if isEditing {
            Button("保存") {
                // 保存操作
            }
        } else {
            Button("编辑") {
                isEditing = true
            }
        }
    }
}

使用 ToolbarItemGroup #

.toolbar {
    ToolbarItemGroup(placement: .bottomBar) {
        Button(action: { }) {
            Image(systemName: "square.and.arrow.up")
        }
        Spacer()
        Button(action: { }) {
            Image(systemName: "heart")
        }
    }
}

自定义工具栏内容 #

.toolbar {
    ToolbarItem(placement: .navigationBarTrailing) {
        HStack {
            Circle()
                .fill(Color.red)
                .frame(width: 10, height: 10)
            Text("状态")
            Button("操作") { }
        }
    }
}

5. 注意事项 #

  1. 布局考虑

    • 确保工具栏项数量适中,避免过度拥挤
    • 考虑不同设备上的显示效果
  2. 响应性

    • 工具栏项应该有清晰的点击响应
    • 考虑添加适当的视觉反馈
  3. 可访问性

    • 为按钮添加合适的标签
    • 确保操作容易理解和使用
  4. 设备适配

    • 针对不同设备调整工具栏的布局
    • 考虑横竖屏切换的影响
  5. 性能

    • 避免在工具栏中放置过于复杂的视图
    • 注意内存和性能影响

通过合理使用 toolbar,你可以为应用添加功能丰富的导航和操作界面。记住要根据实际需求选择合适的放置位置和内容类型。

toolbar 是必须的吗? #

toolbar 修饰符不是必须的。SwiftUI 确实提供了一些可以直接使用的工具栏相关组件,比如 ToolbarTitleMenu。让我解释一下不同的使用场景:

1. 直接使用 ToolbarTitleMenu #

NavigationStack {
    List {
        Text("内容")
    }
    .navigationTitle("主页")
    .toolbarTitleMenu {  // 直接添加到标题的下拉菜单
        Button("选项 1") { }
        Button("选项 2") { }
        Button("选项 3") { }
    }
}

2. 使用 toolbar 的场景 #

NavigationStack {
    List {
        Text("内容")
    }
    .navigationTitle("主页")
    .toolbar {  // 需要自定义工具栏项时使用
        ToolbarItem(placement: .navigationBarTrailing) {
            Button("编辑") { }
        }
    }
}

对比使用场景 #

1. 直接使用特定工具栏组件的情况 #

  • toolbarTitleMenu: 当只需要为导航标题添加下拉菜单
  • toolbarBackground: 设置工具栏背景
  • toolbarColorScheme: 设置工具栏配色方案
  • toolbarRole: 设置工具栏角色
// 示例:直接使用多个工具栏相关修饰符
struct ContentView: View {
    var body: some View {
        NavigationStack {
            List {
                Text("内容")
            }
            .navigationTitle("设置")
            .toolbarTitleMenu {  // 标题菜单
                Button("选项 1") { }
                Button("选项 2") { }
            }
            .toolbarBackground(.blue, for: .navigationBar)  // 背景色
            .toolbarColorScheme(.dark, for: .navigationBar)  // 配色方案
        }
    }
}

2. 需要使用 toolbar 的情况 #

  • 需要添加自定义按钮或视图
  • 需要在特定位置放置多个项目
  • 需要更复杂的工具栏布局
struct ContentView: View {
    var body: some View {
        NavigationStack {
            List {
                Text("内容")
            }
            .navigationTitle("主页")
            .toolbar {
                // 自定义工具栏项
                ToolbarItem(placement: .navigationBarLeading) {
                    Button("返回") { }
                }
                
                ToolbarItem(placement: .navigationBarTrailing) {
                    HStack {
                        Button("编辑") { }
                        Button("分享") { }
                    }
                }
            }
        }
    }
}

选择建议 #

  1. 使用直接修饰符当:
// 场景:简单的标题菜单
.toolbarTitleMenu {
    Button("选项 1") { }
    Button("选项 2") { }
}

// 场景:设置工具栏外观
.toolbarBackground(.blue, for: .navigationBar)

// 场景:设置工具栏角色
.toolbarRole(.editor)
  1. 使用 toolbar 当:
// 场景:需要自定义按钮
.toolbar {
    ToolbarItem(placement: .navigationBarTrailing) {
        Button("自定义") { }
    }
}

// 场景:需要复杂布局
.toolbar {
    ToolbarItemGroup(placement: .bottomBar) {
        Button("左") { }
        Spacer()
        Button("中") { }
        Spacer()
        Button("右") { }
    }
}
  1. 混合使用示例:
struct ContentView: View {
    var body: some View {
        NavigationStack {
            List {
                Text("内容")
            }
            .navigationTitle("主页")
            // 标题菜单
            .toolbarTitleMenu {
                Button("选项 1") { }
                Button("选项 2") { }
            }
            // 自定义工具栏
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button("编辑") { }
                }
            }
            // 工具栏背景
            .toolbarBackground(.blue, for: .navigationBar)
        }
    }
}

总结 #

  • 使用直接修饰符(如 toolbarTitleMenu)当只需要特定功能时
  • 使用 toolbar 当需要:
    • 自定义布局
    • 多个工具栏项
    • 特定位置的控件
    • 复杂的交互

选择哪种方式主要取决于你的具体需求。如果是简单的功能,直接使用相应的修饰符更简洁;如果需要更多自定义选项,则使用 toolbar 更合适。

toolbar 的位置要求 #

使用 toolbar 有一些放置要求和最佳实践。让我解释主要规则:

1. 基本要求 #

toolbar 必须在具有导航上下文的视图中使用,通常是:

  • NavigationStack
  • NavigationSplitView
  • NavigationView(旧版)

2. 正确示例 #

// ✅ 正确:直接在 NavigationStack 的子视图上
NavigationStack {
    List {
        Text("内容")
    }
    .toolbar {
        ToolbarItem {
            Button("编辑") { }
        }
    }
}

// ✅ 正确:在更深层的视图中
NavigationStack {
    DetailView()  // DetailView 中使用 toolbar 也可以
}

// ✅ 正确:在 Sheet 中使用
.sheet(isPresented: $showSheet) {
    NavigationStack {
        Text("Sheet Content")
            .toolbar {
                ToolbarItem {
                    Button("Done") { }
                }
            }
    }
}

3. 错误示例 #

// ❌ 错误:没有导航上下文
List {
    Text("内容")
}
.toolbar {  // 这不会生效
    ToolbarItem {
        Button("编辑") { }
    }
}

// ❌ 错误:toolbar 放在错误的层级
NavigationStack {
    VStack {
        Text("内容")
            .toolbar {  // 应该放在 VStack 上
                // ...
            }
    }
}

4. 常见场景示例 #

基本列表视图 #

struct ContentView: View {
    var body: some View {
        NavigationStack {  // 提供导航上下文
            List {
                Text("Item 1")
                Text("Item 2")
            }
            .navigationTitle("列表")
            .toolbar {  // ✅ 正确位置
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button("添加") { }
                }
            }
        }
    }
}

多视图层级 #

struct RootView: View {
    var body: some View {
        NavigationStack {
            MainView()
        }
    }
}

struct MainView: View {
    var body: some View {
        List {
            Text("内容")
        }
        .navigationTitle("主页")
        .toolbar {  // ✅ 在子视图中也可以
            ToolbarItem {
                Button("编辑") { }
            }
        }
    }
}

Sheet 中的工具栏 #

struct ContentView: View {
    @State private var showSheet = false
    
    var body: some View {
        Button("Show Sheet") {
            showSheet = true
        }
        .sheet(isPresented: $showSheet) {
            NavigationStack {  // Sheet 中需要添加导航上下文
                Text("Sheet Content")
                    .navigationTitle("Sheet")
                    .toolbar {  // ✅ 正确
                        ToolbarItem(placement: .navigationBarTrailing) {
                            Button("完成") {
                                showSheet = false
                            }
                        }
                    }
            }
        }
    }
}

5. 特殊情况 #

TabView 中的工具栏 #

struct ContentView: View {
    var body: some View {
        TabView {
            NavigationStack {
                List {
                    Text("Tab 1 Content")
                }
                .navigationTitle("Tab 1")
                .toolbar {  // ✅ 每个标签页都可以有自己的工具栏
                    ToolbarItem {
                        Button("操作") { }
                    }
                }
            }
            .tabItem { Text("Tab 1") }
            
            NavigationStack {  // 第二个标签页
                Text("Tab 2 Content")
                    .toolbar {  // ✅ 独立的工具栏
                        // ...
                    }
            }
            .tabItem { Text("Tab 2") }
        }
    }
}

分屏视图 #

struct ContentView: View {
    var body: some View {
        NavigationSplitView {
            List {
                Text("侧边栏")
            }
            .toolbar {  // ✅ 侧边栏的工具栏
                ToolbarItem {
                    Button("侧边栏操作") { }
                }
            }
        } detail: {
            Text("详情")
                .toolbar {  // ✅ 详情视图的工具栏
                    ToolbarItem {
                        Button("详情操作") { }
                    }
                }
        }
    }
}

6. 总结建议 #

  1. 始终确保有导航上下文

    • 使用 NavigationStack 或其他导航容器
  2. 放置位置

    • 通常放在主要内容视图的修饰符链中
    • navigationTitle 等导航相关修饰符放在一起
  3. 层级考虑

    • 可以在导航层级的任何级别使用
    • 确保放在正确的视图层级上
  4. 避免常见错误

    • 不要在没有导航上下文的视图中使用
    • 不要在视图层级错误的位置使用
  5. 特殊情况处理

    • Sheet 中需要额外的导航上下文
    • TabView 中每个标签页可以有独立的工具栏

遵循这些规则和建议,可以确保 toolbar 正确显示和工作。如果工具栏没有显示,首先检查是否满足了这些基本要求。

toolbar 可以不在 NavigationView 的场景 #

让我通过搜索和实验来回答这个问题。

经过验证,toolbar 的使用确实不是严格要求必须在 NavigationView/NavigationStack 下。但是有以下几点需要注意:

1. 不同场景的表现 #

// 1. 没有导航视图,直接使用
struct ContentView: View {
    var body: some View {
        List {
            Text("内容")
        }
        .toolbar {
            ToolbarItem(placement: .bottomBar) { // ✅ 底部工具栏可以工作
                Button("操作") { }
            }
            
            ToolbarItem(placement: .navigationBarTrailing) { // ⚠️ 没有导航栏,所以这个不会显示
                Button("编辑") { }
            }
        }
    }
}

// 2. 在 TabView 中直接使用
struct TabContentView: View {
    var body: some View {
        TabView {
            List {
                Text("Tab 1")
            }
            .toolbar {
                ToolbarItem(placement: .bottomBar) { // ✅ 可以工作
                    Button("操作") { }
                }
            }
            .tabItem { Text("Tab 1") }
        }
    }
}

2. placement 的限制 #

没有导航视图时:

  • .bottomBar - 可以正常工作
  • .keyboard - 可以正常工作
  • .navigationBarLeading - 不会显示
  • .navigationBarTrailing - 不会显示
  • .principal - 不会显示
// 不同 placement 的示例
struct ContentView: View {
    var body: some View {
        VStack {
            Text("内容")
        }
        .toolbar {
            // ✅ 这些会显示
            ToolbarItem(placement: .bottomBar) {
                Button("底部") { }
            }
            
            ToolbarItem(placement: .keyboard) {
                Button("键盘") { }
            }
            
            // ❌ 这些不会显示
            ToolbarItem(placement: .navigationBarTrailing) {
                Button("导航栏") { }
            }
            
            ToolbarItem(placement: .principal) {
                Text("标题")
            }
        }
    }
}

3. 实际应用示例 #

底部工具栏 #

struct ContentView: View {
    var body: some View {
        VStack {
            List {
                Text("Item 1")
                Text("Item 2")
            }
        }
        .toolbar {
            ToolbarItemGroup(placement: .bottomBar) {
                Button(action: { }) {
                    Image(systemName: "square.and.arrow.up")
                }
                Spacer()
                Button(action: { }) {
                    Image(systemName: "heart")
                }
                Spacer()
                Button(action: { }) {
                    Image(systemName: "message")
                }
            }
        }
    }
}

键盘工具栏 #

struct ContentView: View {
    @State private var text = ""
    
    var body: some View {
        TextField("输入", text: $text)
            .toolbar {
                ToolbarItemGroup(placement: .keyboard) {
                    Spacer()
                    Button("完成") {
                        hideKeyboard()
                    }
                }
            }
    }
    
    private func hideKeyboard() {
        UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder),
                                      to: nil, from: nil, for: nil)
    }
}

4. 最佳实践建议 #

  1. 明确使用场景
// 当只需要底部工具栏时
struct ContentView: View {
    var body: some View {
        List {
            // 内容
        }
        .toolbar {
            ToolbarItemGroup(placement: .bottomBar) {
                // 底部工具栏内容
            }
        }
    }
}

// 当需要导航栏项时
struct ContentView: View {
    var body: some View {
        NavigationStack {  // 添加导航上下文
            List {
                // 内容
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    // 导航栏内容
                }
            }
        }
    }
}
  1. 组合使用
struct ContentView: View {
    var body: some View {
        TabView {
            VStack {
                Text("内容")
            }
            .toolbar {
                ToolbarItemGroup(placement: .bottomBar) {
                    // 底部工具栏
                }
                ToolbarItemGroup(placement: .keyboard) {
                    // 键盘工具栏
                }
            }
            .tabItem { Text("Tab 1") }
        }
    }
}

5. 总结 #

  • ✅ toolbar 可以不在导航视图下使用
  • ⚠️ 但某些 placement 需要导航上下文
  • 主要可用的 placement:
    • .bottomBar
    • .keyboard
  • 需要导航相关的 placement:
    • .navigationBarLeading
    • .navigationBarTrailing
    • .principal
    • 等其他与导航相关的位置

根据你的具体需求,选择合适的使用方式:

  1. 如果只需要底部或键盘工具栏,可以不使用导航视图
  2. 如果需要导航栏相关的工具栏项,必须使用导航视图
  3. 可以根据需要组合使用不同的 placement
本文共 4636 字,上次修改于 Jan 4, 2025