SwiftUI实现下拉触发显示.searchable搜索栏的技术咨询
SwiftUI实现默认隐藏、下拉显示的搜索栏(仿Notes应用)
问题描述
我开发了一个SwiftUI视图,用List展示数据并支持搜索过滤。现在用.searchable修饰符创建的搜索栏始终可见,只有向下滚动列表时才隐藏。我想实现苹果Notes这类官方应用的效果:视图加载时搜索栏隐藏,用户轻微下拉列表时才显示。
我尝试过两种方式:
- 把
.searchable(text: $searchText, prompt: Text("Search"))加到Section组件上,搜索栏确实只在下拉时显示,但Section内容出现异常(比如滑动操作重复); - 加到顶部空Section时功能正常,但会显示空区域。
示例代码
ZStack{ NavigationView { List { if(filteredTodayItems.isEmpty == false){ Section(header: ListHeader(text: "Today")) { ForEach(filteredTodayItems) { item in objectCell(item: item, rememberSheet: $showRememberSheet, currentItem: $currentItem, showRememberSheet: $showRememberSheet, showDetailSheet: $showDetailSheet, showAddingChangeSheet: $showAddingChangeSheet) } } }else if(tomorrowItems.isEmpty && laterItems.isEmpty){ Text("Click on the Plus to enter a secret!") } } .searchable(text: $searchText, prompt: Text("Search")) .navigationTitle("Secrets") } }
完整视图代码
ZStack{ NavigationView { List { if(filteredTodayItems.isEmpty == false){ Section(header: ListHeader(text: "Today")) { ForEach(filteredTodayItems) { item in objectCell(item: item, rememberSheet: $showRememberSheet, currentItem: $currentItem, showRememberSheet: $showRememberSheet, showDetailSheet: $showDetailSheet, showAddingChangeSheet: $showAddingChangeSheet, isToday: true) } } }else if(tomorrowItems.isEmpty && laterItems.isEmpty){ Text("Click on the Plus to enter a secret!") } if(filteredTomorrowItems.isEmpty == false){ Section(header: ListHeader(text: "Tomorrow")) { ForEach(filteredTomorrowItems) { item in objectCell(item: item, rememberSheet: $showRememberSheet, currentItem: $currentItem, showRememberSheet: $showRememberSheet, showDetailSheet: $showDetailSheet, showAddingChangeSheet: $showAddingChangeSheet, isToday: false) } } } if(filteredLaterItems.isEmpty == false){ Section(header: ListHeader(text: "Later")) { ForEach(filteredLaterItems) { item in objectCell(item: item, rememberSheet: $showRememberSheet, currentItem: $currentItem, showRememberSheet: $showRememberSheet, showDetailSheet: $showDetailSheet, showAddingChangeSheet: $showAddingChangeSheet, isToday: false) } } } } .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .automatic)) .partialSheet(isPresented: $showRememberSheet) { checkSecretView(currentItem: $currentItem, showRememberSheet: $showRememberSheet, showToast: $showToast, showToast2: $showToast2, value: $value) } .toolbar { ToolbarItem (placement: .navigationBarLeading) { Button(action: { showSettings.toggle() }) { Image(systemName: "gear") } } ToolbarItem (placement: .navigationBarTrailing) { Button(action: { showAddingSheet.toggle() }) { Image(systemName: "plus") } } } .listStyle(InsetGroupedListStyle()) .navigationTitle("Secrets") } .sheet(isPresented: $showAddingSheet) { AddingSheet(editingItem: nil) } .sheet(isPresented: $showDetailSheet) { InfoSheet(currentItem: $currentItem) } .sheet(isPresented: $showSettings) { Settings() } .sheet(isPresented: $showAddingChangeSheet) { let idString = UserDefaults.standard.string(forKey: "ToBeEditedLiveGoalID") let id = UUID(uuidString: idString ?? "") let objectToEdit = items.first(where: { $0.id == id}) AddingSheet(editingItem: objectToEdit) } .attachPartialSheetToRoot() .simpleToast(isPresented: $showToast, options: toastOptions){ HStack{ Image(systemName: "checkmark.circle.fill") Text("Correctly Entered").bold() } .padding(.vertical,8) .padding(.horizontal,16) .background(.green.opacity(0.9)) .foregroundColor(.white) .cornerRadius(10) } .simpleToast(isPresented: $showToast2, options: toastOptions){ HStack{ Image(systemName: "xmark.circle.fill") Text("Wrongly Entered").bold() } .padding(.vertical,8) .padding(.horizontal,16) .background(.red.opacity(0.9)) .foregroundColor(.white) .cornerRadius(10) } } .blur(radius: isUnlocked ? 0 : 25) .disabled(!isUnlocked) .overlay( lockedScreen(color: Binding.constant(.accentColor) , isUnlocked: $isUnlocked) .opacity(isUnlocked ? 0 : 1) ) .onAppear{ isUnlocked = !bioSaveAuthActive if(!isUnlocked){ authenticate() } } }
解决方案
一、通过原生.searchable配置实现需求
不需要自定义,只需调整.searchable的placement参数,并修改修饰符的应用层级:
核心修改代码
NavigationView { // 你的List内容保持不变 List { // ... 原有的Section和单元格代码 } .listStyle(InsetGroupedListStyle()) .toolbar { // 原有的toolbar按钮 } .navigationTitle("Secrets") } // 把.searchable移到NavigationView层级 .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .automatic))
关键说明
.navigationBarDrawer(displayMode: .automatic)是原生支持的抽屉式搜索布局,默认隐藏搜索栏,用户下拉列表时自动滑出显示,完全匹配Notes应用的行为;- 必须将
.searchable修饰符应用在NavigationView/NavigationStack层级,而非List或Section上,这样可以避免Section内容滑动异常的问题; - 之前加在Section上导致的异常,是因为Section的布局上下文与搜索栏的抽屉布局冲突,移到NavigationView层级即可解决。
二、自定义搜索栏方案(适用于特殊UI需求)
如果原生配置无法满足定制化样式要求,可以手动实现隐藏式搜索栏:
步骤1:添加状态变量
@State private var isSearchBarVisible = false @State private var scrollOffset = CGFloat(0)
步骤2:监听滚动偏移并控制搜索栏显示
NavigationView { ScrollViewReader { proxy in List { // 原有的Section和内容 } .background( GeometryReader { geo in Color.clear .preference(key: ScrollOffsetKey.self, value: geo.frame(in: .global).minY) } ) .onPreferenceChange(ScrollOffsetKey.self) { offset in // 下拉偏移超过20pt时显示搜索栏,可根据需求调整阈值 isSearchBarVisible = offset > 20 } .toolbar { // 动态显示搜索栏 if isSearchBarVisible { ToolbarItem(placement: .principal) { TextField("Search", text: $searchText) .textFieldStyle(RoundedBorderTextFieldStyle()) .transition(.move(edge: .top).combined(with: .opacity)) } } // 原有的toolbar按钮 } .animation(.easeInOut, value: isSearchBarVisible) } }
步骤3:定义滚动偏移监听的PreferenceKey
struct ScrollOffsetKey: PreferenceKey { static var defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = nextValue() } }
自定义方案说明
- 通过
GeometryReader监听List的滚动偏移量,达到阈值时显示搜索栏; - 可以完全自定义搜索栏的样式、位置和动画效果;
- 适合需要对搜索栏做特殊UI定制的场景,比如改变背景色、添加自定义按钮等。
内容的提问来源于stack exchange,提问作者Black Fox
相关产品推荐
相关产品推荐

