-
Notifications
You must be signed in to change notification settings - Fork 313
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add MKLocalSearch to the Location pane #180
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
ec5ca52
Add MKLocalSearch to the Location pane
markerpen 728b5df
Added comments to LocalSearchController
markerpen 569d6cb
Cleanup models
markerpen f118f13
Cleanup models
markerpen d8808dc
Update ControlRoom/Controllers/LocalSearchController.swift
markerpen File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
// | ||
// LocalSearchController.swift | ||
// ControlRoom | ||
// | ||
// Created by John McEvoy on 29/11/2023. | ||
// Copyright © 2023 Paul Hudson. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
import MapKit | ||
|
||
@MainActor | ||
class LocalSearchController: NSObject, ObservableObject { | ||
/// Prevents duplicate queries from being made | ||
private var lastQuery: String = "" | ||
|
||
/// Completion handler is called by the `MKLocalSearchCompleter` success callback | ||
private var callback: (([LocalSearchResult]) -> Void)? | ||
|
||
/// the MKLocalSearchCompleter used to make local search requests | ||
private lazy var localSearchCompleter: MKLocalSearchCompleter = { | ||
let completer = MKLocalSearchCompleter() | ||
completer.resultTypes = [.address, .pointOfInterest] | ||
completer.delegate = self | ||
return completer | ||
}() | ||
|
||
/** | ||
Finds places and POIs using a query string and a geographical point to focus on. | ||
|
||
- Parameter for: The partial (autocomplete) query to search for. | ||
- Parameter around: Provides a hint for `MKLocalSearchCompleter` to search around a geographical point. | ||
- Parameter completion: Called if valid search results are found. | ||
|
||
- Returns: If a location is found immediately (a coordinate was pasted in, for example), returns a `Location`. | ||
*/ | ||
func search( | ||
for query: String, | ||
around location: Location, | ||
completion: @escaping ([LocalSearchResult]) -> Void | ||
) -> Location? { | ||
guard query.isNotEmpty, query != lastQuery else { return nil } | ||
callback = completion | ||
lastQuery = query | ||
|
||
if let location = parseCoordinates(query) { | ||
return location | ||
} | ||
|
||
localSearchCompleter.queryFragment = query | ||
localSearchCompleter.region = MKCoordinateRegion( | ||
center: location.center, | ||
latitudinalMeters: CLLocationDistance(20000), | ||
longitudinalMeters: CLLocationDistance(20000) | ||
) | ||
|
||
return nil | ||
} | ||
|
||
/** | ||
Converts an incomplete `LocalSearchResult` to a `Location` with coordinates and map bounds. | ||
|
||
- Parameter result: The `LocalSearchResult` to convert. | ||
- Parameter completion: Called if a valid `Location` is created. | ||
*/ | ||
func select(_ result: LocalSearchResult, completion: @escaping (Location) -> Void) { | ||
guard let completer = result.completer else { return } | ||
|
||
Task { | ||
do { | ||
let request = MKLocalSearch.Request(completion: completer) | ||
let response = try await MKLocalSearch(request: request).start() | ||
guard let mapItem = response.mapItems.first else { return } | ||
let location = Location( | ||
id: result.id, | ||
name: result.title, | ||
latitude: mapItem.placemark.coordinate.latitude, | ||
longitude: mapItem.placemark.coordinate.longitude, | ||
latitudeDelta: response.boundingRegion.span.latitudeDelta, | ||
longitudeDelta: response.boundingRegion.span.longitudeDelta) | ||
completion(location) | ||
} catch { | ||
print("\(error)") | ||
} | ||
} | ||
} | ||
|
||
/// Uses a regex to detect if a string is a lat/long coordinate (e.g. `'37.33467, -122.00898'`) | ||
private func parseCoordinates(_ coordinateString: String) -> Location? { | ||
do { | ||
let regexSearch = try Regex("^-?(?:[1-8]?\\d(?:\\.\\d+)?|90(?:\\.0+)?),\\s*-?(?:180(?:\\.0+)?|1[0-7]\\d(?:\\.\\d+)?|\\d{1,2}(?:\\.\\d+)?)$") | ||
|
||
guard coordinateString.ranges(of: regexSearch).isNotEmpty else { | ||
return nil | ||
} | ||
|
||
let components = coordinateString.split(separator: ",").map { String($0).trimmingCharacters(in: .whitespaces) } | ||
|
||
guard let latitude = Double(components[0]), let longitude = Double(components[1]) else { | ||
return nil | ||
} | ||
|
||
return Location( | ||
id: UUID(), | ||
name: "Map coordinate", | ||
latitude: latitude, | ||
longitude: longitude) | ||
|
||
} catch { | ||
return nil | ||
} | ||
} | ||
} | ||
|
||
/// Adds `MKLocalSearchCompleterDelegate` conformance so the controller can use the delegate's callback methods | ||
extension LocalSearchController: MKLocalSearchCompleterDelegate { | ||
/// Called if `MKLocalSearchCompleter` return valid results from a query string | ||
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) { | ||
guard let callback else { return } | ||
let results = completer.results.map { | ||
LocalSearchResult( result: $0 ) | ||
} | ||
callback(results) | ||
} | ||
|
||
/// Called if `MKLocalSearchCompleter` encounters an error | ||
func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) { | ||
print(error) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
// | ||
// LocalSearchResult.swift | ||
// ControlRoom | ||
// | ||
// Created by John McEvoy on 29/11/2023. | ||
// Copyright © 2023 Paul Hudson. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
import MapKit | ||
|
||
/// A local search result item | ||
struct LocalSearchResult: Identifiable { | ||
var id: UUID | ||
var title: String | ||
var subtitle: String? | ||
var completer: MKLocalSearchCompletion? | ||
|
||
init(result: MKLocalSearchCompletion) { | ||
id = UUID() | ||
self.title = result.title | ||
self.subtitle = result.subtitle.clean() | ||
self.completer = result | ||
} | ||
} | ||
|
||
/// if a string is empty or whitespace, convert it to `nil` | ||
extension String { | ||
func clean() -> String? { | ||
let cleanString = self.trimmingCharacters(in: .whitespacesAndNewlines) | ||
|
||
if cleanString.isEmpty { | ||
return nil | ||
} | ||
|
||
return cleanString | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MKLocalSearchCompleter
is an old delegate-based protocol, so I don't think it throws (I could be wrong, I don't spend much time with delegates any more) - it just sends theError
to thedidFailWithError
function. Suggestions to improve?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No suggestion, I didn't know about
MKLocalSearchCompleter
, it was just for know why you did that, thanks.