I try to make multiple searches synchronously (I mean one after the other, waiting for the previous request to complete before running the next one) and block till all the operations are complete before going ahead.
But completion handle of the local search looks like blocked and run once the semaphore gives up. I have made many attempts without success.
My code and logs are as follows (you can copy/paste to the playground):
import CoreLocation
import MapKit
func search(_ query: String, in span: MKCoordinateSpan, centered center: CLLocationCoordinate2D, id: Int) {
let semaphore = DispatchSemaphore(value: 0)
//let group = DispatchGroup(); group.enter()
// Run the request for this rect
print("\(#function): local search on the \(id)th portion ")
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = query
request.region = MKCoordinateRegion(center: center, span: span)
if #available(iOS 13, *) {
request.resultTypes = .pointOfInterest
}
let search = MKLocalSearch(request: request)
search.start { response, error in
print("\(id) got \(response?.mapItems.count) items")
semaphore.signal()
}
let s = semaphore
//let s = group
// Wait for the request ot complete
print("\(#function): waiting for the \(id)th portion to complete")
//guard _ = s.wait(wallTimeout: .distantFuture) else {
guard s.wait(timeout: .now() + 5) == .success else {
print("\(#function): ***Warning: \(id)th timeout, job incomplete")
return
}
print("\(#function): \(id)th completed")
}
let rect = CGRect(
x: 48.10,
y: 3.43,
width: 0.09,
height: 0.09
)
let n = 4
let latDelta = rect.width / CGFloat(n)
var latOffs = rect.minX
let queue = OperationQueue()
//queue.maxConcurrentOperationCount = 1
var ops = [BlockOperation]()
// -- Run all asyn loca search requests synchronuously
for i in 0..<n {
// Take the next cut of the original region
let portion = CGRect(
x: latOffs,
y: rect.minY,
width: latDelta,
height: rect.height
)
latOffs += latDelta
ops.append(BlockOperation { [portion, i] in
let center = CLLocationCoordinate2D(latitude: CLLocationDegrees(portion.midX), longitude: CLLocationDegrees(portion.midY))
let span = MKCoordinateSpan(latitudeDelta: CLLocationDegrees(portion.width), longitudeDelta: CLLocationDegrees(portion.height))
search("coffee", in: span, centered: center, id: i)
})
}
queue.addOperations(ops, waitUntilFinished: true)
print("All done")
The current bogus output:
search(_:in:centered:id:): local search on the 1th portion
search(_:in:centered:id:): local search on the 2th portion
search(_:in:centered:id:): local search on the 3th portion
search(_:in:centered:id:): local search on the 0th portion
search(_:in:centered:id:): waiting for the 1th portion to complete
search(_:in:centered:id:): waiting for the 3th portion to complete
search(_:in:centered:id:): waiting for the 2th portion to complete
search(_:in:centered:id:): waiting for the 0th portion to complete
search(_:in:centered:id:): ***Warning: 0th timeout, job incomplete
search(_:in:centered:id:): ***Warning: 2th timeout, job incomplete
search(_:in:centered:id:): ***Warning: 1th timeout, job incomplete
search(_:in:centered:id:): ***Warning: 3th timeout, job incomplete
All done
0 got Optional(10) items
2 got Optional(10) items
1 got Optional(10) items
3 got Optional(10) items
[UPDATE]
The expected output should show no ***Warning
and All done
as the last line, as follows (the exact order of the numbering depends on the network conditions):
search(_:in:centered:id:): local search on the 1th portion
search(_:in:centered:id:): local search on the 2th portion
search(_:in:centered:id:): local search on the 3th portion
search(_:in:centered:id:): local search on the 0th portion
search(_:in:centered:id:): waiting for the 1th portion to complete
search(_:in:centered:id:): waiting for the 3th portion to complete
search(_:in:centered:id:): waiting for the 2th portion to complete
search(_:in:centered:id:): waiting for the 0th portion to complete
0 got Optional(10) items
search(_:in:centered:id:): 0th completed
2 got Optional(10) items
search(_:in:centered:id:): 2th completed
1 got Optional(10) items
search(_:in:centered:id:): 1th completed
3 got Optional(10) items
search(_:in:centered:id:): 3th completed
All done
[UPDATE 2] the outputted when uncommenting the line //queue.maxConcurrentOperationCount = 1
search(:in:centered:id:): local search on the 0th portion 2020-03-28 23:49:41 +0000 search(:in:centered:id:): waiting for the 0th portion to complete 2020-03-28 23:49:41 +0000 search(:in:centered:id:): ***Warning: 0th timeout, job incomplete 2020-03-28 23:49:46 +0000 search(:in:centered:id:): local search on the 1th portion 2020-03-28 23:49:46 +0000 search(:in:centered:id:): waiting for the 1th portion to complete 2020-03-28 23:49:46 +0000 search(:in:centered:id:): ***Warning: 1th timeout, job incomplete 2020-03-28 23:49:51 +0000 search(:in:centered:id:): local search on the 2th portion 2020-03-28 23:49:51 +0000 search(:in:centered:id:): waiting for the 2th portion to complete 2020-03-28 23:49:51 +0000 search(:in:centered:id:): ***Warning: 2th timeout, job incomplete 2020-03-28 23:49:56 +0000 search(:in:centered:id:): local search on the 3th portion 2020-03-28 23:49:56 +0000 search(:in:centered:id:): waiting for the 3th portion to complete 2020-03-28 23:49:56 +0000 search(:in:centered:id:): ***Warning: 3th timeout, job incomplete 2020-03-28 23:50:01 +0000 All done 2020-03-28 23:50:01 +0000 0 got Optional(10) items 2020-03-28 23:50:02 +0000 3 got Optional(10) items 2020-03-28 23:50:02 +0000 2 got Optional(10) items 2020-03-28 23:50:02 +0000 1 got Optional(10) items 2020-03-28 23:50:02 +0000
Note: Btw, I also added \(Date())
at the end of each print
If you want these operations to behave in a serial manner, you have to specify that the queue can only run one at a time, e.g.
queue.maxConcurrentOperationCount = 1
And, as you discovered, you want to avoid using waitUntilFinished
option of addOperations
, as that blocks the current thread until the operations are done. Instead, use completion handler pattern.
Here is the code that I used:
func performMultipleSearches(completion: @escaping () -> Void) {
let searches = ["restaurant", "coffee", "hospital", "natural history museum"]
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
for (i, searchText) in searches.enumerated() {
queue.addOperation {
self.search(searchText, in: self.mapView.region, id: i)
}
}
queue.addOperation {
completion()
}
}
func search(_ query: String, in region: MKCoordinateRegion, id: Int) {
let semaphore = DispatchSemaphore(value: 0)
os_log("%d starting", id)
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = query
request.region = region
if #available(iOS 13, *) {
request.resultTypes = .pointOfInterest
}
let search = MKLocalSearch(request: request)
search.start { response, error in
defer { semaphore.signal() }
guard let mapItems = response?.mapItems else {
os_log(" %d failed", id)
return
}
os_log(" %d succeeded, found %d:", id, mapItems.count)
}
os_log(" %d waiting", id)
guard semaphore.wait(timeout: .now() + 5) == .success else {
os_log(" %d timedout", id)
return
}
os_log(" %d done", id)
}
That produced:
2020-03-28 16:16:25.219565-0700 MyApp[46601:2107182] 0 starting
2020-03-28 16:16:25.220018-0700 MyApp[46601:2107182] 0 waiting
2020-03-28 16:16:25.438121-0700 MyApp[46601:2107033] 0 succeeded, found 10:
2020-03-28 16:16:25.438269-0700 MyApp[46601:2107182] 0 done
2020-03-28 16:16:25.438436-0700 MyApp[46601:2107182] 1 starting
2020-03-28 16:16:25.438566-0700 MyApp[46601:2107182] 1 waiting
2020-03-28 16:16:25.639198-0700 MyApp[46601:2107033] 1 succeeded, found 10:
2020-03-28 16:16:25.639357-0700 MyApp[46601:2107182] 1 done
2020-03-28 16:16:25.639490-0700 MyApp[46601:2107182] 2 starting
2020-03-28 16:16:25.639598-0700 MyApp[46601:2107182] 2 waiting
2020-03-28 16:16:25.822085-0700 MyApp[46601:2107033] 2 succeeded, found 10:
2020-03-28 16:16:25.822274-0700 MyApp[46601:2107182] 2 done
2020-03-28 16:16:25.822422-0700 MyApp[46601:2107162] 3 starting
2020-03-28 16:16:25.822567-0700 MyApp[46601:2107162] 3 waiting
2020-03-28 16:16:26.015566-0700 MyApp[46601:2107033] 3 succeeded, found 1:
2020-03-28 16:16:26.015696-0700 MyApp[46601:2107162] 3 done
2020-03-28 16:16:26.015840-0700 MyApp[46601:2107162] all done
For what it’s worth, I wouldn’t use semaphores and instead would use an asynchronous Operation
subclass. For example, you can use the AsynchronousOperation
class defined here, and then do:
class SearchOperation: AsynchronousOperation {
let identifier: Int
let searchText: String
let region: MKCoordinateRegion
init(identifier: Int, searchText: String, region: MKCoordinateRegion) {
self.identifier = identifier
self.searchText = searchText
self.region = region
super.init()
}
override func main() {
os_log("%d started", identifier)
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = searchText
request.region = region
if #available(iOS 13, *) {
request.resultTypes = .pointOfInterest
}
let search = MKLocalSearch(request: request)
search.start { response, error in
defer { self.finish() }
guard let mapItems = response?.mapItems else {
os_log(" %d failed", self.identifier)
return
}
os_log(" %d succeeded, found %d:", self.identifier, mapItems.count)
}
}
}
And then
let searches = ["restaurant", "coffee", "hospital", "natural history museum"]
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
for (i, searchText) in searches.enumerated() {
queue.addOperation(SearchOperation(identifier: i, searchText: searchText, region: mapView.region))
}
queue.addOperation {
completion()
}