Search¶
This section explains how to implement online/offline search in iOS application.
Overview¶
Search is designed to resolve a free-formatted text query to an exact destination on a map
(including title, location, additional info). Also it can search for places of selected categories in
requested area.
Related classes: SYSearchBase
& child classes (e.g. SYOfflineMapSearch
),
SYSearchSession
, SYSearchRequest
, SYSearchResult
.
Reverse search takes location (SYGeoCoordinate
) and resolves it to an address (city, street, house number).
Related classes: SYReverseSearch
, SYReverseSearchResult
.
Info
SYOnlineSession.onlineMapsEnabled
parameter affects Reverse search, and doesn't affect Search.
Search engines¶
Various search engines can be used separately or combined with each other:
SYCoordinateSearch
- recognizesSYGeoCoordinate
by search input like "48.1457, 17.1269"SYOfflineMapSearch
- searches for cities, streets, places in map data already downloaded bySYMapInstaller
SYOnlineMapSearch
- searches for cities, streets, places using online map serviceSYFlatDataSearch
- searches in user-provided flat data items (title, subtitle, location)SYCustomPlacesSearch
- searches for places already installed bySYCustomPlacesManager
Warning
SYOfflineMapSearch
takes a lot of system resources, we recommend to share one instance
across the app, creating many independent sessions if needed (or many instances
of SYSearchSessionProvider
).
SYCustomPlacesSearch
might take some time to initialize because of indexing process,
we also recommend to share one instance across the app.
Composite search engine¶
To combine different search types, use a composite search. It distributes a request to all search engines it contains.
SYCompositeSequentialSearch
- calls search engines one by one, until something is found (if one search provides results, next search engines are not called)SYCompositeParallelSearch
- performs search with all searches at the same time, results are joined into one list
Info
SYSearchBase
is a parent class for all search engines- A search engine can be used separately while being a part of one or more composite searches
- A composite search can be a part of other composite searches
The following example creates a complex search for a navigation app (it can be used as a
replacement of SYSearch
, which is deprecated).
func makeComplexSearch() -> SYSearchBase {
let map = SYOfflineMapSearch()
let coordinate = SYCoordinateSearch()
let favorites = SYFlatDataSearch(priority: 1.0) // should be filled with data later
let history = SYFlatDataSearch(priority: 0.75) // should be filled with data later
return SYCompositeSequentialSearch(
searches: [
coordinate, // this goes first to quickly react when user enters exact coordinate
SYCompositeParallelSearch(searches: [map, favorites, history])
]
)
}
Search request types¶
Every search engine can create SYSearchSession
. Search session allows to perform search requests
on the corresponding search engine. There are 4 types of requests:
- Autocomplete - quick and lightweight, providing immediate suggestions while user is typing a text query, results can be shown in a list but not on the map
- Geocode Location - takes Location ID (from Autocomplete result in the same session) and provides full information about this exact search result
- Geocode - takes input text query, delivers matched results with full information, it makes sense when a user finished typing and pressed Search button
- Places - finds places by category in the selected area, results contain full information
Session lifecycle¶
Search engine may have more than one active session at once, but lifecycle of each session is limited.
Search session instance can perform any amount of Autocomplete requests, and only one of these: Geocode Location, Geocode, or Places. After that the session is finished, next search will be possible only in the next session.
Lifecycle examples:
- New session => Autocomplete => Geocode Location
- New session => Autocomplete => Autocomplete => Autocomplete => Geocode Location
- New session => Geocode
- New session => Autocomplete => Geocode
- New session => Autocomplete => Autocomplete => Geocode
- New session => Places
- New session => Autocomplete => Places
- New session => Autocomplete => Autocomplete => Places
To simplify the work with lifecycle, use SYSearchSessionProvider
method session()
. If the last returned session
is still valid, it will be returned again, otherwise a new session will be created.
Info
Inside of one search session, every next request cancels the previous request, if it's still in progress. For independent request processing, create several sessions.
Autocomplete and Geocode¶
Assuming SYContext
is already initialized, this example shows Autocomplete, Geocode Location and Geocode
requests. Results are printed to debug console in methods printAutocompleteResult
, printDetailedResult
.
class DemoSearch {
let search: SYSearchBase = SYOnlineMapSearch()
func demoAutocomplete() {
let request = SYSearchRequest(query: "tovarenska", location: SYGeoCoordinate(latitude: 48.1457, longitude: 17.1269))
request.maxResultsCount = 3
let session = search.startNewSession()
session.autocomplete(request) { (results: [SYSearchAutocompleteResult]?, status: SYSearchStatus) in
guard status == .success, let results = results else {
print("MYLOG: ERROR: Autocomplete status=\(status.rawValue)")
return
}
results.forEach { (result) in
self.printAutocompleteResult(result)
}
}
}
func demoAutocompleteAndGeocodeLocation() {
let request = SYSearchRequest(query: "tovarenska", location: SYGeoCoordinate(latitude: 48.1457, longitude: 17.1269))
request.maxResultsCount = 3
let session = search.startNewSession()
session.autocomplete(request) { (suggestions: [SYSearchAutocompleteResult]?, status: SYSearchStatus) in
guard status == .success, let suggestions = suggestions else {
print("MYLOG: ERROR: Autocomplete status=\(status.rawValue)")
return
}
guard let firstLocationId = suggestions.first?.locationId else {
print("MYLOG: Autocomplete no results")
return
}
let request = SYGeocodeLocationRequest(locationId: firstLocationId)
session.finish(byRequestingGeocodeLocation: request) { (result: SYSearchGeocodingResult?, status: SYSearchStatus) in
guard status == .success, let result = result else {
print("MYLOG: ERROR: GeocodeLocation status=\(status.rawValue)")
return
}
self.printDetailedResult(result)
}
}
}
func demoGeocode() {
let request = SYSearchRequest(query: "airport", location: SYGeoCoordinate(latitude: 48.1457, longitude: 17.1269))
let session = search.startNewSession()
session.finish(byRequestingGeocode: request) {
(results: [SYSearchGeocodingResult]?, status: SYSearchStatus) in
guard status == .success, let results = results else {
print("MYLOG: ERROR: Geocode status=\(status.rawValue)")
return
}
results.forEach { (result) in
self.printDetailedResult(result)
}
}
}
func printAutocompleteResult(_ result: SYSearchAutocompleteResult) {
let title = result.title?.value ?? ""
print("MYLOG: Autocomplete: '\(title)', type=\(result.type.rawValue), distance=\(result.distance)")
}
func printDetailedResult(_ result: SYSearchGeocodingResult) {
let title = result.title?.value ?? ""
let lat = result.location?.latitude ?? 0
let lon = result.location?.longitude ?? 0
print("MYLOG: GeocodingResult: '\(title)', type=\(result.type.rawValue)); \(lat),\(lon)")
if let mapResult = result as? SYSearchMapResult {
// mapResult provides more specific info
} else if let flatDataResult = result as? SYSearchFlatDataResult {
// flatDataResult provides more specific info
} // else if let ... SYSearch...Result
}
}
Places¶
You can also search places around a specific coordinate in a radius or in a Bounding Box. Here's an example on how to do it.
Info
To request more places with the same request, use SYSearchMorePlacesSession
provided in completion.
The number of places per page is configured with request.maxResultsCount
.
extension DemoSearch {
func placesRequestLocation() -> SYSearchPlacesRequest {
SYSearchPlacesRequest(
location: SYGeoCoordinate(latitude: 48.1457, longitude: 17.1269),
radius: 10000,
tags: [SYPlaceCategoryPetrolStation, SYPlaceCategoryPharmacy]
)
}
func placesRequestBoundingBox() -> SYSearchPlacesRequest {
SYSearchPlacesRequest(
boundingBox: SYGeoBoundingBox(
topLeft: SYGeoCoordinate(latitude: 48.1495, longitude: 17.1038),
bottomRight: SYGeoCoordinate(latitude: 48.1395, longitude: 17.1181)
),
tags: [SYPlaceCategoryRestaurant]
)
}
func demoPlaces() {
let request = placesRequestLocation() //or placesRequestBoundingBox()
let session = search.startNewSession()
session.finish(byRequestingPlaces: request) {
(places: [SYPlace]?, morePlacesSession: SYSearchMorePlacesSession?, status: SYSearchStatus) in
guard status == .success, let places = places else {
print("MYLOG: ERROR: Places status=\(status.rawValue)")
return
}
places.forEach { (place) in
self.printPlace(place)
}
// morePlacesSession can be saved and then used to get more places for the same request
}
}
func printPlace(_ place: SYPlace) {
print("MYLOG: ---")
let lat = place.link.coordinate.latitude
let lon = place.link.coordinate.longitude
print("MYLOG: Place name: \(place.link.name); cat: \(place.link.category); \(lat),\(lon)")
place.details.forEach { (key, value) in
print("MYLOG: Place detail: \(key): \(value)")
}
}
}
Reverse search¶
Reverse search (also called Reverse geocoding) converts a location (SYGeoCoordinate
) to an address
(country, city, street, house number). Current state of SYOnlineSession.onlineMapsEnabled
affects if the online or offline map data is used. For offline, the corresponding country must be already
downloaded by SYMapInstaller
.
class DemoReverseGeocoding {
let search = SYReverseSearch()
func reverseSearch() {
let coordinate = SYGeoCoordinate(latitude: 48.146410, longitude: 17.138270)
search.reverseSearch(with: coordinate, withFilter: nil) { (results: [SYReverseSearchResult]?, error) in
guard let results = results else {
print("SYReverseSearch error: \(String(describing: error))")
return
}
results.forEach { $0.printDescription() }
}
}
}
extension SYReverseSearchResult {
func printDescription() {
let description = resultDescription
print("""
\nSYReverseSearchResult:
country: \(description.countryIso)
city: \(String(describing: description.city))
street: \(String(describing: description.street))
house number: \(String(describing: description.houseNumber))
distance: \(distance)
""")
}
}
Get the timezone of a location¶
One of the two prerequisites need to be met in order to get the time zone of a location, otherwise the operation will finish with error:
- The coordinate must be inside of already downloaded maps
- Online maps need to be active
Asynchronously returns time zone (in seconds from GMT) at a specific location and in a specific time (result may differ for summer/winter time). It's recommended to use the result for creation of a NSTimeZone object.
Here we use the current time to get the seconds from GMT at a location and directly convert it to a TimeZone object.
SYReverseSearch().getTimeZone(atLocation: SYGeoCoordinate(latitude: 12.34, longitude: 56.78), atTime: Date()) { result, error in
let timezone = TimeZone(secondsFromGMT: result as! Int)
}
Custom Places Search¶
Custom Places Search is a search engine that searches for places that were installed by SYCustomPlacesManager
.
It can be used separately or as a part of a composite search.
If you are using the Online mode where the places are not downloaded to the device, you can still search for the custom
places that are stored on the server.
These places are automatically included in the results when calling the SYOnlineMapSearch
's searchPlaces()
or
autocomplete()
or geocode()
function. You can read about them in the previous chapters.
To search for Custom Places that are stored locally, you can create the SYCustomPlacesSearch
class which you can use directly
for searching or add it to a composite/parallel search.
Warning
However, please note that the downloaded places need to be indexed after the installation and that this can take a considerable amount of time depending on the number of imported places but also the device's performance.
The Custom places search will be unavailable until the indexing is finished.
To check whether the indexing is finished, the SYCustomPlacesManagerDelegate
has two callbacks that can be implemented:
class BasicCustomPlacesSearch: SYCustomPlacesManagerDelegate {}
extension BasicCustomPlacesSearch {
func customPlacesManager(_ manager: SYCustomPlacesManager, didStartCreatingSearchIndexForDataset datasetId: String) {
print("started indexing for dataset \(datasetId)")
}
func customPlacesManager(_ manager: SYCustomPlacesManager, didFinishCreatingSearchIndexForDataset datasetId: String, withError error: Error?) {
print("finished indexing for dataset \(datasetId)")
}
}
Note
The callbacks are sent for batches of indexing jobs and not for each country / JSON separately. That means that if two or more imports/installs finish at the same time and the indexing of the first one takes longer, the callback will be sent only after the indexing of the last one finishes.