Почему, собственно?
Имея Mac и iPhone, не попытаться написать мобильное приложение? Как-то неправильно. Благо тут подвернулась задачка, которая прекрасно легла в тему, как весьма полезная и в то же время не очень сложная в реализации. Итак, я погрузился в Objective-C и Cocoa.
Disclaimer
Прошу помнить, что не только мое первое приложение для iOS, но и первое приложение на Objective-C в принципе. Ни разу не претендую ни качество реализации, ни на эффективность, но хочу сказать, что получился весьма целостный несложный пример, который дает представление об Objective-C и разработке под iOS в целом. Особенно для тех, кто вообще этот язык не знает.
Disclaimer 2
Данный пост был изначально опубликован в виде статьи в журнале «The Pragmatic Bookshelf Magazine» на английском языке — US Visa: My First iPhone App. Русская версия, публикуемая здесь, не является точным переводом журнальной версии, так как была написана как отдельный текст несколько позже.
«Хьюстон! У нас проблема!»
За последний год я несколько раз вынужден был подавать на американскую визу в посольстве в Лондоне. Каждый раз мне говорили, что конкретно в моем случае требуется «administrative processing». Документы то у тебя принимают, но потом вместо визы дают номерок (batch number) и говорят периодически заглядывать на их сайт, где есть PDF-ка, в которой по данному номеру следует искать указания, что делать дальше (досылать еще документы, посылать паспорт и т.д.). Нажимаешь на ссылку с официальной PDF'кой, открывается файл, жмешь CTRL-F, вводишь номер (batch number) и вперед.
Возникла идея автоматизации — сделать приложение для айфона, в которое может вбить номер заявки один раз, и затем одним нажатием на кнопку получать статус обработки визы. Приложение должно уметь скачивать PDF файл, парсить его и вычленять данные по заявке.
Что делать, если у меня Windows?
Не все еще потеряно. Objective-C можно запустить на Windows через Cygwin или MinGW. Более того, проект GNUstep дает возможность использовать библиотеки AppKit и Foundation для написания графических программ в Windows на Objective-C. Увы, я не буду погружаться столь глубоко в этой статье. Мы сделаем только приложение, работающее в командной строке. Оно будет уметь скачивать PDF и парсить его. Собрать приложение можно будет и на Windows, и на Маке. После, мы практически без изменений будем использовать модули этого приложения для создания полноценной программы для iOS. Но, увы, это уже только для владельцев Маков. Можно, конечно, Хакинтош на виртуалку поставить и гонять приложение на симуляторе айфона в Xcode, но вот загрузить его в реальный айфон вряд ли получится без настоящего Мака.
Установка GNUstep под Windows
Я нашел два великолепных поста:
- «Learn Objective-C on Windows» — Как поставить GNUstep и попробовать минимальное приложение.
- «Clang and Objective-C on Windows» — Как собрать свежий компилятор Clang под Windows. К сожалению, GCC, идущий на данный момент с GNUstep, не поддерживает уровня языка Objective-C, требуемого Apple'ом. К тому же, Apple полностью переключился на Clang с некоторого времени. Так что, надо собирать Clang, так как установщика под Windows у него пока нет. Я просто следовал один в один инструкциям из поста, и все встало без проблем.
Неплохо было бы познакомиться с Objective-C и iOS API
Я про Objective-C не знал ничего, кроме слухов о его необычном подходе к управлению памятью, поэтому пришлось пролистать следующие книжки.
Предупреждение: Ссылки снизу содержат мой личный номер партнерской программы с Амазоном. От возможных покупок, совершенных после перехода по этим ссылкам, я могу получить небольшой процент. Если вас это не устраивает, пожалуйста, не нажимайте на ссылки, или вручную «почистите» URL через cut-paste. Спасибо за понимание.
1. iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides)
2. Objective-C Programming: The Big Nerd Ranch Guide (Big Nerd Ranch Guides)

