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

SwiftUI实现Sheet顶部固定搜索栏的方案咨询

实现Sheet内固定顶部的搜索栏(仿苹果地图效果)

问题描述

我想在Sheet顶部始终固定显示搜索栏,目前使用searchable修饰符,不管设置哪种navigationBarDrawer参数,点击搜索栏时它都会移动到导航栏顶部。想要实现类似苹果地图底部面板里搜索栏的固定效果,该怎么做?

当前代码:

import SwiftUI
import Foundation
import Combine

@main
struct SampleAppApp: App {
    var body: some Scene {
        WindowGroup {
            NavigationStack {
                ContentView()
            }
        }
    }
}

struct SimpleTestData {
    let id = UUID()
    let score: Int
    let name: String
}

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello World")
        }
        .sheet(isPresented: .constant(true)) {
            ContentNewView()
        }
    }
}

struct ContentNewView: View {
    @State private var searchText = ""
    let searchTextPublisher = PassthroughSubject<String, Never>()
    
    var body: some View {
        NavigationStack {
            ZStack {
                Color(red: 0.95, green: 0.95, blue: 0.97).ignoresSafeArea()
                
                VStack {
                    ResultsList(searchText: searchText, searchTextPublisher: searchTextPublisher)
                    .searchable(
                        text: $searchText,
                        placement: .navigationBarDrawer(displayMode: .always), 
                        prompt: "Search"
                    )
                    .onChange(of: searchText) { newText in
                        searchTextPublisher.send(newText) /// Publishes when a search term is changed. This is used to debounce the search.
                    }

                    Spacer()
                }
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(Color(red: 0.95, green: 0.95, blue: 0.97))
            }
        }
        .presentationDetents([.medium, .large])
        .interactiveDismissDisabled(true)
        .presentationBackgroundInteraction(.enabled)
        .onAppear {
            let appearance = UITabBarAppearance()
            UITabBar.appearance().scrollEdgeAppearance = appearance
        }
    }
}

struct ResultsList: View {
    var searchText: String
    var searchTextPublisher: PassthroughSubject<String, Never>
    
    init(searchText: String, searchTextPublisher: PassthroughSubject<String, Never>) {
        self.searchText = searchText
        self.searchTextPublisher = searchTextPublisher
        UITableView.appearance().sectionFooterHeight = 0
    }
    
    let results = [
        SimpleTestData(score: 8, name: "Book1"),
        SimpleTestData(score: 5, name: "Book2"),
        SimpleTestData(score: 10, name: "Book3")
    ]
    
    var body: some View {
        ScrollViewReader { proxy in
            VStack(alignment: .leading, spacing: 8.0) {
                if !results.isEmpty {
                    List {
                        Section(header: Text("Results")) {
                            ForEach(results, id: \.id) { result in
                                HStack {
                                    Text(result.name).font(.body)
                                    Spacer()
                                    Text("\(result.score)").font(.body)
                                }
                                .listRowBackground(Color.gray)
                                .contentShape(Rectangle())
                            }
                        }
                        
                        Section(header: Text("Results")) {
                            ForEach(results, id: \.id) { result in
                                HStack {
                                    Text(result.name).font(.body)
                                    Spacer()
                                    Text("\(result.score)").font(.body)
                                }
                                .listRowBackground(Color.gray)
                                .contentShape(Rectangle())
                            }
                        }
                    }
                    .listStyle(.insetGrouped)
                    .foregroundColor(.black)
                    .padding([.leading, .trailing], 20.0)
                }
            }
            .padding(.horizontal, -20.0)
        }
    }
}

当前效果:
未点击搜索栏的效果
点击后搜索栏跳转到顶部的效果

解决方案

系统的searchable修饰符结合navigationBarDrawer会默认将搜索栏关联到导航栏,点击时必然跳转。要实现固定效果,需要自定义搜索栏,脱离系统导航栏的关联:

修改后的完整代码

import SwiftUI
import Foundation
import Combine

@main
struct SampleAppApp: App {
    var body: some Scene {
        WindowGroup {
            NavigationStack {
                ContentView()
            }
        }
    }
}

