mana-swift-ui/Sources/ManaComponentsUI/ManaSearchResultsView.swift
Till JS cba98d61bf feat(ManaSearchResultsView): isLoading-Zustand fuer Server-Suche
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>
2026-06-06 20:02:15 +02:00

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)
}
}
}