Using native Apple’s Places

Andrew Konovalskyi
5 min readSep 20, 2019

--

Thanks to Vladimir Liubarskiy for this amazing animation!

A few years ago I worked on a project that was supposed to have similar features with Snapchat and at the same time be a narrowly thematic Instagram-like application. The project was developed only for one platform -iOS. Naturally, during the development of the main feature - publishing photos, the client suddenly wanted to add the ability to mark the place where the photo was taken. In most cases, many immediately recall the Places API from Google and Facebook, but the client was not happy with the fact that these solutions had certain limits. Therefore, after an additional review, another alternative from Apple was found - CLPlacemark, which was free, and the documentation at that time did not mention limits for daily use. Since development for other platforms was not originally planned at all, this seemed like a very suitable option.

Apple’s documentation shows that CLPlacemark can provide a lot of details about the point, and CLGeocoder also has a method that allows you to easily return the CLPlacemark array with the required data by location name. As it turned out, it’s not all so good.

The source code looked something like this:

import CoreLocationlet geocoder = CLGeocoder()func findPlace(name: String) {
geocoder.geocodeAddressString(name) { placemarks, error in

print(placemarks)
}
}

In this simple scenario, geocoder always returns a CLPlacemark array, but the catch was that this array never contains more than one element. As a result, on the whole screen, where a large list of placements was expected like: New York, New Zeland, New Balance Store, etc. I received only one element that was not always relevant to what I entered.

After some unsuccessful struggle with CLGeocoder, my colleague told me:

Maybe you try to look MapKit, and he has a similar opportunity?

As it turned out, MapKit has MKLocalSearch, where we can get the MKPlacemark array, which is inherited from CLPlacemark. The thing looked quite working, so I started trying this approach:

import MapKitlet request = MKLocalSearchRequest()
var localSearch: MKLocalSearch?
func findPlace(name: String) {

request.naturalLanguageQuery = text
localSearch = MKLocalSearch(request: request)
localSearch?.start { (searchResponse, _) in
guard let items = searchResponse?.mapItems else {
return
}
print(items)
}
}
findPlace(name: “New)
Result

In this case, I received an array with 10 CLPlacemark elements in the response. Such a result seemed more acceptable because in result I received several places in the response. But not always when I start entering the name of any of the institutions located nearby, it immediately showed the desired result. I want to find, all the Domino’s Pizza cafes nearby me. So, when I enter such a query in the line, first of all, need try to get the establishments as close to me as possible.

I began to study on the basis of what the response is formed, and how it can be improved. Finally, I identified several things that can influence the response:

  1. The IP address from which the request is made to Apple. With the VPN turned on, the objects in the results were already closer to the location of the VPN server.
  2. The current location of the user. If the current user coordinates are sent to the request, the results will be much more accurate.
  3. The system language of the device.

Example without VPN and geolocation:

Example without VPN

Example with VPN from London, Toronto, New York:

Results when I use VPN service from London, Toronto, New York

Quite possibly, there are other factors that can influence the search results, but this was enough for me to achieve the desired result.

The further development plan was the use of the current location of the device:

import UIKit
import MapKit
import CoreLocation
final class ViewController: UIViewController, CLLocationManagerDelegate { private let locationManager = CLLocationManager()
private let request = MKLocalSearch.Request()
private var localSearch: MKLocalSearch?
private var region = MKCoordinateRegion()
override func viewDidLoad() {
super.viewDidLoad()

if CLLocationManager.locationServicesEnabled() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
}
}
func searchPlace(_ place: String) {
localSearch?.cancel()

request.naturalLanguageQuery = place
request.region = region
localSearch = MKLocalSearch(request: request)
localSearch?.start { [weak self] response, error in
// received response as MKMapItem items
let mapItems = response.mapItems
}
}
// MARK: - CLLocationManagerDelegate
func locationManager(_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]) {

guard let lastLocation = locations.last else {
return
}
let span = MKCoordinateSpan(latitudeDelta: 0.5,
longitudeDelta: 0.5)
region = MKCoordinateRegion(center: lastLocation.coordinate,
span: span)
}
}
Result. The current location of the device — Madrid, Internet provider Vodafone ES

In the delegate method didUpdateLocations, we create MKCoordinateSpan. If I correctly understood the Apple documentation, then the smaller the value we set latitude/longitude Delta, the narrower (and more accurate) our current region will be indicated, since it is a kind of zoom at our current coordinates in MapKit.

After that, indeed, the order in the response changed and showed me primarily those places that are next to me.

It remains only to make the names in the list more beautiful. Since sometimes, some properties of CLPlacemark may have the same name, in the end, it will not look very nice: New York, New York, NY. To do this, you need to create a separate Structure, which will form a beautiful name on the list.

import Foundation
import MapKit
struct Placemark {

let location: String

init(item: MKMapItem) {

var locationString: String = ""

if let name = item.name {
locationString += "\(name)"
}

if let locality = item.placemark.locality,
locality != item.name {
locationString += ", \(locality)"
}

if let adminArea = item.placemark.administrativeArea,
adminArea != item.placemark.locality {
locationString += ", \(adminArea)"
}

if let country = item.placemark.country,
country != item.name {
locationString += ", \(country)"
}

location = locationString
}
}

Then, already in response to the search, we can easily map CLPlacemark into the created structure and pass it to the TableView.

localSearch?.start { [weak self] response, error in   guard let items = searchResponse?.mapItems else {
return
}
// Convert CLPlacemark to the created structure
let placemarks = items.map { Placemark(item: $0) }
}

Now the results look more elegant and can already be used in the project to mark their visited locations.

One of the main disadvantages is that you can use this solution only if the project is developed for iOS/Mac OS. If the project involves development for other platforms, I would recommend using a solution from Google or Facebook. Also, not all locations are ideally defined in all regions.

You can see the final code of the project in the repository.

--

--