Skip to content

Latest commit

 

History

History

InteractingWithNearbyPointsOfInterest

Interacting with nearby points of interest

Provide automatic search completions for a partial search query, search the map for relevant locations nearby, and retrieve details for selected points of interest.

Overview

This sample code project demonstrates how to programmatically search for map-based addresses and points of interest using a natural language string, and get more information for points of interest that a person selects on the map. The search results center around the locations visible in the map view.

Request search completions

MKLocalSearchCompleter retrieves autocomplete suggestions for a partial search query within a map region. A person can type cof, and a search completion suggests coffee as the query string.

/// Ask for completion suggestions based on the query text.
func provideCompletionSuggestions(for query: String) {
    /**
     Configure the search to return completion results based only on the options in the app. For example,
     someone can configure the app to exclude specific point-of-interest categories, or to only return results for addresses.
     */
    searchCompleter?.resultTypes = mapConfiguration.resultType.completionResultType
    searchCompleter?.regionPriority = mapConfiguration.regionPriority.localSearchRegionPriority
    if mapConfiguration.resultType == .pointsOfInterest {
        searchCompleter?.pointOfInterestFilter = mapConfiguration.pointOfInterestOptions.filter
    } else if mapConfiguration.resultType == .addresses {
        searchCompleter?.addressFilter = mapConfiguration.addressOptions.filter
    }
    
    searchCompleter?.region = mapConfiguration.region
    searchCompleter?.queryFragment = query
}

View in Source

As someone types a query into a search bar, the sample app updates the queryFragment through the UISearchResultsUpdating protocol.

func updateSearchResults(for searchController: UISearchController) {
     // Clear the search results when someone changes the query string. The updated query string may not match the existing search
     // results or the new suggested completions.
    displaySearchResults([])
    
    // Ask for new completion suggestions based on the change in the text that someone enters in `UISearchBar`.
    searchDataSource.provideCompletionSuggestions(for: searchController.searchBar.text ?? "")
}

View in Source

Receive completion results

Completion results represent fully formed query strings based on the query fragment someone types. The sample app uses completion results to populate UI elements to quickly fill in a search query. The app receives the latest completion results as an array of MKLocalSearchCompletion objects by adopting the MKLocalSearchCompleterDelegate protocol.

nonisolated func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
    Task {
        /**
         As a person types, new completion suggestions continuously return to this method. Update the property storing the current
         results, so that the app UI can observe the change and display the updated suggestions.
         */
        let suggestedCompletions = completer.results
        await resultStreamContinuation?.yield(suggestedCompletions)
    }
}

View in Source

An AsyncStream delivers the array of completion results to the code responsible for converting the contents of the array to UISearchSuggestionItem elements for display.

// Receive the search completions through an `AsyncStream` continuation that the search data source manages.
let searchCompletionStream = AsyncStream<[MKLocalSearchCompletion]>.makeStream()
searchDataSource.startProvidingSearchCompletions(with: searchCompletionStream.continuation)
searchCompletionsTask = searchCompletionsTask ?? Task { @MainActor in
    for await searchCompletions in searchCompletionStream.stream {
        
        // Use UIKit's search suggestions features to display the search completions.
        let completions = searchCompletions.map { completion in
            let suggestion = UISearchSuggestionItem(localizedAttributedSuggestion: completion.highlightedTitleStringForDisplay)
            suggestion.representedObject = completion
            return suggestion
        }
        searchController?.searchSuggestions = completions
    }
}

View in Source

Highlight the relationship of a query fragment to the suggestion

Within the UI elements that represent each query result, the sample code uses the titleHighlightRanges on an MKLocalSearchCompletion to show how the query someone enters relates to the suggested result. For example, the following code applies a highlight with NSAttributedString:

