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

в 9:05, , рубрики: aviasales, core data, iOS, MagicalRecord, mobile development, разработка, разработка под iOS, метки: , , ,

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

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

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

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

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

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

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

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(_airports){
    _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];

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

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

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

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

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

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

[_searchOperation cancel];

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

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

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

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

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

Автор: sevabill

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js