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() }
关键细节说明
- 自定义头部替代Toolbar:Toolbar的布局受系统约束,无法自由放置搜索框和筛选芯片在标题下方,自定义头部放在ScrollView内,能跟随滚动自然交互。
- 滚动偏移处理:通过
scrollPosition监听滚动位置,计算偏移量来调整头部的透明度和位置,模拟原生大标题的折叠效果。 - 筛选芯片实现:用自定义
FilterChip组件,通过绑定选中状态切换样式,贴合原生UI风格。 - 搜索框优化:添加放大镜图标和清除按钮,完全对齐原生搜索框的交互体验。
- 键盘处理:
scrollDismissesKeyboard(.interactively)实现滚动时键盘跟随收起,符合用户习惯。
兼容低版本(iOS15及以下)
如果需要兼容iOS15,把NavigationStack换成NavigationView,滚动偏移监听可以用ScrollViewReader结合onAppear和onChange来实现,核心逻辑一致。
内容的提问来源于stack exchange,提问作者Brandon Stillitano
相关产品推荐
相关产品推荐

