Vierter Zustand: nicht-leere Query + noch keine Treffer + isLoading -> Spinner (statt 'keine Treffer'); hat-Treffer zeigt weiter die Liste (kein Flackern beim Reload). isLoading:Bool=false -> backward-compatible (lokale Suche wie memoro unveraendert). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
80 lines
3 KiB
Swift
80 lines
3 KiB
Swift
import ManaTokens
|
|
import SwiftUI
|
|
|
|
/// Geteilte Hülle für die **native Such-Ansicht** hinter dem iOS-26-Such-Tab
|
|
/// (`Tab(role: .search)`). Vereinheitlicht die drei Such-Zustände flottenweit,
|
|
/// während die App nur ihre **Ergebnis-Liste** + Query + Treffer-Status reicht:
|
|
///
|
|
/// 1. **Leere Query** → Einstiegs-Prompt (Lupe + App-Text).
|
|
/// 2. **Query, aber keine Treffer** → System-`ContentUnavailableView.search`.
|
|
/// 3. **Treffer** → die app-eigene `results`-Liste.
|
|
///
|
|
/// Bewusst **ohne** eigenen `NavigationStack`/`.searchable` — die App umschließt
|
|
/// die View damit (und hängt `navigationDestination`/`.searchable` selbst an),
|
|
/// weil Ziel-Typen + Query-Binding app-spezifisch sind. So bleibt nur die
|
|
/// Leerzustands-/Treffer-Logik geteilt.
|
|
///
|
|
/// ```swift
|
|
/// NavigationStack {
|
|
/// ManaSearchResultsView(query: q, hasResults: !hits.isEmpty, prompt: "Memos durchsuchen") {
|
|
/// List { ForEach(hits) { … } }
|
|
/// }
|
|
/// .navigationDestination(for: Memo.self) { … }
|
|
/// }
|
|
/// .searchable(text: $q)
|
|
/// ```
|
|
public struct ManaSearchResultsView<Results: View>: View {
|
|
@Environment(\.manaTheme) private var theme
|
|
|
|
private let query: String
|
|
private let hasResults: Bool
|
|
private let isLoading: Bool
|
|
private let prompt: String
|
|
private let results: Results
|
|
|
|
/// - Parameters:
|
|
/// - query: aktueller Suchtext (zur Zustands-Wahl + No-Results-Label).
|
|
/// - hasResults: ob die App bei nicht-leerer Query Treffer hat.
|
|
/// - isLoading: Server-Suche läuft (zeigt Spinner statt „keine Treffer",
|
|
/// solange noch keine Treffer da sind). Default `false` für rein lokale
|
|
/// Suche.
|
|
/// - prompt: Einstiegs-Text bei leerer Query (z.B. „Memos durchsuchen").
|
|
/// - results: die app-eigene Ergebnis-Liste (nur gezeigt, wenn Treffer).
|
|
public init(
|
|
query: String,
|
|
hasResults: Bool,
|
|
isLoading: Bool = false,
|
|
prompt: String,
|
|
@ViewBuilder results: () -> Results
|
|
) {
|
|
self.query = query
|
|
self.hasResults = hasResults
|
|
self.isLoading = isLoading
|
|
self.prompt = prompt
|
|
self.results = results()
|
|
}
|
|
|
|
private var trimmedQuery: String {
|
|
query.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
}
|
|
|
|
public var body: some View {
|
|
if trimmedQuery.isEmpty {
|
|
// Einstieg: leere Query → Prompt.
|
|
ContentUnavailableView {
|
|
Label(prompt, systemImage: "magnifyingglass")
|
|
}
|
|
} else if hasResults {
|
|
// Treffer (auch während eines laufenden Reloads) → kein Flackern.
|
|
results
|
|
} else if isLoading {
|
|
// Server-Suche läuft, noch keine Treffer → Spinner.
|
|
ProgressView()
|
|
.controlSize(.large)
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
} else {
|
|
// Query, fertig geladen, nichts gefunden → System-No-Results.
|
|
ContentUnavailableView.search(text: trimmedQuery)
|
|
}
|
|
}
|
|
}
|