Make iOS search a normal tab with a focused text field.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
pepaskopec-blip 2026-09-21 14:17:31 +02:00
commit 7761b52ed0
2 changed files with 63 additions and 3 deletions

View file

@ -51,7 +51,7 @@ struct NativeRoot: View {
SettingsScreen(vm: vm) SettingsScreen(vm: vm)
} }
} }
Tab(vm.tr("search"), systemImage: "magnifyingglass", value: .search, role: .search) { Tab(vm.tr("search"), systemImage: "magnifyingglass", value: .search) {
NavigationStack { NavigationStack {
SearchScreen(vm: vm) SearchScreen(vm: vm)
} }

View file

@ -369,16 +369,25 @@ private func subjectSum(_ vm: AppModel, _ s: J) -> ProgressSum {
struct SearchScreen: View { struct SearchScreen: View {
@ObservedObject var vm: AppModel @ObservedObject var vm: AppModel
@State private var query = ""
#if os(iOS)
@FocusState private var fieldFocused: Bool
#endif
var body: some View { var body: some View {
let q = normalizeAnswer(vm.searchQuery) let q = normalizeAnswer(query)
let hits = buildSearch(vm).filter { q.isEmpty || normalizeAnswer($0.hay).contains(q) } let hits = buildSearch(vm).filter { q.isEmpty || normalizeAnswer($0.hay).contains(q) }
List { return List {
if hits.isEmpty { if hits.isEmpty {
ContentUnavailableView.search ContentUnavailableView.search
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
} else { } else {
ForEach(hits) { hit in ForEach(hits) { hit in
Button { Button {
#if os(iOS)
fieldFocused = false
#endif
if !hit.locked { vm.openFromSearch(hit.target) } if !hit.locked { vm.openFromSearch(hit.target) }
} label: { } label: {
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
@ -392,9 +401,60 @@ struct SearchScreen: View {
} }
} }
.navigationTitle(vm.tr("search")) .navigationTitle(vm.tr("search"))
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
.safeAreaBar(edge: .top) {
searchField
}
.onAppear {
query = vm.searchQuery
DispatchQueue.main.async {
fieldFocused = true
}
}
.onChange(of: query) { _, q in
vm.searchQuery = q
}
.onChange(of: vm.searchQuery) { _, q in
if q != query { query = q }
}
#else
.searchable(text: $vm.searchQuery, prompt: vm.tr("search_placeholder")) .searchable(text: $vm.searchQuery, prompt: vm.tr("search_placeholder"))
.onAppear { query = vm.searchQuery }
.onChange(of: vm.searchQuery) { _, q in query = q }
#endif
.appListStyle() .appListStyle()
} }
#if os(iOS)
private var searchField: some View {
HStack(spacing: 8) {
Image(systemName: "magnifyingglass")
.foregroundStyle(.secondary)
TextField(vm.tr("search_placeholder"), text: $query)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.submitLabel(.search)
.focused($fieldFocused)
if !query.isEmpty {
Button {
query = ""
fieldFocused = true
} label: {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.tertiary)
}
.buttonStyle(.plain)
.accessibilityLabel("Clear")
}
}
.padding(.horizontal, 12)
.padding(.vertical, 10)
.background(Color(.tertiarySystemFill), in: RoundedRectangle(cornerRadius: 12, style: .continuous))
.padding(.horizontal, 16)
.padding(.vertical, 8)
}
#endif
} }
private struct Hit: Identifiable { private struct Hit: Identifiable {