Pull to refresh
0

Разработка iOS-приложения Aviasales.ru. Экран выбора аэропортов

Reading time4 min
Views8.8K
При создании приложения Aviasales.ru для iOS перед нами стояло много интересных задач. Одна из них — реализация удобного механизма выбора пунктов отправления и назначения. В этом посте мы бы хотели вкратце рассказать, как мы эту задачу решали и какие возможности iOS SDK при этом использовали.



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

Экран выбора аэропорта оправления разделен на три части: ближайшие аэропорты, список аэропортов, выбранных пользователем ранее, и строка ввода названия аэропорта.

Функциональность ближайших аэропортов реализуется в два этапа: сначала приложение, предварительно спросив разрешения у пользователя, узнаёт координаты девайса. Кстати, в запрос на получение геолокационных данных можно добавить уточнение, с какой, собственно, целью интересуемся. Для этого можно воспользоваться свойством purpose у CLLocationManager:

locationManager.purpose = @”Для определения ближайших аэропортов”;



Нам не нужны точные координаты пользователя, достаточно знать, рядом с каким городом он находится. Поэтому точность геолокации можно снизить:

locationManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers;

Второй этап — полученные координаты отправляются на наш сервер, который определяет, какие аэропорты могут быть интересны пользователю. Например, жителям Санкт-Петербурга, помимо Пулково, предлагается финская Лаппеэнранта.

История поиска — это последние пять выбранных пунктов, которые сохраняются в базе данных (используем SQLite с Core Data, ничего сложного).

Перейдём к самому интересному — поиску аэропортов и городов по строке, вводимой пользователем. Поиск работает в три этапа:
  1. ищем точное совпадение в списке популярных аэропортов;
  2. если ничего не нашлось, ищем неточные совпадения в том же списке;
  3. если пользователь ввёл более двух символов, у него появляется возможность отправить поисковый запрос на сервер, чтобы отыскать менее популярный аэропорт.

Теперь по порядку.

Аэропортов в мире, оказывается, великое множество — около 10 тысяч. Но реально популярных из них (тех, которые пользователи с определенной регулярностью используют в поисковых запросах) — лишь полторы тысячи. Мы решили этот список популярных направлений изначально поставлять внутри приложения, чтобы при первом запуске пользователь мог выбрать нужный город максимально быстро. Для хранения информации об этих аэропортах мы также используем SQLite с Core Data (на Хабре есть статья о том, как предзагрузить данные Core Data в приложение). При запуске приложение считывает данные об аэропортах из базы в массив:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  NSManagedObjectContext* context = [[ASCoreDataManager sharedInstance] currentManagedObjectContext]; 
  NSArray *dbAirports = [ASAirport MR_findAllInContext:context];
  @synchronized(self){
    _airports = dbAirports;
  }
});


Мы используем механизм Grand Central Dispatch, чтобы произвести эту операцию в фоновом потоке. Вызов через synchronized необходим для обеспечения безопасности доступа к памяти при многопоточной работе.

“Что за метод MR_findAllInContext?” — спросите вы. Это функция библиотеки MagicalRecord. Дело в том, что сам по себе механизм Core Data не является потокобезопасным. На практике это может привести к падению приложения, если fetch запросы на чтение отправляются из разных потоков. Это решается использованием отдельных NSManagedObjectContext для каждого потока, при этом persistentStoreCoordinator у них у всех будет общим. Координировать все эти контексты и помогает библиотека MagicalRecord.

Во-первых, в ней реализован метод [NSManagedObjectContext MR_contextForCurrentThread] (используется в методе currentManagedObjectContext из кода выше), который возвращает нам контекст для того потока, где он был вызван. 
Во-вторых, в MagicalRecord есть много упрощающих жизнь оберток над стандартными блоками кода: например, создание разнообразных NSManagedObjectModel, NSPersistentStoreCoordinator, NSManagedObject и их наследников можно делать в одну строчку, просто задав необходимые параметры.

Иногда данные о популярных аэропортах обновляются — одни направления приобретают популярность, другие, напротив, становятся менее актуальными, какие-то меняют названия. Поэтому периодически приложение скачивает с сервера сжатый с помощью gzip JSON-файл, данными из которого, так же в фоновом потоке, обновляется база данных. Сначала очищаем базу от записей:

[ASAirport MR_truncateAllInContext:context];

Затем записываем новые данные:

NSManagedObjectContext* context = [[ASCoreDataManager sharedInstance] currentManagedObjectContext];
        
for (APIAirport *airport in airportsArray) {
  ASAirport *initialAirport = [ASAirport MR_createInContext:context];
  //далее выставляем необходимые свойства у новоиспеченного объекта
}
[context MR_save];


Вернемся к поиску: итак, приложение считало данные об аэропортах в память и ждёт, когда пользователь начнет вводить символы в поисковую строку. При вводе каждого нового символа программа обходит массив данных и находит те аэропорты, названия которых начинаются в точности с той строки, что ввёл пользователь. В случае, если ничего не найдено, запускается повторный обход массива, и отбираются те названия, которые имеют малое расстояние Левенштейна от поисковой строки. Это помогает в том случае, если пользователь допустил опечатку или просто не знает, как точно пишется название города.



Процесс поиска может занимать значительное время, особенно если ищем нечёткое совпадение. Поэтому он реализован в классе-наследнике NSOperation. Это дает важное преимущество над простым асинхронным выполнением блока: мы можем прервать операцию в случае, если процесс поиска еще не завершен, а пользователь уже изменил поисковую строку. На практике это выглядит так:

При реализации функции main, после каждой итерации цикла проверяем, не отменена ли операция:

for (ASAirport *currentAirport in initialAirports) {
  if (self.isCancelled) {
    return;
  }
  //далее идёт сравнение строк
}

Отменяем операцию, когда она больше не актуальна:

[_searchOperation cancel];

и она тут же прекратит выполнение, не потребляя драгоценные ресурсы.

В случае, если пользователь не смог найти нужный аэропорт в списке популярных, он всегда может отправить запрос на сервер. Пример: ищем Назрань — находим Казань (Назрань — непопулярный аэропорт, его нет в локальной базе данных; а Казань — наиболее близка по Левенштейну).



Секунда ожидания ответа от сервера, и счастливый назрановец теперь может найти дешёвый билет и улететь!

Спасибо за внимание!

P.S.
Приложение Aviasales для iOS.
А еще у нас есть приложение для Android.
Tags:
Hubs:
+19
Comments7

Articles

Information

Website
www.aviasales.ru
Registered
Founded
Employees
51–100 employees
Location
Таиланд