Статьи

Создание приложения для поиска с поддержкой GPS для iPad

Частью привлекательности мобильных сервисов является то, что они имеют отношение к тому, где вы находитесь сейчас. Большинство телефонов поддерживают GPS и подключение к сети. И когда вы объединяете их, вы получаете удобный сервис на основе местоположения. В этом примере приложения мы собираемся создать бэкэнд-сервис на PHP, к которому будет подключаться приложение iPad на внешнем интерфейсе. Этот сервис будет иметь список магазинов в локальной области в базе данных MySQL. Вы можете использовать этот пример кода как на внутреннем, так и на внешнем интерфейсах в качестве начального набора для своей собственной услуги, основанной на геолокации. Соберите весь код для StoreFinder на GitHub в BuildMobile .

Создание Бэкэнда

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

Листинг 1. db.sql

 DROP TABLE IF EXISTS locations; CREATE TABLE locations( lon FLOAT, lat FLOAT, name VARCHAR(255) ); INSERT INTO locations VALUES ( -122.035706, 37.332802, 'Starbucks' ); INSERT INTO locations VALUES ( -122.036133, 37.331711, 'Peets' ); INSERT INTO locations VALUES ( -122.033173, 37.336182, 'Chipotle' ); ... INSERT INTO locations VALUES ( -122.035919, 37.324341, 'Buy More' ); INSERT INTO locations VALUES ( -122.031410, 37.333176, 'Costco' ); 

Первым делом скрипт создает таблицу местоположений, которая включает широту, долготу и название каждого из магазинов. Затем он вставляет кучу тестовых записей, которые расположены примерно вокруг кампуса Apple в Купертино. Возможно, вам придется добавить несколько записей рядом с вами, когда вы тестируете этот код, чтобы получить какие-либо результаты.

Чтобы запустить это в производство, мы сначала создаем базу данных, используя mysqladmin затем используем команду mysql для запуска сценария, например так:

 $ mysqladmin --user=root create geo $ mysql –user=root geo < db.sql 

В зависимости от вашей установки MySQL вам придется изменить имя пользователя, а также добавить пароль, если он требуется.

Когда база данных готова, пришло время создать скрипт PHP, который будет выполнять поиск и возвращать результаты в виде XML. Это начинается с нахождения ограничительной рамки минимальной и максимальной широты и долготы, как показано в листинге 2.

Листинг 2. circle.php границ

 <?php define( 'LATMILES', 1 / 69 ); define( 'LONMILES', 1 / 53 ); $lat = 37.3328; $lon = -122.036; $radius = 1.0; if ( isset( $_GET['lat'] ) ) { $lat = (float)$_GET['lat']; } if ( isset( $_GET['lon'] ) ) { $lon = (float)$_GET['lon']; } if ( isset( $_GET['radius'] ) ) { $radius = (float)$_GET['radius']; } $minlat = $lat - ( $radius * LONMILES ); $minlon = $lon - ( $radius * LATMILES ); $maxlat = $lat + ( $radius * LONMILES ); $maxlon = $lon + ( $radius * LATMILES ); 

Каждая точка широты составляет 69 миль, а каждая точка долготы — 53 мили. Они хранятся как LATMILES и LONMILES в верхней части скрипта. Сценарий использует их для создания minlat , maxlat , minlon и maxlon которые представляют ограничивающий прямоугольник для запроса. Измерения блока определяются значениями радиуса, которые передаются. Для удобства все значения по умолчанию установлены на разумные значения.

Следующим шагом является создание запроса, который найдет записи местоположения внутри ограничительной рамки. Код для этого показан в листинге 3.

Листинг 3. circle.php Создание запроса

 $dbh = new PDO('mysql:host=localhost;dbname=geo', 'root', ''); $sql = 'SELECT lat, lon, name FROM locations WHERE lat >= ? AND lat <= ? AND lon >= ? AND lon <= ?'; $params = array( $minlat, $maxlat, $minlon, $maxlon ); if ( isset( $_GET['q'] ) ) { $sql .= " AND name LIKE ?"; $params []= '%'.$_GET['q'].'%'; } $q = $dbh->prepare( $sql ); $q->execute( $params ); 

