@@ -5,57 +5,45 @@ import SwiftUI
55
66struct SearchView : View {
77 var onSelect : ( Airport ) -> Void
8- @State private var searchText = " "
8+ @State private var viewModel : SearchViewModel ?
9+
10+ @Environment ( \. modelContext)
11+ private var modelContext
912
1013 var body : some View {
1114 NavigationStack {
12- SearchResults ( searchText: searchText, onSelect: onSelect)
13- . searchable ( text: $searchText)
15+ if let viewModel {
16+ SearchResults ( viewModel: viewModel, onSelect: onSelect)
17+ . searchable (
18+ text: Binding (
19+ get: { viewModel. searchText } ,
20+ set: { viewModel. searchText = $0 }
21+ )
22+ )
23+ }
24+ }
25+ . onAppear {
26+ if viewModel == nil {
27+ viewModel = SearchViewModel ( container: modelContext. container)
28+ }
1429 }
1530 }
1631}
1732
1833private struct SearchResults : View {
19- var searchText : String
34+ var viewModel : SearchViewModel
2035 var onSelect : ( Airport ) -> Void
2136
22- @Environment ( \. modelContext)
23- private var modelContext
24-
25- @State private var airports : [ Airport ] = [ ]
26- @State private var isLoading = false
27- @State private var searchTask : Task < Void , Never > ?
28-
29- private var sortedAirports : [ Airport ] {
30- return airports. sorted { airport1, airport2 in
31- let score1 = relevanceScore ( for: airport1, searchText: searchText)
32- let score2 = relevanceScore ( for: airport2, searchText: searchText)
33- if score1 != score2 { return score1 > score2 }
34-
35- // If same relevance score, sort by name similarity (primary) + city similarity (secondary)
36- let nameSim1 = nameSimilarity ( airport1. name, to: searchText)
37- let nameSim2 = nameSimilarity ( airport2. name, to: searchText)
38- let citySim1 = citySimilarity ( airport1. city, to: searchText)
39- let citySim2 = citySimilarity ( airport2. city, to: searchText)
40- let similarity1 = max ( nameSim1, citySim1)
41- let similarity2 = max ( nameSim2, citySim2)
42- if similarity1 != similarity2 { return similarity1 > similarity2 }
43-
44- // Final tie-breaker: alphabetical by name
45- return airport1. name. localizedStandardCompare ( airport2. name) == . orderedAscending
46- }
47- }
48-
4937 var body : some View {
5038 Group {
51- if sortedAirports. isEmpty {
39+ if viewModel . sortedAirports. isEmpty {
5240 List {
5341 Text ( " No results. " )
5442 . foregroundStyle ( . secondary)
5543 . multilineTextAlignment ( . leading)
5644 }
5745 } else {
58- List ( sortedAirports) { ( airport: Airport ) in
46+ List ( viewModel . sortedAirports) { ( airport: Airport ) in
5947 AirportRow ( airport: airport, showFavoriteButton: true )
6048 . onTapGesture {
6149 onSelect ( airport)
@@ -65,92 +53,6 @@ private struct SearchResults: View {
6553 }
6654 }
6755 }
68- . onChange ( of: searchText) { debouncedSearch ( ) }
69- . task { performSearch ( ) }
70- . onDisappear { searchTask? . cancel ( ) }
71- }
72-
73- init ( searchText: String , onSelect: @escaping ( Airport ) -> Void ) {
74- self . searchText = searchText
75- self . onSelect = onSelect
76- }
77-
78- private func relevanceScore( for airport: Airport , searchText: String ) -> Int {
79- if airport. locationID == searchText. uppercased ( ) { return 3 }
80- if let ICAO_ID = airport. ICAO_ID, ICAO_ID == searchText. uppercased ( ) { return 3 }
81- if airport. name. localizedStandardContains ( searchText) { return 2 }
82- if let city = airport. city, city. localizedStandardContains ( searchText) { return 1 }
83- return 0
84- }
85-
86- private func nameSimilarity( _ name: String , to searchText: String ) -> Double {
87- if name. localizedStandardEquals ( searchText) { return 1.0 }
88- if name. localizedStandardHasPrefix ( searchText) { return 0.8 }
89- if name. localizedStandardContains ( searchText) { return 0.6 }
90-
91- // Calculate simple similarity based on common characters
92- let commonChars = Set ( name. localizedLowercase) . intersection ( Set ( searchText. localizedLowercase) )
93- . count
94- let totalChars = max ( name. count, searchText. count)
95- return Double ( commonChars) / Double( totalChars) * 0.4
96- }
97-
98- private func citySimilarity( _ city: String ? , to searchText: String ) -> Double {
99- guard let city else { return 0.0 }
100- if city. localizedStandardEquals ( searchText) { return 0.5 }
101- if city. localizedStandardHasPrefix ( searchText) { return 0.4 }
102- if city. localizedStandardContains ( searchText) { return 0.3 }
103- return 0.0
104- }
105-
106- private func debouncedSearch( ) {
107- searchTask? . cancel ( )
108- searchTask = Task {
109- // Wait 300ms before executing the search
110- try ? await Task . sleep ( nanoseconds: 300_000_000 )
111- if !Task. isCancelled { performSearch ( ) }
112- }
113- }
114-
115- private func performSearch( ) {
116- guard searchText. count > 2 else {
117- airports = [ ]
118- return
119- }
120-
121- isLoading = true
122- let searchTextCopy = searchText
123- let container = modelContext. container
124-
125- Task . detached {
126- let context = ModelContext ( container)
127- let uppercaseText = searchTextCopy. uppercased ( )
128-
129- let predicate = #Predicate< Airport> { airport in
130- searchTextCopy. count > 2
131- && ( airport. locationID == uppercaseText
132- || airport. name. localizedStandardContains ( searchTextCopy)
133- || airport. ICAO_ID == uppercaseText
134- || airport. city? . localizedStandardContains ( searchTextCopy) == true )
135- }
136- let descriptor = FetchDescriptor ( predicate: predicate)
137-
138- do {
139- let results = try context. fetch ( descriptor)
140- await MainActor . run {
141- // Only update if search text hasn't changed
142- if searchTextCopy == searchText {
143- airports = results
144- isLoading = false
145- }
146- }
147- } catch {
148- await MainActor . run {
149- airports = [ ]
150- isLoading = false
151- }
152- }
153- }
15456 }
15557}
15658
0 commit comments