You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI实现导航栏内嵌搜索筛选芯片及原生导航布局

SwiftUI实现导航栏内嵌搜索筛选芯片(含原生滚动交互)

直接上解决方案:核心是放弃用.toolbar硬塞内容,而是自定义导航栏下方的头部区域,结合ScrollView的滚动状态模拟原生导航栏的折叠/展开效果,完全贴合App Store、GitHub这类App的布局。

核心思路

  • 用NavigationStack(iOS16+)搭建基础导航框架,设置大标题模式但通过动态调整实现自定义标题效果
  • 在ScrollView顶部放置自定义头部:包含大标题、副标题、搜索框、筛选芯片
  • 通过绑定ScrollView的滚动偏移,动态调整头部的大小、透明度,实现滚动折叠效果
  • 利用scrollDismissesKeyboard控制滚动时键盘的行为,贴合原生体验

完整代码示例

import SwiftUI

// 自定义筛选芯片组件
struct FilterChip: View {
    let title: String
    @Binding var isSelected: Bool
    
    var body: some View {
        Button(action: { isSelected.toggle() }) {
            Text(title)
                .font(.caption)
                .padding(.horizontal, 12)
                .padding(.vertical, 6)
                .background(isSelected ? Color.blue : Color(UIColor.systemGray5))
                .foregroundColor(isSelected ? .white : .primary)
                .cornerRadius(16)
        }
    }
}

struct MainView: View {
    @State private var searchText = ""
    @State private var selectedFilters = [false, false, false]
    @State private var scrollOffset: CGFloat = 0
    
    let filterTitles = ["全部", "未读", "已归档"]
    let sampleItems = (1...50).map { "列表项 \($0)" }
    
    var body: some View {
        NavigationStack {
            ScrollView {
                // 自定义头部区域
                VStack(alignment: .leading, spacing: 12) {
                    // 标题组(滚动时逐渐隐藏)
                    VStack(alignment: .leading, spacing: 4) {
                        Text("我的仓库")
                            .font(.largeTitle)
                            .fontWeight(.bold)
                            .opacity(scrollOffset < 80 ? 1 : 0)
                        Text("共 128 个项目")
                            .font(.subheadline)
                            .foregroundColor(.secondary)
                            .opacity(scrollOffset < 80 ? 1 : 0)
                    }
                    
                    // 搜索框
                    TextField("搜索仓库", text: $searchText)
                        .padding(.horizontal, 16)
                        .padding(.vertical, 10)
                        .background(Color(UIColor.systemGray6))
                        .cornerRadius(8)
                        .overlay(
                            HStack {
                                Image(systemName: "magnifyingglass")
                                    .foregroundColor(.secondary)
                                    .padding(.leading, 8)
                                Spacer()
                                if !searchText.isEmpty {
                                    Button(action: { searchText = "" }) {
                                        Image(systemName: "xmark.circle.fill")
                                            .foregroundColor(.secondary)
                                            .padding(.trailing, 8)
                                    }
                                }
                            }
                        )
                    
                    // 筛选芯片组
                    HStack(spacing: 8) {
                        ForEach(0..<filterTitles.count, id: \.self) { index in
                            FilterChip(title: filterTitles[index], isSelected: $selectedFilters[index])
                        }
                        Spacer()
                    }
                }
                .padding(.horizontal, 16)
                .padding(.top, 16)
                .padding(.bottom, 8)
                .background(Color(.systemBackground))
                .offset(y: scrollOffset > 0 ? -scrollOffset : 0)
                .opacity(scrollOffset < 120 ? 1 : 0)
                
                // 列表内容
                LazyVStack(spacing: 0) {
                    ForEach(sampleItems, id: \.self) { item in
                        Text(item)
                            .padding(.horizontal, 16)
                            .padding(.vertical, 12)
                            .border(.bottom, width: 0.5)
                    }
                }
            }
            .scrollDismissesKeyboard(.interactively)
            .scrollPosition(id: \.self, anchor: .top) { position in
                // 计算滚动偏移,实现头部折叠效果
                guard let position = position else { return }
                let index = sampleItems.firstIndex(of: position) ?? 0
                scrollOffset = max(0, CGFloat(index) * 44 - 80)
            }
            .navigationTitle("我的仓库")
            .navigationBarTitleDisplayMode(.large)
            .toolbar {
                // 导航栏右侧按钮示例
                Button(action: {}) {
                    Image(systemName: "plus")
                }
            }
            .toolbarBackground(.visible, for: .navigationBar)
        }
    }
}

#Preview {
    MainView()
}

关键细节说明

  1. 自定义头部替代Toolbar:Toolbar的布局受系统约束,无法自由放置搜索框和筛选芯片在标题下方,自定义头部放在ScrollView内,能跟随滚动自然交互。
  2. 滚动偏移处理:通过scrollPosition监听滚动位置,计算偏移量来调整头部的透明度和位置,模拟原生大标题的折叠效果。
  3. 筛选芯片实现:用自定义FilterChip组件,通过绑定选中状态切换样式,贴合原生UI风格。
  4. 搜索框优化:添加放大镜图标和清除按钮,完全对齐原生搜索框的交互体验。
  5. 键盘处理:scrollDismissesKeyboard(.interactively)实现滚动时键盘跟随收起,符合用户习惯。

兼容低版本(iOS15及以下)

如果需要兼容iOS15,把NavigationStack换成NavigationView,滚动偏移监听可以用ScrollViewReader结合onAppear和onChange来实现,核心逻辑一致。

内容的提问来源于stack exchange,提问作者Brandon Stillitano

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 16:25:09