imageНесколько лет назад я работал над проектом, который должен был иметь схожие функции со Snapchat и при этом быть узко тематическим Instagram-подобным приложением. Проект разрабатывался только под одну платформу — iOS. Естественно, во время разработки основной фичи — публикация фотографий, клиент внезапно захотел дополнительно добавить возможность отмечать место, где было сделано фото. В большинстве случаев, многие сразу вспоминают про Places API от Google и Facebook, но клиента не устраивал тот факт, что данные решения имели определенные лимиты. Поэтому после дополнительного ресерча, был найден еще один альтернативный вариант от Apple — CLPlacemark, который был бесплатным, и в документации на тот момент не было упоминаний о лимитах для ежедневного использования. Поскольку разработка для других платформ вообще изначально не планировалась, это казалось очень подходящим вариантом.

Документация Apple показывает, что CLPlacemark может предоставлять много подробностей о точке, а так же есть метод у CLGeocoder который позволяет легко, по названию места, вернуть массив CLPlacemark с нужными данными. Как оказалось, работает это все не настолько радужно.

Исходный код выглядел примерно так:

import CoreLocation

let geocoder = CLGeocoder()

func findPlace(name: String) {
	geocoder.geocodeAddressString(name) { placemarks, error in
		
		print(placemarks)
	}
}

findPlace(name: “New”)

При таком простом раскладе, geocoder всегда возвращает массив CLPlacemark, но загвоздка оказалась в том, что этот массив никогда не содержит более одного элемента. В итоге, на весь экран, где ожидался большой список плейсментов вроде: New York, New Zeland, Магазин New Balance и т.п, я получал только один какой-то элемент, который даже не всегда был релевантным тому, что я вводил.

После некоторой безуспешной борьбы с CLGeocoder мне коллега подсказал: “А ты не думал попробовать посмотреть, может у MatKit есть подобная возможность?” Как оказалось, MapKit имеет MKLocalSearch, где мы можем получать массив MKPlacemark, который наследуется от CLPlacemark. Схема, выглядела вполне рабочей, поэтому я начал пробовать этот подход:

import MapKit

let 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)

Результат

В данном случае, я получал в ответ массив с уже 10 элементами CLPlacemark. Такой результат выглядел более приемлемым, потому что в результате предоставлялся достаточный перечень. Но далеко не всегда, при начале ввода названия какого-либо из заведений, расположенных рядом, оно сразу показывало нужный результат. К примеру, рядом со мной находится Domino's Pizza. Мне хотелось, что бы когда я ввожу в строке такой запрос, в первую очередь получать заведения как можно ближе ко мне.

Я принялся изучать, на основе чего формируется массив, и как его можно улучшить. Я определил несколько вещей, которые могут влиять на выбор параметров:

  1. IP адрес с которого делается запрос к Apple. При включенном VPN объекты в выдаче были уже более приближены к локации VPN сервера.
  2. Текущая локация пользователя. Если будут передаваться в запрос текущие координаты пользователя, то результаты получатся намного точнее.
  3. Системный язык устройства.

Пример без VPN

Примеры с VPN
New York

Toronto

Kyiv

London

Frankfurt


Вполне возможно, есть и другие факторы, которые могут влиять на результаты поиска, но мне было этого достаточно для достижения нужного результата.

Дальнейшим ходом разработки было использование текущей локации девайса.

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
            let mapItems = response.mapItems // получаем ответ в формате MKMapItem
        }
    }

    // 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)
    }
}

Результат
Текущая локация девайса — Мадрид, интернет провайдер Vodafone ES

В методе делагата didUpdateLocations, мы создаем MKCoordinateSpan. Если я правильно понял документацию Apple, то чем меньше значение мы ставим latitude/longitude Delta, тем более узким (и точным) будет указываться наш текущий регион, поскольку он является своеобразным зумом на наших текущих координатах в MapKit.
После этого, действительно, приоритет выдачи изменился и мне показывало в первую очередь те места, которые рядом со мной.

Осталось только сделать более красивыми названия в списке. Так как иногда, некоторые свойства у CLPlacemark могут иметь одинаковые названия, в итоге это будет не очень красиво выглядеть: New York, New York, NY. Для этого необходимо создать отдельную Структуру, которая будет формировать красивое название в списке.

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 administrativeArea = item.placemark.administrativeArea,
            administrativeArea != item.placemark.locality {
            locationString += ", \(administrativeArea)"
        }
        
        if let country = item.placemark.country, country != item.name {
            locationString += ", \(country)"
        }
        
        location = locationString
    }
}

Тогда уже в ответе на поиск, мы можем легко смапить CLPlacemark в созданную структуру и передать ее в список.

localSearch?.start { [weak self] searchResponse, error in

            guard let items = searchResponse?.mapItems else {
                return
            }
            // Конвертируем CLPlacemark в созданную структуру  
            let placemarks = items.map { Placemark(item: $0) }
}

Теперь выдача смотрится более элегантно и ее можно уже использовать в проекте для отметок своих посещенных локаций.

Один из основных недостатков заключается в том, что использовать данное решение можно только если проект заточен под iOS/Mac OS. Если же проект подразумевает разработку для других платформ, я бы рекомендовал использовать решение от Google или Facebook. Так же, не во всех регионах идеально определяются все локации.

Итоговый код проекта вы можете посмотреть в репозитории.

Комментарии (0)