Move search to a dedicated tab in the tab bar
Replace the Notes list's searchable accessory with a Tab(role: .search) — the separated magnifier at the trailing edge of the iOS 26 tab bar — backed by a new SearchView over text, tags, and capture location. The Notes list drops its search state and always shows the context shelf plus all notes. Claude-Session: https://claude.ai/code/session_01GKfAiKiQKLDTmbiGva4FGE
This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@ Transcription now supports many more languages, including Russian, Ukrainian, an
|
||||
|
||||
New notes start from two big buttons right above the list — one for text, one for voice
|
||||
|
||||
On iPhone, search now lives in the toolbar at the bottom of the screen
|
||||
Search now has its own tab in the bottom tab bar
|
||||
|
||||
Record voice notes that are transcribed to text right on your device, with the original audio kept alongside the note for playback
|
||||
|
||||
|
||||
@@ -19,6 +19,12 @@ struct ContentView: View {
|
||||
Tab("Settings", systemImage: "gearshape") {
|
||||
SettingsView()
|
||||
}
|
||||
|
||||
// role: .search renders as the separated magnifier tab at the
|
||||
// trailing edge of the tab bar on iOS 26.
|
||||
Tab("Search", systemImage: "magnifyingglass", role: .search) {
|
||||
SearchView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@ import SwiftData
|
||||
import SwiftUI
|
||||
|
||||
/// The main list: a "Right here, right now" shelf of context-relevant notes
|
||||
/// on top, then everything else newest-first. Search filters text and tags.
|
||||
/// on top, then everything else newest-first. Search lives in its own tab
|
||||
/// (`SearchView`, the tab bar's trailing magnifier).
|
||||
struct NotesListView: View {
|
||||
@Environment(SyncEngine.self) private var syncEngine
|
||||
@Environment(ContextService.self) private var contextService
|
||||
@Query(sort: \NoteEntity.createdAt, order: .reverse) private var notes: [NoteEntity]
|
||||
|
||||
@State private var searchText = ""
|
||||
@State private var composing = false
|
||||
@State private var recording = false
|
||||
@State private var editingNote: NoteEntity?
|
||||
@@ -21,15 +21,6 @@ struct NotesListView: View {
|
||||
list
|
||||
}
|
||||
.navigationTitle("Notes")
|
||||
// iOS 26: attached at the NavigationStack root inside a TabView, this
|
||||
// renders as the Liquid-Glass toolbar search accessory (bottom, in the
|
||||
// tab bar). `.searchToolbarBehavior(.minimize)` collapses it to the
|
||||
// magnifier capsule until tapped — a modifier that only exists on iOS,
|
||||
// hence the guard; macOS keeps the standard toolbar search field.
|
||||
.searchable(text: $searchText, prompt: "Search notes and tags")
|
||||
#if os(iOS)
|
||||
.searchToolbarBehavior(.minimize)
|
||||
#endif
|
||||
.sheet(isPresented: $composing) {
|
||||
NoteEditorView(mode: .create)
|
||||
}
|
||||
@@ -79,7 +70,7 @@ struct NotesListView: View {
|
||||
}
|
||||
}
|
||||
|
||||
if searchText.isEmpty, !relevantNotes.isEmpty {
|
||||
if !relevantNotes.isEmpty {
|
||||
Section("Right here, right now") {
|
||||
ForEach(relevantNotes) { note in
|
||||
row(note)
|
||||
@@ -87,8 +78,8 @@ struct NotesListView: View {
|
||||
}
|
||||
}
|
||||
|
||||
Section(searchText.isEmpty ? "All notes" : "Results") {
|
||||
ForEach(filteredNotes) { note in
|
||||
Section("All notes") {
|
||||
ForEach(notes) { note in
|
||||
row(note)
|
||||
}
|
||||
}
|
||||
@@ -133,17 +124,6 @@ struct NotesListView: View {
|
||||
.map(\.note)
|
||||
}
|
||||
|
||||
private var filteredNotes: [NoteEntity] {
|
||||
guard !searchText.isEmpty else { return notes }
|
||||
let needle = searchText.lowercased()
|
||||
return notes.filter { note in
|
||||
note.text.lowercased().contains(needle)
|
||||
|| note.tags.contains { $0.lowercased().contains(needle) }
|
||||
|| (note.placeName?.lowercased().contains(needle) ?? false)
|
||||
|| (note.locality?.lowercased().contains(needle) ?? false)
|
||||
}
|
||||
}
|
||||
|
||||
private func delete(_ note: NoteEntity) {
|
||||
guard let id = ULID(string: note.id) else { return }
|
||||
Task {
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import IndieSync
|
||||
import SwiftData
|
||||
import SwiftUI
|
||||
|
||||
/// The Search tab: full-library search over note text, tags, and capture
|
||||
/// location. Lives behind the tab bar's dedicated search tab (`role: .search`),
|
||||
/// which iOS 26 renders as the separated magnifier at the trailing edge.
|
||||
struct SearchView: View {
|
||||
@Environment(SyncEngine.self) private var syncEngine
|
||||
@Query(sort: \NoteEntity.createdAt, order: .reverse) private var notes: [NoteEntity]
|
||||
|
||||
@State private var searchText = ""
|
||||
@State private var editingNote: NoteEntity?
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
ForEach(results) { note in
|
||||
row(note)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Search")
|
||||
.searchable(text: $searchText, prompt: "Search notes and tags")
|
||||
.overlay {
|
||||
if searchText.isEmpty {
|
||||
ContentUnavailableView(
|
||||
"Search your notes",
|
||||
systemImage: "magnifyingglass",
|
||||
description: Text("Find notes by text, tag, or the place you captured them.")
|
||||
)
|
||||
} else if results.isEmpty {
|
||||
ContentUnavailableView.search(text: searchText)
|
||||
}
|
||||
}
|
||||
.sheet(item: $editingNote) { note in
|
||||
NoteEditorView(mode: .edit(note))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func row(_ note: NoteEntity) -> some View {
|
||||
Button {
|
||||
editingNote = note
|
||||
} label: {
|
||||
NoteRowView(note: note)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.swipeActions(edge: .trailing) {
|
||||
Button(role: .destructive) {
|
||||
delete(note)
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var results: [NoteEntity] {
|
||||
guard !searchText.isEmpty else { return [] }
|
||||
return notes.filter { $0.matches(search: searchText) }
|
||||
}
|
||||
|
||||
private func delete(_ note: NoteEntity) {
|
||||
guard let id = ULID(string: note.id) else { return }
|
||||
Task {
|
||||
try? await syncEngine.deleteNote(id: id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NoteEntity {
|
||||
/// Case-insensitive match against text, tags, and capture location.
|
||||
func matches(search: String) -> Bool {
|
||||
let needle = search.lowercased()
|
||||
return text.lowercased().contains(needle)
|
||||
|| tags.contains { $0.lowercased().contains(needle) }
|
||||
|| (placeName?.lowercased().contains(needle) ?? false)
|
||||
|| (locality?.lowercased().contains(needle) ?? false)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user