Этот код использует библиотеку PDO для подключения к базе данных. Затем он форматирует и выполняет инструкцию SQL, которая ограничивает возвращаемые записи только теми записями в пределах ограничивающего прямоугольника, который был создан в листинге 2. По желанию, если параметр ‘q’ указан, будут возвращены только записи, в имени которых содержится указанное значение. Например, пользователь может указать, что он хочет видеть только магазины с названием «баксы» в названии, и он будет получать только магазины Starbucks поблизости.

Последний шаг для этого сценария — отформатировать результаты в формате XML. Это показано в листинге 4.

Листинг 4. circle.php Форматирование результатов

 $doc = new DOMDocument(); $r = $doc->createElement( "locations" ); $doc->appendChild( $r ); foreach ( $q->fetchAll() as $row) { $dlat = ( (float)$row['lat'] - $lat ) / LATMILES; $dlon = ( (float)$row['lon'] - $lon ) / LONMILES; $d = sqrt( ( $dlat * $dlat ) + ( $dlon * $dlon ) ); if ( $d <= $radius ) { $e = $doc->createElement( "location" ); $e->setAttribute( 'lat', $row['lat'] ); $e->setAttribute( 'lon', $row['lon'] ); $e->setAttribute( 'name', $row['name'] ); $e->setAttribute( 'd', $d ); $r->appendChild( $e ); } } print $doc->saveXML(); ?> 

Чтобы упростить жизнь, мы используем библиотеку XML DOM, чтобы создать XML-документ в памяти, а затем просто записать его с помощью метода saveXML . Это гарантирует, что XML будет правильно форматировать специальные символы, такие как «меньше», «больше», «амперсанд», кавычки и так далее. Это также намного легче читать, чем код XML, написанный вручную.

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

Когда база данных построена и сценарий готов к работе, пришло время протестировать его.

Тестирование Бэкэнда

Вы можете посмотреть на результат в браузере, просто перейдя на страницу и уточнив параметры поиска по своему усмотрению. Но, как видно из листинга 5, иногда проще просто использовать CURL.

Листинг 5. Запуск скрипта с использованием CURL

 $ curl "http://localhost/circle.php?radius=1" <?xml version="1.0"?> <locations> <location lat="37.3328" lon="-122.036" name="Starbucks" d="0"/> <location lat="37.3317" lon="-122.036" name="Peets" d="0.075900000000068"/> ... </locations> $ 

CURL — это утилита командной строки, которая выбирает указанный URL и распечатывает результат в STDOUT . Из URL видно, что я не указал широту и долготу. Это означает, что поиск будет сосредоточен в штаб-квартире Apple в Купертино, где сосредоточено большинство данных примера. Все, что я сделал, — указал, что возвращенные магазины должны быть в радиусе одной мили, что немного уточняет результаты.

База данных построена, PHP-скрипт для выполнения запроса на стороне сервера завершен и протестирован. Следующим шагом будет установка простого интерфейса iPad.

Создание приложения для iPad

Это приложение для iPad будет приложением «View-based». Это означает, что мы начинаем с чистого холста, к которому добавляем наши элементы управления. В этом приложении нам нужны только два элемента управления: UISearchBar который будет иметь термин поискового запроса (например, «баксы», чтобы найти все Starbucks) и UITableView который будет отображать все возвращенные записи. Это можно увидеть в листинге 6.

Листинг 6. StoreFinderViewController.h

 #import <UIKit/UIKit.h> #import <CoreLocation/CoreLocation.h> @interface StoreFinderViewController : UIViewController<UITableViewDataSource, CLLocationManagerDelegate, NSXMLParserDelegate, UISearchBarDelegate> { IBOutlet UISearchBar *searchBar; IBOutlet UITableView *tableView; CLLocationManager *locationManager; NSMutableArray *locations; float latitude; float longitude; } @property (nonatomic, retain) CLLocationManager *locationManager; @end 

У нас есть не только указатели searchBar и tableView , но у нас также есть locationManager , который используется для получения данных GPS. Массив положений, в котором хранятся все найденные нами местоположения. И значения широты и долготы, которые хранят самые последние значения широты и долготы.

Как только у нас есть контрольные указатели, определенные в коде, нам нужно добавить их в файл определения отображения. Добавление элементов управления пользовательского интерфейса на дисплей начинается с открытия файла StoreFinderViewController.XIB . Оттуда выберите «Проверка файлов» в меню «Вид»> «Утилиты». В правом нижнем углу будет панель объектов, которые вы можете перетащить в область контроллера вида. Сначала перетащите UISearchBar и найдите ее в верхней части окна. Затем возьмите UITableView и UITableView его на дисплей. Он должен заполнить все содержимое, если это не так, просто сдвиньте его, пока он не сделает.