3. Programming in Objective-C (4th Edition) (Developer's Library)

А еще есть один волшебный бесплатный документ — «From C++ to Objective-C».
Итак, задача делится на три основные части:
- Парсер PDF
- Скачивалка PDF (желательно ее сделать без привязки к интерфейсу)
- Интерфейс под iOS
После ознакомления с Objective-C, могу сказать, что для более менее опытного разработчика на C или C++, особенно, если есть опыт разработки UI (я в свое время много возился с Delphi/C++Builder), «въехать» в Objective-C и Cocoa несложно. Достаточно сфокусироваться на весьма необычной полу-ручной модели управления памятью (особенно после RAII в C++ и сборщика мусора в Java). Objective-C сам управляет памятью, но вот контроль за подсчетом ссылок на объекты для их правильного освобождения лежит на вас. Надо понять принцип, иначе утечки памяти неизбежны. У меня именно так и было в начале. Благо отличные инструменты профилировки в Xcode позволяют основные проблемы выявлять практически сразу.
Ниже я приведу несколько личных субъективных впечатлений, как новичка в Objective-C и Cocoa. Вряд ли это будет интересно, если вы уже имеете опыт в них, но вот если нет — думаю, будет интересно.
Для начала интересно посмотреть, как в Objective-C формируются имена функций-членов класса. Это почти как человеческий язык. Если я по-английски скажу «please, find a needle in a portion of some data and add the result to a list implemented as a mutable array», в Objective-C это будет:
+ (bool)findInPortion:(NSMutableData *)someData needle:(NSString*)aNeedle andAddTo:(NSMutableArray*)aList { ... }
Если прочитать этот код слева направо сверху вниз, то получается почти полноценное предложение. Формально, полное имя этого метода —
findInPortion:needle:andAddTo:. Аргументы именованы, и их имена являются частью полного имени метода. Если правильно давать имена переменных аргументов (someData, aNeedle and aList), то можно фактически писать по-английски. Конечно, это все довольно «многословный» подход, но фантастическая система предсказания в Xcode при наборе кода позволяет быстро и просто набивать все эти обороты. Обратите внимание также, что традиционное выравнивание при разбивке длинных строк происходит по двоеточию, разделяющему формальное имя параметра от переменной, его представляющей.В Objective-C нетрадиционный синтаксис для вызова методов. Например, вместо:
NSMutableArray* list = NSMutableArray.alloc.init;
пишется:
NSMutableArray* list = [[NSMutableArray alloc] init];
Выглядит странно, но это вопрос привычки. Опять таки, система предсказания кода при вводе позволяет вводить квадратные скобки даже почти физически не набивая их.
Objective-C и Cocoa используют активно несколько шаблонов программирования, которые просто необходимо освоить. Например, делегаты. Они везде в Cocoa. Делегат — это класс, содержащий в себе обратные вызовы. Вместе передачи пачки отдельных функций или методов, просто передается один объект, реализующий все требуемые обратные вызовы. Например, я использовал стандартный класс NSURLConnection для скачивания PDF'ки. Этот класс требует предоставление ему делегата NSURLConnectionDelegate, методы которого вызываются при различных событиях в процессе скачивания.
Итак, пара недель вечерних бдений за книгами, и я набросал остов моего первого приложения. Но это была только первая часть марлезонского балета. Далее надо было разобраться с форматом PDF.
Парсер PDF
Как уже было сказано, файл, содержащий информацию из посольства, в формате PDF. Описание этого формата доступно на сайте Adobe. Я использовал документ «PDF Reference third edition, Version 1.4».
Разбор PDF у меня реализован весьма кондово. Так как данные приходят порциями, то мы будем анализировать документ по частям, последовательно. Каждую новую порцию данных добавляем в буфер и пытаемся в нем разобрать формат PDF. Сначала ищем фрагменты, обрамленные в маркеры
stream и endstream. Содержимое каждого такого блока «разжимаем» через zlib/inflate. После это уже чистый текст, и мы в нем ищем наш batch number, конечно, с учетом языка разметки PDF. Если номер обнаружен, то печатаем его и переходим к следующему блоку. Основные шаги парсера:
- Если в данных, принятых на текущий момент, есть блок, ограниченных тегами
stream\r\nиendstream\r\n, то вырезаем его из буфера и «разжимаем» черезzlib/inflate. - Разжатый на первом шаге блок являет текстовым. Нам надо найти в нем фрагменты, обрамленные тегами
BT\r\n(Begin Text) иET\r\n(End Text). Находим все такие блоки и объединяем их в список строк. - Внутри каждой строки, найденной на шаге 2, удаляем подстроки, неокруженные круглыми скобками. Все что вокруг круглых скобок — это служебная информация, и она нам не нужна.
- Итак, мы вычленили чистый текст из PDF'ки. Логически информация в этом файле организована в виде таблицы с тремя колонками: номер заявки (batch number), статус и дата. Увы, среди этого еще попадаются колонтитулы страниц. Чтобы их отсеить, мы будем смотреть, что если текущая строка выглядит как batch number (11 цифр), то за ней обязательно идет строка-статус и строка-даты. Берем их и снова ждем нового batch number'а.
Как я уже сказал, разбор заточен под конкретный файл, и если в посольстве его изменят, то все сломается. Если хотя бы использовать регулярные выражения, то будет гораздо гибче, но я оставлю это читателям на самостоятельную проработку.
ДОПОЛНЕНИЕ. В процессе работы над статьей, появилась идея сделать специальный веб-сервис, обращаясь к которому по простым URL'ам можно получать данные о заявке, а вся «кухня» по разбору PDF'ки происходит «на облаке». В журнале Dr.Dobb's недавно вышла моя статья — RESTful Web Service in Go Powered by the Google App Engine, описывающая данный подход. Желающие могут «допилить» приложение для работы через этот веб-сервис. Можно вообще сделать хитро: сначала обратиться к веб-сервису, и если от него есть ответ, то на этом закончить, а если нет — запустить процедуру самостоятельного скачивания и разбора PDF'ки.
Приложение для командной строки
Итак, мы знаем почти все, чтобы написать приложение, которое будет скачивать PDF и вычленять из него информацию по нашей заявке. Приложение будет работать из командной строки. Его можно будет собрать из на Маке, и на Windows через GNUstep и Clang. Далее, исходные файлы этого приложения будут использоваться без изменений для версии под iOS.
Файлы:
BatchPDFParser.m(и.h) — PDF-парсер.NSURLConnectionDirectDownload.m(и.h) — Скачивалка. Тут «обвеска» дляNSURLConnection(инициализация, делегаты, цикл обработки событий).DirectDownloadDelegate.m(и.h) — Делегат дляNSURLConnection, принимающий вызовы в различные моменты скачивания.ViewController.m— прототип ViewController. Это «прослойка» между скачивалкой и будущим графическим интерфейсом. В OSX и iOS используется концепция MVC (Model-View-Controller). «Контроллер» обеспечивает связь между элементами интерфейса и бизнес-логикой приложения. Текущий контроллер в основном содержит заглушки, которые будут реализованы в полной графической версии.main-cli.m— Точка входа.
BatchPDFParser.h
Этот файл содержит объявление класса
Batch, содержащего информацию об обновлении статуса заявки, и класса BatchPDFParser, который реализует метод findInPortion:needle:andAddTo: (кстати, это статический метод класса, видите + начале строки?).@interface Batch: NSObject { NSString *batchNumber, *status, *date; } @property (atomic, copy) NSString* batchNumber, *status, *date; @end @interface BatchPDFParser: NSObject + (bool)findInPortion:(NSMutableData *)data needle:(NSString* const)needle andAddTo:(NSMutableArray*)list; @end
BatchPDFParser.m
В этом файле реализация парсера PDF.
#import <Foundation/Foundation.h> #import "BatchPDFParser.h" #import "zlib.h" @implementation Batch @synthesize batchNumber, status, date; - (void) dealloc { [batchNumber release]; [status release]; [date release]; [super dealloc]; } @end @implementation BatchPDFParser
Метод
findInData:fromOffset:needle: ищет подстроку в данном блоке данных (типа strstr()). Поиск примитивный, и его можно ускорить, например, реализовав алгоритм КМП.+ (int) findInData:(NSMutableData *)data fromOffset:(size_t)offset needle:(char const * const)needle { int const needleSize = strlen(needle); char const* const bytes = [data mutableBytes]; int const bytesLength = [data length] - needleSize; for (int i = 0; i < bytesLength;) { char const* const current = memchr(bytes + i, needle[0], bytesLength - i); if (current == NULL) return -1; if (memcmp(current, needle, needleSize) == 0) return current - bytes; i = current - bytes + 1; } return -1; }
Метод
isBatchNumber:number: проверяет, является ли строка номером заявки (batch number):+ (bool) isBatchNumber:(NSString*)number { long long const value = [number longLongValue]; return value >= 20000000000L && value < 29000000000L; }
Метод
findBatchNumberInChunk:needle:andAddTo: ищет фрагменты, обрамленные тегами BT и ET. В них выделяет текст в круглых скобках, и уже среди найденного выделяет конкретно номер заявки, строку-статус и строку-дату.+ (bool) findBatchNumberInChunk:(char const*)chunk needle:(NSString*)needle andAddTo:(NSMutableArray*)list { enum { waitBT, waitText, insideText } state = waitBT; enum { waitBatchNumber, waitStatus, waitDate } batchParserState = waitBatchNumber; NSMutableString* line = [[NSMutableString alloc] init]; Batch* batch = nil; bool found = NO; while (*chunk) { if (state == waitBT) { if (chunk[0] == 'B' && chunk[1] == 'T') { state = waitText; [line deleteCharactersInRange:NSMakeRange(0, [line length])]; } } else if (state == waitText) { if (chunk[0] == '(') { state = insideText; } else if (chunk[0] == 'E' && chunk[1] == 'T') { if (batchParserState == waitBatchNumber) { if ([self isBatchNumber:line]) { [batch autorelease]; batch = [[Batch alloc] init]; batch.batchNumber = line; batchParserState = waitStatus; } } else if (batchParserState == waitStatus) { batch.status = line; batchParserState = waitDate; } else if (batchParserState == waitDate) { batch.date = line; batchParserState = waitBatchNumber; if ([batch.batchNumber isEqualToString:needle]) { NSString* pair = [NSString stringWithFormat:@"%@\n%@", batch.status, batch.date]; [list addObject:pair]; NSLog(@"Found match: '%@' '%@' '%@'", batch.batchNumber, batch.status, batch.date); found = YES; } } [line autorelease]; line = [[NSMutableString alloc] init]; state = waitBT; } } else if (state == insideText) { if (chunk[0] == ')') { state = waitText; } else { char const c[2] = { chunk[0], 0 }; [line appendString:[NSString stringWithUTF8String:&c[0]]]; } } chunk += 1; } [line release]; [batch release]; return found; }
Теперь главный метод
findInPortion:needle:andAddTo:. Тут выделяются куски, обрамленные тегами stream\r\n и endstream\r\n, содержимое разжимается через zlib/inflate и передается в findBatchNumberInChunk:needle:andAddTo: на анализ.+ (bool)findInPortion:(NSMutableData *)portion needle:(NSString*)needle andAddTo:(NSMutableArray*)list { static char const* const streamStartMarker = "stream\x0d\x0a"; static char const* const streamStopMarker = "endstream\x0d\x0a"; bool found = false; while (true) { int const beginPosition = [self findInData:portion fromOffset:0 needle:streamStartMarker]; if (beginPosition == -1) break; int const endPosition = [self findInData:portion fromOffset:beginPosition needle:streamStopMarker]; if (endPosition == -1) break; int const blockLength = endPosition + strlen(streamStopMarker) - beginPosition; char const* const zipped = [portion mutableBytes] + beginPosition + strlen(streamStartMarker); z_stream zstream; memset(&zstream, 0, sizeof(zstream)); int const zippedLength = blockLength - strlen(streamStartMarker) - strlen(streamStopMarker); zstream.avail_in = zippedLength; zstream.avail_out = zstream.avail_in * 10; zstream.next_in = (Bytef*)zipped; char* const unzipped = malloc(zstream.avail_out); zstream.next_out = (Bytef*)unzipped; int const zstatus = inflateInit(&zstream); if (zstatus == Z_OK) { int const inflateStatus = inflate(&zstream, Z_FINISH); if (inflateStatus >= 0) { found = found || [BatchPDFParser findBatchNumberInChunk:unzipped needle:needle andAddTo:list]; } else { NSLog(@"inflate() failed, error %d", inflateStatus); } } else { NSLog(@"Unable to initialize zlib, error %d", zstatus); } free(unzipped); inflateEnd(&zstream); int const cutLength = endPosition + strlen(streamStopMarker); [portion replaceBytesInRange:NSMakeRange(0, cutLength) withBytes:NULL length:0]; } return found; } @end
DirectDownloadViewDelegate.h
Заголовок делегата для
NSURLConnectionDelegate:@protocol DirectDownloadViewDelegate<NSObject> - (void)setProgress: (float)progress; - (void)appendStatus: (NSString*)status; - (void)setCompleteDate: (NSString*)date; @end
DirectDownloadDelegate.h
Собственно, сам делегат
NSURLConnectionDelegate.#import "DirectDownloadViewDelegate.h" @interface DirectDownloadDelegate : NSObject { NSError *error; BOOL done; BOOL found; NSMutableData *receivedData; float expectedBytes, receivedBytes; id<DirectDownloadViewDelegate> viewDelegate; NSString* needle; } - (id) initWithNeedle:(NSString*)aNeedle andViewDelegate:(id<DirectDownloadViewDelegate>)aViewDelegate; @property (atomic, readonly, getter=isDone) BOOL done; @property (atomic, readonly, getter=isFound) BOOL found; @property (atomic, readonly) NSError *error; @end
DirectDownloadDelegate.m
И его реализация:
#import <Foundation/Foundation.h> #import "DirectDownloadDelegate.h" #import "BatchPDFParser.h" @implementation DirectDownloadDelegate @synthesize error, done, found;
Конструктор
initWithNeedle:andViewDelegate: создает делегата и параметризирует его другим делегатом, DirectDownloadViewDelegate, который будет использоваться для задачи обновления экрана. Тут, кстати, мы впервые видит и деструктор, (void) dealloc:.- (id) initWithNeedle:(NSString*)aNeedle andViewDelegate:(id<DirectDownloadViewDelegate>)aViewDelegate { viewDelegate = aViewDelegate; [viewDelegate retain]; needle = [[NSString alloc] initWithString:aNeedle]; receivedData = [[NSMutableData alloc] init]; expectedBytes = receivedBytes = 0.0; found = NO; return self; } - (void) dealloc { [error release]; [receivedData release]; [needle release]; [viewDelegate release]; [super dealloc]; }
Метод
connectionDidFinishLoading: вызывается, когда соединение закончено.- (void) connectionDidFinishLoading:(NSURLConnection *)connection { done = YES; NSLog(@"Connection finished"); }
Метод
connection:didFailWithError: вызывает при ошибке при скачивании файла.- (void) connection:(NSURLConnection *)connection didFailWithError:(NSError *)anError { error = [anError retain]; [self connectionDidFinishLoading:connection]; }
Метод
connection:didReceiveData: вызывается, когда получена новая порция данных из канала. Каждую такую порцию мы добавляем в буфер, обновляем индикатор прогресса скачивания (через еще один делегат, viewDelegate), затем пробуем вычленить фрагменты данных по PDF формату, и, наконец, печатаем то, что было найдено.- (void) connection:(NSURLConnection *)connection didReceiveData:(NSData *)someData { receivedBytes += [someData length]; [viewDelegate setProgress:(receivedBytes / expectedBytes)]; [receivedData appendData:someData]; NSMutableArray* list = [[NSMutableArray alloc] init]; bool foundInCurrentPortion = [BatchPDFParser findInPortion:receivedData needle:needle andAddTo:list]; for (id batch in list) { NSLog(@"[%@]", [batch stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"]); [viewDelegate appendStatus:batch]; } [list release]; found = found || foundInCurrentPortion; }
Последний обратный вызов делегата
NSURLConnectionDelegate, что мы используем, называется connection:didReceiveResponse:. Он вызывается, когда получен информационный ответ по HTTP, содержащий заголовки. Мы из заголовка «Content-Length» берем длину будущего файла, чтобы позже сообразно обновлять индикатор скачивания.- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSHTTPURLResponse *)someResponse { NSDictionary *headers = [someResponse allHeaderFields]; NSLog(@"[didReceiveResponse] response headers: %@", headers); if (headers) { if ([headers objectForKey: @"Content-Length"]) { NSLog(@"Content-Length: %@", [headers objectForKey: @"Content-Length"]); expectedBytes = [[headers objectForKey: @"Content-Length"] floatValue]; } else { NSLog(@"No Content-Length header found"); } } }
NSURLConnectionDirectDownload.h
В этом файле находится метод
donwloadAtURL:searching:viewingOn:, который мы добавляем к классу NSURLConnection. Интересно тут то, что через понятие категорий в Objective-C можно «примешивать» новые методы к существующим классам. Тут мы к классу NSURLConnection добавляем категорию DirectDownload.@interface NSURLConnection (DirectDownload) + (BOOL) downloadAtURL:(NSURL *)url searching:(NSString*)batchNumber viewingOn:(id)viewDelegate; @end
NSURLConnectionDirectDownload.m
Ну и финальная часть скачивалки PDF. Метод
donwloadAtURL:searching:viewingOn: создает соединение и запускает скачивание. Затем происходит ожидание в цикле NSRunLoop, пока скачивание не закончится. Этот цикл позволяет приложению реагировать на события в процессе скачивания. Обратите внимание, это до сих пор скачивалка ни как не привязана к графическому интерфейсу. Она использует делегат viewDelegate для общения с «мордой» приложения.#import <Foundation/Foundation.h> #import "DirectDownloadDelegate.h" @implementation NSURLConnection (DirectDownload) + (BOOL) downloadAtURL:(NSURL *)url searching:(NSString*)batchNumber viewingOn:(id)viewDelegate { NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url]; DirectDownloadDelegate *delegate = [[[DirectDownloadDelegate alloc] initWithNeedle:batchNumber andViewDelegate:viewDelegate] autorelease]; NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:delegate]; [request release]; while ([delegate isDone] == NO) { [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; } if ([delegate isFound] != YES) { [viewDelegate appendStatus:@"This batch number is not found."]; NSLog(@"This batch number is not found."); } NSLog(@"PDF is processed"); [connection release]; NSDateFormatter* dateFormatter = [[NSDateFormatter alloc] init]; dateFormatter.dateFormat = @"yyyy/MM/dd HH:mm:ss"; NSString* lastUpdateDate = [dateFormatter stringFromDate:[NSDate date]]; NSLog(@"Last update at: %@", lastUpdateDate); [viewDelegate setCompleteDate:lastUpdateDate]; [dateFormatter release]; NSError *error = [delegate error]; if (error != nil) { NSLog(@"Download error: %@", error); return NO; } return YES; } @end
ViewController.m
Как уже было сказано, в приложении для командной строки контроллер будет содержать только заглушки, которые мы реализуем позже в полной версии программы.
#import <Foundation/Foundation.h> #import "DirectDownloadViewDelegate.h" #define IBAction void
Пустой класс-заглушка
ViewController.@interface ViewController : NSObject <DirectDownloadViewDelegate> @end #import "NSURLConnectionDirectDownload.h"
Адрес, откуда качать файл.
static char const* const pdf = "http://photos.state.gov/libraries/unitedkingdom/164203/cons-visa/admin_processing_dates.pdf";
И mock-реализация класса-контроллера.
@implementation ViewController
Тестовый обратный вызов
appendStatus: вызывается, когда обнаружено очередное обновление по заявке. Тут мы просто логируем, а в полном приложении будем обновлять экранную форму.- (void) appendStatus:(NSString*)status { NSLog(@"appendStatus(): '%@'", [status stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"]); // Some code is skipped here because not required for the command line mode. }
Тестовый обратный вызов
setProgress: вызывается, когда после принятия очередной порции данных надо обновить индикатор скачивания.- (void) setProgress:(float)progress { // Some code is skipped here because not required for the command line mode. }
Тестовый обратный вызов
setCompleteDate: вызывается, когда анализ PDF полностью закончен. Тут, опять, мы просто логируем.- (void) setCompleteDate:(NSString*)date { NSLog(@"setCompleteDate(): '%@'", date); // Some code is skipped here because not required for the command line mode. }
Ну и финальный метод, который все запускает,
updateBatchStatus:. В полной программе он будет вызываться при нажатии кнопки на форме. Тут он вызывается из main().- (bool) updateBatchStatus:(NSString*)batchNumber { NSURL *url = [[[NSURL alloc] initWithString:[NSString stringWithCString:pdf encoding:NSASCIIStringEncoding]] autorelease]; return [NSURLConnection downloadAtURL:url searching:batchNumber viewingOn:self]; } @end
main-cli.m
Запуск из командной строки.
#import <Foundation/Foundation.h> #import "DirectDownloadDelegate.h" @interface ViewController : NSObject <DirectDownloadViewDelegate> - (bool) updateBatchStatus:(NSString*)batchNumber; @end int main(int argc, char *argv[]) { @autoreleasepool { ViewController* viewController = [ViewController alloc]; [viewController updateBatchStatus:[NSString stringWithCString:argv[1] encoding:NSASCIIStringEncoding]]; [viewController release]; } return 0; }
Попробуем все это собрать и запустить?
Makefile для Мак:files = \ ViewController.m \ BatchPDFParser.m \ NSURLConnectionDirectDownload.m \ DirectDownloadDelegate.m main-cli.m all: build run build: clang -o USVisaTest -DTESTING -framework Foundation -lz $(files) run: ./USVisaTest 20121456171
Makefile
GNUmakefile для GNUstep:include $(GNUSTEP_MAKEFILES)/common.make TOOL_NAME = USVisa USVisa_OBJC_FILES = \ ../ViewController.m \ ../BatchPDFParser.m \ ../NSURLConnectionDirectDownload.m \ ../DirectDownloadDelegate.m \ ../main-cli.m USVisa_TOOL_LIBS = -lz ADDITIONAL_OBJCFLAGS = -DTESTING CC = clang include $(GNUSTEP_MAKEFILES)/tool.make run: ./obj/USVisa 20121456171
Набираем
make. Windows:This is gnustep-make 2.6.2. Type 'mmake print-gnustep-make-help' for help. Making all for tool USVisa... Creating obj/USVisa.obj/../... Compiling file ViewController.m ... Compiling file BatchPDFParser.m ... Compiling file NSURLConnectionDirectDownload.m ... Compiling file DirectDownloadDelegate.m ... Compiling file main-cli.m ... Linking tool USVisa ...
Можно запустить проверить реальную заявку:
make run
У меня вывелось следующее:
This is gnustep-make 2.6.2. Type 'mmake print-gnustep-make-help' for help. ./obj/USVisa 20121456171 2012-06-19 17:27:11.472 USVisa[3420] [didReceiveResponse] response headers: {"Accept-Ranges" = bytes; "Cache-Control" = "max-age=600"; Connection = "keep-alive"; "Content-Length" = 2237242; "Content-Type" = "application/pdf"; Date = "Tue, 19 Jun 2012 16:27:11 GMT"; ETag = "\"4b2ca3e41de5ba4ae45670e776edfc3b:1339778351\""; "Last-Modified" = "Fri, 15 Jun 2012 16:06:15 GMT"; Server = Apache; } 2012-06-19 17:27:11.604 USVisa[3420] Content-Length: 2237242 2012-06-19 17:27:12.093 USVisa[3420] Found match: '20121456171' 'send passport & new travel itinerary' '14-Jun-12' 2012-06-19 17:27:12.104 USVisa[3420] [send passport & new travel itinerary\n14-Jun-12] 2012-06-19 17:27:12.111 USVisa[3420] appendStatus(): 'send passport & new travel itinerary\n14-Jun-12' 2012-06-19 17:27:13.769 USVisa[3420] Connection finished 2012-06-19 17:27:13.774 USVisa[3420] PDF is processed 2012-06-19 17:27:13.961 USVisa[3420] Last update at: 2012/06/19 16:27:13 2012-06-19 17:27:13.972 USVisa[3420] setCompleteDate(): '2012/06/19 16:27:13'
Итак, все работает: скачивалка и парсер PDF. Теперь займемся версией под iOS. Увы, только для пользователей Mac.
Макет экранной формы
Я сделал приложение крайне простым: одна форма с полем ввода, кнопкой и местом для вывода обновлений.

Индикатор скачивания и крутящийся бегунок появляются временно.
ViewController.h
Вот сейчас это полная реализации контроллера. Через макрос
TESTING я сделал разделение между упрощенной и полной версией.#import <Foundation/Foundation.h> #import "DirectDownloadViewDelegate.h" #ifdef TESTING #define IBAction void @interface ViewController : NSObject <DirectDownloadViewDelegate> @end #else #import "ViewController.h" #endif #import "NSURLConnectionDirectDownload.h" static char const* const pdf = "http://photos.state.gov/libraries/unitedkingdom/164203/cons-visa/admin_processing_dates.pdf"; @implementation ViewController #ifndef TESTING @synthesize updateProgressView, batchNumberTextField, statusTextView, lastUpdatedLabel, updateButton; #endif NSString* const PropertiesFilename = @"Properties"; NSString *pathInDocumentDirectory(NSString *fileName) { NSArray *documentDirectories = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *documentDirectory = [documentDirectories objectAtIndex:0]; return [documentDirectory stringByAppendingPathComponent:fileName]; }
Сейчас обратный вызов
appendStatus: не только логирует, но и обновляет экранную форму.- (void) appendStatus:(NSString*)status { NSLog(@"appendStatus(): '%@'", [status stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"]); #ifndef TESTING if ([[statusTextView text] length] == 0) [statusTextView setText:@"Status:\n"]; [statusTextView setText:[[statusTextView text] stringByAppendingString:status]]; [statusTextView setText:[[statusTextView text] stringByAppendingString:@"\n"]]; #endif }
setProcess: обновляет индикатор скачивания.- (void) setProgress:(float)progress { #ifndef TESTING updateProgressView.progress = progress; #endif }
setCompleteDate: выводит дату обновления в текстовое поле на экране.- (void) setCompleteDate:(NSString*)date { NSLog(@"setCompleteDate(): '%@'", date); #ifndef TESTING [lastUpdatedLabel setText:date]; #endif } - (bool) updateBatchStatus:(NSString*)batchNumber { NSURL *url = [[[NSURL alloc] initWithString:[NSString stringWithCString:pdf encoding:NSASCIIStringEncoding]] autorelease]; return [NSURLConnection downloadAtURL:url searching:batchNumber viewingOn:self]; }
Теперь несколько вызовов, специфичных для iOS. Метод
viewDidLoad: вызывается системой, когда экранная форма загружена и готова к использованию. Тут мы вручную создаем крутящийся бегунок и подправляем высоты двух элементов, кнопки и поля ввода, так как почему-то Xcode Interface Builder не позволяет менять их при дизайне формы.#ifndef TESTING - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. spinnerActivityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]; [spinnerActivityIndicatorView setColor:[UIColor blueColor]]; CGSize size = [[self view] frame].size; [spinnerActivityIndicatorView setCenter:CGPointMake(size.width / 2, size.height / 2 + 60)]; [self.view addSubview:spinnerActivityIndicatorView]; CGRect rect = [self.updateButton bounds]; rect.size.height += 10; [self.updateButton setBounds:rect]; rect = [self.batchNumberTextField bounds]; rect.size.height += 20; [self.batchNumberTextField setBounds:rect]; #ifdef DEBUG NSLog(@"DEBUG mode"); #endif }
viewDidUnload вызывается, когда форма становится неактивной.- (void)viewDidUnload { [super viewDidUnload]; // Release any retained subviews of the main view. }
Метод
shouldAutorotateToInterfaceOrientation: позволяет контролировать поведение для смене ориентации аппарата. Тут мы разрешаем только портретное положение, не вверх ногами.- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { return (interfaceOrientation == UIInterfaceOrientationPortrait); } #endif
Метод
launchUpdate: вызывает при нажатии на кнопку Update на форме. Мы блокируем кнопку от повторного нажатия, вывод индикатор скачивания и крутящийся бегунок.- (IBAction)launchUpdate:(id)sender { [self setProgress:0.0]; #ifndef TESTING [updateButton setEnabled: NO]; [updateProgressView setHidden:NO]; NSString* previousStatus = [statusTextView text]; [statusTextView setText:@""]; NSString* batchNumber = [batchNumberTextField text]; [spinnerActivityIndicatorView startAnimating]; BOOL const ok = [self updateBatchStatus:batchNumber]; [spinnerActivityIndicatorView stopAnimating]; if (!ok) { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error" message:@"Internet connectivity problem" delegate:self cancelButtonTitle:nil otherButtonTitles:@"OK", nil]; [alert show]; [alert release]; [statusTextView setText:previousStatus]; } [updateProgressView setHidden:YES]; [updateButton setEnabled: YES]; #endif }
Методы
saveProperties: и loadProperties: сохраняют и восстанавливают содержимое формы при запуске и остановке приложения. Обратите внимание, что для сохранения данных в файле надо запросить у системы положение предназначенного для этого каталога.- (void) saveProperties { NSDictionary *props = [[NSDictionary alloc] initWithObjectsAndKeys: #ifndef TESTING batchNumberTextField.text, @"batchNumberTextField", statusTextView.text, @"statusTextView", lastUpdatedLabel.text, @"lastUpdatedLabel", #endif nil]; for (NSString* key in props) { NSLog(@"%@ - %@", key, [props objectForKey:key]); } NSString* filename = pathInDocumentDirectory(PropertiesFilename); if ([props writeToFile:filename atomically:YES] == NO) NSLog(@"Unable to save properties into file [%@]", filename); [props release]; } - (void) loadProperties { NSDictionary *props = [[NSDictionary alloc] initWithContentsOfFile:pathInDocumentDirectory(PropertiesFilename)]; for (NSString* key in props) { NSLog(@"%@ - %@", key, [props objectForKey:key]); } #ifndef TESTING [batchNumberTextField setText:[props objectForKey:@"batchNumberTextField"]]; [statusTextView setText:[props objectForKey:@"statusTextView"]]; [lastUpdatedLabel setText:[props objectForKey:@"lastUpdatedLabel"]]; #endif [props release]; } - (IBAction)textFieldReturn:(id)sender { #ifndef TESTING [sender resignFirstResponder]; #endif } -(IBAction)backgroundTouched:(id)sender { #ifndef TESTING [batchNumberTextField resignFirstResponder]; #endif } @end
Все! Мы рассмотрели все основные файлы. Приложение полностью готово. Можно собирать и заливать в аппарат (не забыв купить у Apple лицензию разработчика).
Я выложил полный проект на GitHub — usvisa-app. Замечания и мысли принимаются.
Можно заценить видео:
И еще!
Если вы подумываете о том, чтобы ваше приложение было распродано миллионным тиражом, стоит начать с красивой иконки. Для приложения обычно надо их несколько: 57x57 и 114x114 для непосредственно приложения, и 512x512 и 1024x1024 для публикации в AppStore.
Мы поступим проще и возьмем иконку из открытых источников — The Great Seal of the United States.
P.S.
Я решил написать пост про это приложение после того, как цензоры AppStore его «завернули», сославшись на пункт в правилах, где говорится, что приложения с минимальной функциональной нагрузкой, которые можно реализовать через HTML5, не будут допущены. Видимо, они более не хотят видеть пукающих или просто отображающих статическую картинку приложений. Можно было бы поспорить с цензорами на тему минимальной функциональной нагрузки или реализации через HTML5, но я забил. Во-первых, лично мне нравится, что Apple старается не пропускать бесполезные и некачественные приложения, и во-вторых — я и так получил массу удовольствия от освоения Objective-C, и на данный момент работаю еще над двумя приложениями.
P.P.S.
Скоро будет еще статейка про разработку приложений для iOS для новичков, так что следите за анонсами.
