Development of iOS application Aviasales.ru. Airport Selection Screen

    When creating the Aviasales.ru application for iOS, we had many interesting tasks. One of them is the implementation of a convenient mechanism for selecting departure and destination points. In this post, we would like to briefly tell you how we solved this problem and what features of the iOS SDK we used.



    Perhaps you already saw this post a while ago. It’s just that the last time we wrote the wrong way - and it was removed from publication. And I really do not want the material to disappear. So if you have already seen him - just do not pay attention to him.

    The destination airport selection screen is divided into three parts: the nearest airports, a list of airports that the user has previously selected, and the airport name entry line.

    The functionality of the nearest airports is implemented in two stages: first, the application, after asking permission from the user, learns the coordinates of the device. By the way, you can add a clarification to the request for geolocation data for what, in fact, we are interested in. To do this, you can use the purpose property of CLLocationManager:

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



    We don’t need the exact coordinates of the user, it’s enough to know what city it is near. Therefore, the accuracy of geolocation can be reduced:

    locationManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers;
    

    The second stage - the received coordinates are sent to our server, which determines which airports may be of interest to the user. For example, residents of St. Petersburg, in addition to Pulkovo, are invited to Finnish Lappeenranta.

    The search history is the last five selected items that are stored in the database (using SQLite with Core Data, nothing complicated).

    Let's move on to the most interesting - the search for airports and cities by the line entered by the user. Search works in three stages:
    1. We are looking for an exact match in the list of popular airports;
    2. if nothing is found, look for inaccurate matches in the same list;
    3. if the user has entered more than two characters, he will be able to send a search request to the server in order to find a less popular airport.

    Now in order.

    Airports in the world, it turns out, a great many - about 10 thousand. But the really popular of them (those that users use with certain regularity in search queries) are only one and a half thousand. We decided to initially supply this list of popular destinations within the application, so that at the first start the user can select the desired city as quickly as possible. To store information about these airports we also use SQLite with Core Data (there is an article on Habr on how to preload Core Data into the application ). When launched, the application reads data about airports from the database to the array:

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


    We use the Grand Central Dispatch mechanism to perform this operation in the background thread. A call through synchronized is necessary to ensure secure memory access during multithreaded operation.

    “What kind of method MR_findAllInContext?” - you ask. This is a feature of the MagicalRecord library. The fact is that the Core Data engine itself is not thread safe. In practice, this can lead to application crashes if fetch read requests are sent from different threads. This is solved by using separate NSManagedObjectContextfor each thread, while persistentStoreCoordinatorthey all have in common. The MagicalRecord library helps coordinate all these contexts.

    Firstly, it implements a method [NSManagedObjectContext MR_contextForCurrentThread](used in the methodcurrentManagedObjectContextfrom the code above), which returns us the context for the thread where it was called. Secondly, there are many in MagicalRecord simplifying life wrappers over standard blocks of code: for example, the creation of diverse NSManagedObjectModel, NSPersistentStoreCoordinator, NSManagedObjectand their heirs can be done in one line, simply setting the necessary parameters.

    Sometimes the data on popular airports is updated - some destinations gain popularity, others, on the contrary, become less relevant, some change their names. Therefore, from time to time, the application downloads a JSON file compressed with gzip from the server, the data from which, also in the background stream, updates the database. First, we clear the database of entries:

    [ASAirport MR_truncateAllInContext:context];
    

    Then we write new data:

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


    Let's go back to the search: so, the application read data about airports into memory and waits for the user to enter characters in the search string. When you enter each new character, the program bypasses the data array and finds those airports whose names begin exactly with the line that the user entered. In case nothing is found, a repeated traversal of the array is started, and those names that have a small Levenshtein distance from the search string are selected . This helps if the user makes a typo or just does not know how to spell the name of the city.



    The search process can take considerable time, especially if we are looking for a fuzzy match. Therefore, it is implemented in the successor class.NSOperation. This gives an important advantage over simple asynchronous execution of the block: we can abort the operation if the search process is not completed yet, and the user has already changed the search string. In practice, it looks like this:

    When implementing the main function, after each iteration of the loop, we check whether the operation is canceled:

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

    Cancel the operation when it is no longer relevant:

    [_searchOperation cancel];
    

    and she will immediately stop doing it without consuming precious resources.

    If the user could not find the desired airport in the list of popular ones, he can always send a request to the server. Example: looking for Nazran - we find Kazan (Nazran is an unpopular airport, it is not in the local database; and Kazan is the closest to Levenshtein).



    A second of waiting for a response from the server, and a happy Nazran can now find a cheap ticket and fly away!

    Thanks for attention!

    PS
    Aviasales App for iOS .
    And we also have an Android app .

    Also popular now: