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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 23:04:52