Далее нам нужно подключить эти элементы управления к ViewController . Сначала щелкните правой кнопкой мыши элемент UISearchBar на панели «Объекты» в левой части экрана. Появится всплывающее меню, в котором есть пункт с надписью «New Referencing Outlet». Нажмите на маленький пузырь справа и перетащите линию к элементу «Владелец файла» и отпустите его. Это вызовет другое меню опций. Прикрепите его к элементу searchBar.

Теперь сделайте то же самое для UITableView чтобы подключить его к tableView в контроллере представления. И, наконец, снова щелкните правой кнопкой мыши на UITableView и на этот раз перетащите пузырек dataSource к File's Owner и отпустите.

Это все, что вам нужно сделать, чтобы подключить интерфейс. С этого момента все это в коде.

Чтобы включить locationManager вам нужно добавить базовую платформу Location в проект. Для этого выберите проект «StoreFinder», а затем цель «StoreFinder», на которой есть маленькая кисть и значок линейки. Оттуда выберите вкладку «Build Phases» и откройте раздел «Link Binary With Libraries». Это покажет вам библиотеки, которые вы в настоящее время включили. Нажмите кнопку «плюс» и выберите базовую инфраструктуру расположения, чтобы добавить ее в свое приложение.

Класс также определяет много делегатов. UITableViewDataSource означает, что объект будет использоваться для передачи данных для табличного представления. CLLocationManagerDelegate означает, что объект будет отвечать на обновления GPS. NSXMLParserDelegate используется для анализа XML-ответа от сервера. И, наконец, UISearchBarDelegate означает, что объект будет обрабатывать события из панели поиска. В частности, когда пользователь нажимает кнопку поиска или отменяет поиск.

С определением класса все в порядке, пришло время приступить к созданию самого класса контроллера представления. Это начинается с кода инициализации, показанного в листинге 7.

Листинг 7. Инициализация StoreFinderViewController.m

 #import "StoreFinderViewController.h" @implementation StoreFinderViewController - (void)dealloc { [super dealloc]; [self.locationManager release]; if ( locations ) [locations release]; } - (void)viewDidLoad { [super viewDidLoad]; locations = NULL; self.locationManager = [[[CLLocationManager alloc] init] autorelease]; self.locationManager.delegate = self; [self.locationManager startUpdatingLocation]; } - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { return YES; } 

Единственная действительно интересная вещь — это создание объекта locationManager и запрос на получение данных с GPS. В листинге 8 у нас есть код, который обрабатывает обратные вызовы из UITableView для данных.

Листинг 8. StoreFinderViewController.m Обработка таблиц

 - (NSInteger)tableView:(UITableView *)table numberOfRowsInSection:(NSInteger)section { if ( locations != NULL ) { return [locations count]; } return 0; } - (UITableViewCell *)tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MyIdentifier"]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:@"MyIdentifier"] autorelease]; } NSDictionary *itemAtIndex = (NSDictionary *)[locations objectAtIndex:indexPath.row]; UILabel *newCellLabel = [cell textLabel]; [newCellLabel setText:[itemAtIndex objectForKey:@"name"]]; return cell; } 

Если вы еще не видели UITableViewDataSource это может быть довольно интересно. Происходит то, что элемент управления перезванивает только для того, чтобы получить достаточно данных, поскольку он должен отображать то, что видимо пользователю. Первый вызов — numberOfRowsInSection который получает размер таблицы. Затем cellForRowAtIndexPath возвращает объект ячейки для данного индекса. В этом случае код использует массив местоположений для установки метки с именем магазина, найденного по заданному индексу строки.

Конечно, табличное отображение данных бесполезно без некоторых данных для его управления. Код в листинге 9 представляет собой фактический запрос к серверу и анализирует возвращенные данные.

