Swift 6 Concurrency Errors with MKLocalSearchCompleterDelegate results

Has anyone found a thread-safe pattern that can extract results from completerDidUpdateResults(MKLocalSearchCompleter) in the MKLocalSearchCompleterDelegate ?

I've downloaded the code sample from Interacting with nearby points of interest and notice the conformance throws multiple errors in Xcode 16 Beta 5 with Swift 6:

extension SearchDataSource: MKLocalSearchCompleterDelegate {

    nonisolated func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
        Task {
            let suggestedCompletions = completer.results
            await resultStreamContinuation?.yield(suggestedCompletions)
        }
    }

Error: Task-isolated value of type '() async -> ()' passed as a strongly transferred parameter; later accesses could race

and

Error: Sending 'suggestedCompletions' risks causing data races

Is there another technique I can use to share state of suggestedCompletions outside of the delegate in the code sample?

Answered by DTS Engineer in 799526022

Thank you for bringing that to our attention. Those compiler warnings in that sample are new to a recent Xcode beta, as I'm sure they weren't present in the earlier betas of Xcode 16. I'll see about getting that project updated so it compiles again.

In the mean time, you can bind the Task to the main actor and remove the await keyword, like this:

nonisolated func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
        Task { @MainActor in
            let suggestedCompletions = completer.results
            resultStreamContinuation?.yield(suggestedCompletions)
        }
    }

With the way this sample project is set up, search completions are very tied to the app UI, so you generally want to ensure that the completer code is associated with the main actor.

—Ed Ford,  DTS Engineer

Thank you for bringing that to our attention. Those compiler warnings in that sample are new to a recent Xcode beta, as I'm sure they weren't present in the earlier betas of Xcode 16. I'll see about getting that project updated so it compiles again.

In the mean time, you can bind the Task to the main actor and remove the await keyword, like this:

nonisolated func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
        Task { @MainActor in
            let suggestedCompletions = completer.results
            resultStreamContinuation?.yield(suggestedCompletions)
        }
    }

With the way this sample project is set up, search completions are very tied to the app UI, so you generally want to ensure that the completer code is associated with the main actor.

—Ed Ford,  DTS Engineer

Thank you, @DTS Engineer. I would also expect this approach to work, but the compiler disagrees on two points:

Task { @MainActor in
    let suggestedCompletions = completer.results
    // (with or without the .yield()
}

Error: Task or actor isolated value cannot be sent

and

Task { @MainActor in
...
resultStreamContinuation?.yield(suggestedCompletions)
}

Error: Sending 'suggestedCompletions' risks causing data races

Main actor-isolated 'suggestedCompletions' is passed as a 'sending' parameter; Uses in callee may race with later main actor-isolated uses

(for my own understanding): Is the error suggesting the completer.results reference may have changed by the time the Task { ... } is scheduled/executed?

Hi @b_rare, your message helped knock my brain into gear when I was really confused, thank you... in case this helps you or any other lost devs...

I was seeing this same Task or actor isolated value cannot be sent warning on Xcode 16.1 when I tried to do something very similar:

Unable to move from nonisolated to @MainActor with a Task

extension ViewController: MKLocalSearchCompleterDelegate {
  
  nonisolated func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
    Task { @MainActor in
      searchBar.searchTextField.searchSuggestions = completer.results.map { result in
        UISearchSuggestionItem(localizedSuggestion: result.title, localizedDescription: result.description)
      }
    }
  }

Fix

I have made the errors go away by mapping the results properties to a tuple with just the String values I needed:

extension ViewController: MKLocalSearchCompleterDelegate {
  
  nonisolated func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
    let results = completer.results.map { result in
      (result.title, result.description)
    }
    
    Task { @MainActor in
      searchBar.searchTextField.searchSuggestions = results.map { result in
        UISearchSuggestionItem(localizedSuggestion: result.0, localizedDescription: result.1)
      }
    }
  }

Why it works

My understanding is that since completer: MKLocalSearchCompleter and completer.results: [MKLocalSearchCompletion] are both classes (reference types) and not final, they are therefore NOT Sendable.

Strings, however, are value types, so they are Sendable and can be passed between isolation domains.

There are two considerations:

  1. The MKLocalSearchCompletion results, much less the MKLocalSearchCompleter, are not Sendable. So, we would map that to something that is. (The term of art is a “data transfer object”.) A tuple is fine, but a custom DTO object (that captures all four properties, but not description) would be best.

  2. The completerDidUpdateResults is a nonisolated requirement. So you probably want to get back to the main actor. If you were not on the main thread, you would use Task { @MainActor in … }. But since this delegate method is called on the main thread, MainActor.assumeIsolated {…} is preferable.

Thus:

extension ViewController: MKLocalSearchCompleterDelegate {
    nonisolated func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
        let results = completer.results.map(MKLocalSearchCompletionDTO.init)

        MainActor.assumeIsolated {
            handleCompleterResults(results)
        }
    }
}

Where:

struct MKLocalSearchCompletionDTO {
    let title: String
    let subtitle: String
    let titleHighlightRanges: [Range<String.Index>]
    let subtitleHighlightRanges: [Range<String.Index>]

    init(_ result: MKLocalSearchCompletion) {
        title = result.title
        subtitle = result.subtitle
        titleHighlightRanges = result.titleHighlightRanges
            .compactMap { Range($0.rangeValue, in: result.title) }
        subtitleHighlightRanges = result.subtitleHighlightRanges
            .compactMap { Range($0.rangeValue, in: result.subtitle) }
    }
}

Note, one would generally mirror the properties of the original object in the DTO rendition, but I am converting the two highlight ranges of [NSValue] to swiftier [Range<String.Index>] for easier use where the object is consumed. But that is a matter of personal preference.

Swift 6 Concurrency Errors with MKLocalSearchCompleterDelegate results
 
 
Q