struct SimpleTestData {
    let id = UUID()
    let score: Int
    let name: String
}

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello World")
        }
        .sheet(isPresented: .constant(true)) {
            ContentNewView()
        }
    }
}

struct ContentNewView: View {
    @State private var searchText = ""
    let searchTextPublisher = PassthroughSubject<String, Never>()
    
    var body: some View {
        NavigationStack {
            ZStack {
                Color(red: 0.95, green: 0.95, blue: 0.97).ignoresSafeArea()
                
                VStack(spacing: 12) {
                    // 自定义固定顶部的搜索栏
                    HStack {
                        Image(systemName: "magnifyingglass")
                            .foregroundColor(.secondary)
                        TextField("Search", text: $searchText)
                            .onChange(of: searchText) { newText in
                                searchTextPublisher.send(newText)
                            }
                        if !searchText.isEmpty {
                            Button(action: { searchText = "" }) {
                                Image(systemName: "xmark.circle.fill")
                                    .foregroundColor(.secondary)
                            }
                        }
                    }
                    .padding(.horizontal, 16)
                    .padding(.vertical, 10)
                    .background(Color.white)
                    .cornerRadius(8)
                    .padding(.horizontal, 20)
                    .padding(.top, 16)
                    
                    // 列表部分,在搜索栏下方滚动
                    ResultsList(searchText: searchText, searchTextPublisher: searchTextPublisher)
                    
                    Spacer()
                }
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(Color(red: 0.95, green: 0.95, blue: 0.97))
            }
        }
        .presentationDetents([.medium, .large])
        .interactiveDismissDisabled(true)
        .presentationBackgroundInteraction(.enabled)
        .onAppear {
            let appearance = UITabBarAppearance()
            UITabBar.appearance().scrollEdgeAppearance = appearance
        }
    }
}

struct ResultsList: View {
    var searchText: String
    var searchTextPublisher: PassthroughSubject<String, Never>
    
    init(searchText: String, searchTextPublisher: PassthroughSubject<String, Never>) {
        self.searchText = searchText
        self.searchTextPublisher = searchTextPublisher
        UITableView.appearance().sectionFooterHeight = 0
    }
    
    let results = [
        SimpleTestData(score: 8, name: "Book1"),
        SimpleTestData(score: 5, name: "Book2"),
        SimpleTestData(score: 10, name: "Book3")
    ]
    
    var body: some View {
        ScrollViewReader { proxy in
            VStack(alignment: .leading, spacing: 8.0) {
                if !results.isEmpty {
                    List {
                        Section(header: Text("Results")) {
                            ForEach(results, id: \.id) { result in
                                HStack {
                                    Text(result.name).font(.body)
                                    Spacer()
                                    Text("\(result.score)").font(.body)
                                }
                                .listRowBackground(Color.gray)
                                .contentShape(Rectangle())
                            }
                        }
                        
                        Section(header: Text("Results")) {
                            ForEach(results, id: \.id) { result in
                                HStack {
                                    Text(result.name).font(.body)
                                    Spacer()
                                    Text("\(result.score)").font(.body)
                                }
                                .listRowBackground(Color.gray)
                                .contentShape(Rectangle())
                            }
                        }
                    }
                    .listStyle(.insetGrouped)
                    .foregroundColor(.black)
                    .padding([.leading, .trailing], 20.0)
                }
            }
            .padding(.horizontal, -20.0)
        }
    }
}

关键修改说明

  1. 移除系统searchable修饰符:不再依赖系统导航栏的搜索栏实现,避免点击跳转的问题
  2. 自定义搜索栏:用HStack+TextField实现和系统样式一致的搜索框,添加清除按钮保证交互完整性
  3. 布局调整:将搜索栏放在VStack的最顶部,列表紧随其后,这样列表滚动时搜索栏始终固定在Sheet顶部
  4. 保留原有搜索逻辑:继续使用searchTextPublisher发布搜索文本变化,不影响后续的防抖或搜索请求逻辑

这样修改后,搜索栏会像苹果地图底部面板的搜索栏一样,始终固定在Sheet顶部,点击输入时不会发生位置跳转。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 11:54:53