@@ -15,6 +15,33 @@ extension CLLocationCoordinate2D: Equatable {
1515 }
1616}
1717
18+ // MARK: - Search Completer
19+
20+ @MainActor
21+ final class LocationSearchCompleter : NSObject , ObservableObject , MKLocalSearchCompleterDelegate {
22+ @Published var results : [ MKLocalSearchCompletion ] = [ ]
23+ private let completer = MKLocalSearchCompleter ( )
24+
25+ override init ( ) {
26+ super. init ( )
27+ completer. delegate = self
28+ completer. resultTypes = [ . address, . pointOfInterest]
29+ }
30+
31+ func update( query: String ) {
32+ completer. queryFragment = query
33+ }
34+
35+ nonisolated func completerDidUpdateResults( _ completer: MKLocalSearchCompleter ) {
36+ let results = completer. results
37+ Task { @MainActor in self . results = results }
38+ }
39+
40+ nonisolated func completer( _ completer: MKLocalSearchCompleter , didFailWithError error: Error ) {
41+ Task { @MainActor in self . results = [ ] }
42+ }
43+ }
44+
1845struct LocationSimulationView : View {
1946 // Serial queue: simulate_location and clear_simulated_location share C global
2047 // state — serialising all calls eliminates the use-after-free race.
@@ -31,6 +58,9 @@ struct LocationSimulationView: View {
3158 @State private var alertTitle = " "
3259 @State private var alertMessage = " "
3360
61+ @State private var searchText = " "
62+ @StateObject private var searchCompleter = LocationSearchCompleter ( )
63+
3464 private var pairingFilePath : String {
3565 URL . documentsDirectory. appendingPathComponent ( " pairingFile.plist " ) . path ( )
3666 }
@@ -70,32 +100,94 @@ struct LocationSimulationView: View {
70100 }
71101 }
72102
73- VStack ( spacing: 12 ) {
74- if let coord = coordinate {
75- Text ( String ( format: " %.6f, %.6f " , coord. latitude, coord. longitude) )
76- . font ( . footnote. monospaced ( ) )
77- . foregroundStyle ( . secondary)
103+ VStack ( spacing: 0 ) {
104+ if !searchCompleter. results. isEmpty {
105+ if #available( iOS 26 , * ) {
106+ List ( searchCompleter. results. prefix ( 5 ) , id: \. self) { result in
107+ Button {
108+ selectSearchResult ( result)
109+ } label: {
110+ VStack ( alignment: . leading, spacing: 2 ) {
111+ Text ( result. title)
112+ . font ( . subheadline)
113+ if !result. subtitle. isEmpty {
114+ Text ( result. subtitle)
115+ . font ( . caption)
116+ . foregroundStyle ( . secondary)
117+ }
118+ }
119+ }
120+ }
121+ . listStyle ( . plain)
122+ . frame ( maxHeight: 350 )
123+ . scrollDisabled ( true )
124+ . glassEffect ( in: . rect( cornerRadius: 12 ) )
125+ . padding ( . horizontal, 16 )
126+ . padding ( . top, 8 )
127+ } else {
128+ List ( searchCompleter. results. prefix ( 5 ) , id: \. self) { result in
129+ Button {
130+ selectSearchResult ( result)
131+ } label: {
132+ VStack ( alignment: . leading, spacing: 2 ) {
133+ Text ( result. title)
134+ . font ( . subheadline)
135+ if !result. subtitle. isEmpty {
136+ Text ( result. subtitle)
137+ . font ( . caption)
138+ . foregroundStyle ( . secondary)
139+ }
140+ }
141+ }
142+ }
143+ . listStyle ( . plain)
144+ . frame ( maxHeight: 350 )
145+ . scrollDisabled ( true )
146+ . padding ( . horizontal, 16 )
147+ . padding ( . top, 8 )
148+ }
149+ }
150+
151+ Spacer ( )
78152
79- HStack ( spacing: 12 ) {
80- Button ( " Stop " , action: clear)
81- . buttonStyle ( . bordered)
82- . tint ( . red)
83- . disabled ( !pairingExists || isBusy)
153+ // Bottom controls
154+ VStack ( spacing: 12 ) {
155+ if let coord = coordinate {
156+ Text ( String ( format: " %.6f, %.6f " , coord. latitude, coord. longitude) )
157+ . font ( . footnote. monospaced ( ) )
158+ . foregroundStyle ( . secondary)
159+
160+ HStack ( spacing: 12 ) {
161+ Button ( " Stop " , action: clear)
162+ . buttonStyle ( . bordered)
163+ . tint ( . red)
164+ . disabled ( !pairingExists || isBusy)
84165
85- Button ( " Simulate Location " , action: simulate)
86- . buttonStyle ( . borderedProminent)
87- . disabled ( !pairingExists || isBusy)
166+ Button ( " Simulate Location " , action: simulate)
167+ . buttonStyle ( . borderedProminent)
168+ . disabled ( !pairingExists || isBusy)
169+ }
170+ } else {
171+ Text ( " Tap map to drop pin " )
172+ . font ( . subheadline)
173+ . foregroundStyle ( . secondary)
88174 }
89- } else {
90- Text ( " Tap map to drop pin " )
91- . font ( . subheadline)
92- . foregroundStyle ( . secondary)
93175 }
176+ . padding ( . bottom, 24 )
177+ . padding ( . horizontal, 16 )
94178 }
95- . padding ( . bottom, 24 )
96- . padding ( . horizontal, 16 )
97179 }
98180 . navigationBarTitleDisplayMode ( . inline)
181+ . toolbar {
182+ ToolbarItem ( placement: . topBarTrailing) {
183+ TextField ( " Search location... " , text: $searchText)
184+ . padding ( . leading, 6 )
185+ . autocorrectionDisabled ( )
186+ . onChange ( of: searchText) { _, newValue in
187+ searchCompleter. update ( query: newValue)
188+ }
189+ }
190+ }
99191 . alert ( alertTitle, isPresented: $showAlert) {
100192 Button ( " OK " , role: . cancel) { }
101193 } message: {
@@ -107,6 +199,18 @@ struct LocationSimulationView: View {
107199 }
108200 }
109201
202+ private func selectSearchResult( _ result: MKLocalSearchCompletion ) {
203+ searchText = " "
204+ searchCompleter. results = [ ]
205+
206+ let request = MKLocalSearch . Request ( completion: result)
207+ MKLocalSearch ( request: request) . start { response, _ in
208+ if let item = response? . mapItems. first {
209+ coordinate = item. placemark. coordinate
210+ }
211+ }
212+ }
213+
110214 private func simulate( ) {
111215 guard pairingExists, let coord = coordinate, !isBusy else { return }
112216 isBusy = true
0 commit comments