private func createHighlightedString(text: String, rangeValues: [NSValue]) -> NSAttributedString {
    let attributes = [NSAttributedString.Key.backgroundColor: UIColor(named: "suggestionHighlight")!]
    let highlightedString = NSMutableAttributedString(string: text)

    // Each `NSValue` wraps an `NSRange` that functions as a style attribute's range with `NSAttributedString`.
    let ranges = rangeValues.map { $0.rangeValue }
    for range in ranges {
        highlightedString.addAttributes(attributes, range: range)
    }

    return highlightedString
}

var highlightedTitleStringForDisplay: NSAttributedString {
    return createHighlightedString(text: title, rangeValues: titleHighlightRanges)
}

View in Source

Search for map items

An MKLocalSearch.Request takes either an MKLocalSearchCompletion or a natural language query string, and returns an array of MKMapItem objects. Each MKMapItem represents a geographic location, like a specific address, that matches the search query. The sample code asynchronously retrieves the array of MKMapItem objects by calling start(completionHandler:) on MKLocalSearch.

let search = MKLocalSearch(request: request)
currentSearch = search
defer {
    // After the search completes, the reference is no longer needed.
    currentSearch = nil
}

var results: [MKMapItem]

do {
    let response = try await search.start()
    results = response.mapItems
} catch let error {
    searchLogging.error("Search error: \(error.localizedDescription)")
    results = []
}

View in Source

Allow someone to select points of interest on the map

If a person is exploring the map, they can get information for a point of interest by tapping it. To provide these interactions, the sample code enables selectable map features as follows:

mapView?.selectableMapFeatures = [.pointsOfInterest]

// Filter out some point-of-interest categories based on selected settings within the app.
let mapConfiguration = MKStandardMapConfiguration()
mapConfiguration.pointOfInterestFilter = searchConfiguration.pointOfInterestOptions.filter
mapView?.preferredConfiguration = mapConfiguration

View in Source

When someone taps a point of interest, the system calls mapView(_:, selectionAccessoryFor:) on the MKMapViewDelegate with an MKMapFeatureAnnotation that represents the tapped item. The delegate returns an MKSelectionAccessory that displays the details of the map item. MapKit presents the map item's details using an MKMapItemDetailViewController, which includes information like a phone number, business hours, and buttons to start navigation to the location using Apple Maps.

/// This delegate method allows the selection of a point of interest on the map to show details about the selected item.
func mapView(_ mapView: MKMapView, selectionAccessoryFor annotation: any MKAnnotation) -> MKSelectionAccessory? {
    // Adapt the presentation to make the best use of space to see the most information possible. The `.automatic` presentation
    // style adapts to either a sheet presentation style or a callout presentation style.
    // The annotation that passes to this delegate method may be either `MKMapItemAnnotation` or `MKMapFeatureAnnotation`, depending
    // on whether the selected annotation is an annotation that the app adds to the map, or a feature that MapKit provides.
    .mapItemDetail(.automatic(presentationViewController: self))
}

View in Source

Persist and retrieve map items

If someone is exploring the map, they may want the app to store places they looked at so that they can come back to them later, including across app launches. MKMapItem has an identifier property, which the app stores in its VisitedPlace model using SwiftData.

guard let identifier = mapItem.identifier else { return }
let visit = VisitedPlace(id: identifier.rawValue)

View in Source

When the app launches, it retrieves the history of visited locations from SwiftData. To get the MKMapItem from the previously stored identifier, the app creates an MKMapItemRequest with the stored identifier and calls getMapItem(completionHandler:).

@MainActor
func convertToMapItem() async -> MKMapItem? {
    guard let identifier = MKMapItem.Identifier(rawValue: id) else { return nil }
    let request = MKMapItemRequest(mapItemIdentifier: identifier)
    var mapItem: MKMapItem? = nil
    do {
        mapItem = try await request.mapItem
    } catch let error {
        let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "Map Item Requests")
        logger.error("Getting map item from identifier failed. Error: \(error.localizedDescription)")
    }
    return mapItem
}

View in Source