Листинг 9. StoreFinderViewController.m XML-запрос и анализ

 - (void)updateLocation:(CLLocation *)newLocation { if ( locations ) { [locations release]; } locations = [[NSMutableArray alloc] init]; if ( newLocation ) { latitude = newLocation.coordinate.latitude; longitude = newLocation.coordinate.longitude; } NSString *urlString = [NSString stringWithFormat:@"http://localhost/geo2/circle.php?lat=%g&amp;lon=%g&amp;radius=100&amp;q=%@", latitude, longitude, searchBar.text ? searchBar.text : @""]; NSXMLParser *locationParser = [[[NSXMLParser alloc] initWithContentsOfURL:[NSURL URLWithString:urlString]] autorelease]; [locationParser setDelegate:self]; [locationParser parse]; [tableView reloadData]; } - (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict { if ( [elementName isEqualToString:@"location"]) { [locations addObject:[[NSDictionary alloc] initWithDictionary:attributeDict]]; } } 

Он начинается с метода updateLocation который вызывается либо при изменении GPS, либо когда пользователь делает запрос на поиск. Метод updateLocation создает NSXMLParser и затем указывает его на URL-адрес сервера. В этом случае сервер находится на локальном хосте, но вы можете разместить его где угодно. Синтаксический анализатор XML запрашивает данные, возвращает результаты и начинает их анализ.

Анализ данных запускает методы обратного вызова, в данном случае didStartElement , который указывает, что тег был запущен. Этого достаточно для этого кода, поскольку тег location содержит все данные, которые нам нужно знать в атрибутах, которые передаются из анализатора XML через параметр attribute. didStartElement просто просматривает тег, чтобы увидеть, является ли его имя «location», и если это так, он копирует атрибут в новый объект в массиве location.

В конце процесса reloadData метод reloadData который обновляет элемент управления UITableView .

Следующее, что нужно сделать, это обработать обратные вызовы GPS, которые показаны в листинге 10.

Листинг 10. StoreFinderViewController.m GPS StoreFinderViewController.m

 - (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation { [self updateLocation:newLocation]; } - (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error { } 

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

И последний маленький фрагмент кода обрабатывает строку поиска, как показано в листинге 11.

Листинг 11. StoreFinderViewController.m поиска StoreFinderViewController.m

 - (void)searchBarSearchButtonClicked:(UISearchBar *)sb { [self updateLocation:NULL]; [searchBar resignFirstResponder]; } - (void)searchBarCancelButtonClicked:(UISearchBar *)sb { [searchBar resignFirstResponder]; } @end 

Эти два метода обрабатывают поиск или отмену из панели поиска. Когда пользователь выбирает поиск, мы просто запускаем запрос снова и обновляем представление таблицы с updateLocation метода updateLocation , а затем избавляемся от клавиатуры с resignFirstResponder вызова resignFirstResponder . Метод отмены просто избавляет от клавиатуры.

С написанным на переднем и заднем концах пришло время попробовать этого щенка.

Пробовать приложение

Первое, что происходит при запуске приложения, это то, что вам предлагается разрешить приложению использовать ваше текущее местоположение, как показано на рисунке 1.

Фигура 1

Фигура 1

Если вы хотите, чтобы код работал, нажмите кнопку ОК. Если вы хотите избавить себя от некоторых головных болей позже, вам также следует установить флажок «Больше не спрашивать».

С удалением местоположения способ, которым приложение запрашивает службу PHP на серверной части и анализирует возвращенный XML в массив местоположений. Этот массив затем отображается в UITableView как вы можете видеть на рисунке 2.

Фигура 2

Фигура 2

Хорошо, теперь мы действительно куда-то добираемся. Поэтому давайте удостоверимся, что мы можем не только найти магазины, которые находятся вокруг нас, но которые соответствуют нашим критериям поиска. Мы делаем это, нажимая в строке поиска, которая вызывает клавиатуру, как показано на рисунке 3.

Рисунок 3

Рисунок 3

Подняв клавиатуру, мы можем ввести поисковый запрос, например, «лед», как показано на рисунке 4.

Figure4

Figure4

Затем мы нажимаем кнопку поиска, и результаты уточняются до ледового катка, показанного на рисунке 5.

Figure5

Figure5

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

Выводы

Создание этого приложения GPS было удивительно легко как с передней, так и с задней стороны. Бэкэнд представляет собой очень простую базу данных с элементарным ограниченным запросом. А приложение iPad на внешнем интерфейсе получает данные GPS и выполняет очень простой HTTP-запрос, чтобы получить данные с внутреннего конца для показа. Это только отправная точка, отсюда вы можете создать бэкэнд для добавления дополнительных данных или поддержки поиска. На переднем конце вы можете добавить отображение или дополнительный дисплей данных.