📜 ⬆️ ⬇️

US Visa: My first iPhone app

Why, exactly?


Having a Mac and iPhone, don't try to write a mobile application? Somehow wrong. The benefit here turned up the problem, which is perfectly laid down in the subject, as very useful and at the same time not very difficult to implement. So, I plunged into Objective-C and Cocoa.

Disclaimer


Please remember that not only my first iOS application, but also the first Objective-C application in principle. I never pretend to either the quality of implementation or efficiency, but I want to say that it turned out to be a very holistic, simple example that gives an idea about Objective-C and development for iOS as a whole. Especially for those who do not know this language at all.

Disclaimer 2


This post was originally published as an article in The Pragmatic Bookshelf Magazine in English - US Visa: My First iPhone App . The Russian version published here is not an exact translation of the journal version, since it was written as a separate text a little later.

"Houston! We have a problem!"


Over the past year I have been forced to apply for a US visa at the embassy in London several times. Every time I was told that specifically in my case “administrative processing” is required. Documents are accepted from you, but then they give a number (batch number) instead of a visa and say periodically to look at their website, where there is a PDF, in which you should look for this number for instructions on what to do next (send more documents, send a passport and etc.). Click on the link with the official PDF , open the file, press CTRL-F, enter the number (batch number) and go ahead.
')
The idea of ​​automation arose - to make an application for an iPhone, into which it can drive the application number once, and then with one click on the button to receive the visa processing status. The application must be able to download a PDF file, parse it and extract the data on the application.

What if I have windows?


All is not lost. Objective-C can be run on Windows through Cygwin or MinGW. Moreover, the GNUstep project makes it possible to use the AppKit and Foundation libraries for writing graphical programs in Windows in Objective-C. Alas, I will not dive so deep in this article. We will make only the application running on the command line. It will be able to download PDF and parse it. You can build the application on both Windows and Mac. After that, we will use virtually no changes to the modules of this application to create a full-fledged program for iOS. But, alas, this is only for Mac owners. You can, of course, put Hackintosh on a virtual machine and drive an application on an iPhone simulator in Xcode, but it’s unlikely to load it into a real iPhone without a real Mac.

Installing GNUstep under Windows


I found two great posts:

It would be nice to get acquainted with Objective-C and iOS API


I didn't know anything about Objective-C, except rumors about his unusual approach to memory management, so I had to scroll through the following books.

Warning : The links below contain my personal affiliate program number with Amazon. From possible purchases made after clicking on these links, I can get a small percentage. If this does not suit you, please do not click on the links, or manually “clean” the URL through cut-paste. Thank you for understanding.

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)



And there is one magical free document - “ From C ++ to Objective-C ”.

So, the task is divided into three main parts:

After getting acquainted with Objective-C, I can say that for a less experienced developer in C or C ++, especially if I have experience in developing UI (I used to tinker a lot with Delphi / C ++ Builder), “move in” in Objective- C and Cocoa is easy. It is enough to focus on a very unusual semi-manual memory management model (especially after RAII in C ++ and garbage collection in Java). Objective-C itself manages memory, but control over the counting of references to objects for their proper release lies with you. It is necessary to understand the principle, otherwise memory leaks are inevitable. I was exactly like that at the beginning. The benefit of the excellent profiling tools in Xcode allow you to identify major problems almost immediately.

Below I will give a few personal subjective impressions, as a newcomer to Objective-C and Cocoa. It is unlikely that it will be interesting if you already have experience in them, but if not, I think it will be interesting.

For a start, it's interesting to see how the names of member functions of a class are formed in Objective-C. It is almost like a human language. If I say in English in the Objective-C:

+ (bool)findInPortion:(NSMutableData *)someData needle:(NSString*)aNeedle andAddTo:(NSMutableArray*)aList { ... } 

If you read this code from left to right from top to bottom, you get an almost complete sentence. Formally, the full name of this method is findInPortion:needle:andAddTo: The arguments are named, and their names are part of the full name of the method. If it is correct to give variable argument names ( someData , aNeedle and aList ), then you can actually write in English. Of course, this is all a rather “verbose” approach, but the fantastic prediction system in Xcode when typing in the code allows you to quickly and easily fill in all these momentum. Note also that the traditional alignment when splitting long strings occurs by a colon, separating the formal name of the parameter from the variable representing it.

In Objective-C, unconventional syntax for calling methods. For example, instead of:

 NSMutableArray* list = NSMutableArray.alloc.init; 

is written:

 NSMutableArray* list = [[NSMutableArray alloc] init]; 

It looks weird, but it's a matter of habit. Again, the code prediction system as you type allows you to enter square brackets even almost physically without stuffing them.

Objective-C and Cocoa are actively using several programming patterns that you simply need to master. For example, delegates. They are everywhere in Cocoa. A delegate is a class that contains callbacks. Together, the transmission of a bundle of individual functions or methods is simply transferred to a single object that implements all the required callbacks. For example, I used the standard NSURLConnection class to download a PDF. This class requires the delegate NSURLConnectionDelegate to be given to it, whose methods are called on various events during the download process.

So, a couple of weeks of evening vigils for the books, and I sketched the skeleton of my first application. But it was only the first part of Marlezonsky ballet. Next, it was necessary to deal with the format of PDF.

PDF parser


As already mentioned, the file containing information from the embassy, ​​in PDF format. A description of this format is available on the Adobe website . I used the document " PDF Reference third edition, Version 1.4 ".

Parsing PDF I have implemented very Kondovo. Since the data comes in chunks, we will analyze the document in parts, sequentially. Each new piece of data is added to the buffer and we try to parse the PDF format in it. First we look for fragments framed in stream and endstream . The content of each such block is “expanded” through zlib/inflate . After this is a clean text, and we are looking for our batch number in it, of course, taking into account the PDF markup language. If the number is found, then print it and go to the next block.

The main steps of the parser:
  1. If there is a block in data currently received, limited by stream\r\n and endstream\r\n tags, then we cut it out of the buffer and zlib/inflate via zlib/inflate .
  2. Decompressed in the first step is a text block. We need to find in it fragments framed with the tags BT\r\n (Begin Text) and ET\r\n (End Text). Find all such blocks and combine them into a list of strings.
  3. Inside each line found in step 2, we delete the substrings that are not surrounded by parentheses. Everything around the parentheses is service information, and we do not need it.
  4. So, we have isolated the pure text from PDF'ki. Logically, the information in this file is organized in a table with three columns: application number (batch number), status and date. Alas, among this there are also page headers. To deselect them, we will see that if the current line looks like a batch number (11 digits), then the status line and the date string always follow it. We take them and wait for the new batch number again.


As I said, the parsing is sharpened for a specific file, and if it is changed at the embassy, ​​then everything will break. If you at least use regular expressions, it will be much more flexible, but I will leave this to readers for self-study.

SUPPLEMENT . In the process of working on the article, an idea appeared to make a special web service, referring to which, using simple URLs, you can receive data about the application, and the entire “kitchen” for parsing PDFs occurs “on the cloud”. Dr.Dobb's recently published my article, RESTful Web Service in Go Powered by the Google App Engine , describing this approach. Those interested can “finish” the application to work through this web service. You can generally do it cleverly: first turn to the web service, and if there is an answer from him, then finish at that, and if not, start the process of self-downloading and parsing the PDF.

Command line application


So, we know almost everything to write an application that will download a PDF and extract information from it on our application. The application will work from the command line. It can be collected from on a Mac, and on Windows via GNUstep and Clang. Further, the source files of this application will be used unchanged for the iOS version.

Files:

BatchPDFParser.h


This file contains the declaration of the Batch class, which contains information about updating the status of the application, and the BatchPDFParser class, which implements the findInPortion:needle:andAddTo: method (by the way, this is a static class method, see + beginning of the line?).

 @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


In this file, the implementation of the PDF parser.

 #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 

The findInData:fromOffset:needle: method searches for a substring in this data block (of type strstr() ). The search is primitive, and it can be accelerated, for example, by implementing the KMP algorithm.

 + (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; } 

The isBatchNumber:number: method isBatchNumber:number: checks if the string is a request number (batch number):

 + (bool) isBatchNumber:(NSString*)number { long long const value = [number longLongValue]; return value >= 20000000000L && value < 29000000000L; } 

The findBatchNumberInChunk:needle:andAddTo: searches for fragments framed with BT and ET tags. In them highlights the text in parentheses, and already among the found out specifically identifies the application number, status line and date string.

 + (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; } 

Now the main method findInPortion:needle:andAddTo: Here, pieces are framed, framed with stream\r\n and endstream\r\n tags, the content is expanded using zlib/inflate and passed to findBatchNumberInChunk:needle:andAddTo: for analysis.

 + (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 delegate NSURLConnectionDelegate :

 @protocol DirectDownloadViewDelegate<NSObject> - (void)setProgress: (float)progress; - (void)appendStatus: (NSString*)status; - (void)setCompleteDate: (NSString*)date; @end 

DirectDownloadDelegate.h


Actually, the delegate of 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


And its implementation:

 #import <Foundation/Foundation.h> #import "DirectDownloadDelegate.h" #import "BatchPDFParser.h" @implementation DirectDownloadDelegate @synthesize error, done, found; 

The initWithNeedle:andViewDelegate: constructor creates a delegate and parameterizes it with another delegate, DirectDownloadViewDelegate , which will be used for the screen update task. Here, by the way, the first time we see the destructor, (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: called when the connection is complete.

 - (void) connectionDidFinishLoading:(NSURLConnection *)connection { done = YES; NSLog(@"Connection finished"); } 

connection:didFailWithError: method connection:didFailWithError: Causes an error when downloading a file.

 - (void) connection:(NSURLConnection *)connection didFailWithError:(NSError *)anError { error = [anError retain]; [self connectionDidFinishLoading:connection]; } 

The connection:didReceiveData: called when a new portion of data from the channel has been received. We add each such batch to the buffer, update the download progress indicator (via another delegate, viewDelegate ), then try to isolate data fragments using PDF format, and finally print what was found.

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

The last callback of the NSURLConnectionDelegate delegate that we use is called connection:didReceiveResponse: It is called when an HTTP HTTP response is received, containing headers. We take the length of the future file from the Content-Length header in order to update the download indicator later.

 - (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


This file contains the donwloadAtURL:searching:viewingOn: , which we add to the NSURLConnection class. The interesting thing is that through the concept of categories in Objective-C, you can “add” new methods to existing classes. Here we add the category DirectDownload to the NSURLConnection class.

 @interface NSURLConnection (DirectDownload) + (BOOL) downloadAtURL:(NSURL *)url searching:(NSString*)batchNumber viewingOn:(id)viewDelegate; @end 

NSURLConnectionDirectDownload.m


Well, the final part of the download PDF. The donwloadAtURL:searching:viewingOn: creates a connection and starts downloading. Then there is a wait in the NSRunLoop cycle until the download is complete. This cycle allows the application to respond to events during the download. Please note that this download is still not tied to the graphical interface. It uses the viewDelegate delegate to communicate with the application “face”.

 #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


As already mentioned, in the command line application the controller will contain only stubs, which we will implement later in the full version of the program.

 #import <Foundation/Foundation.h> #import "DirectDownloadViewDelegate.h" #define IBAction void 

Empty stub class ViewController .

 @interface ViewController : NSObject <DirectDownloadViewDelegate> @end #import "NSURLConnectionDirectDownload.h" 

Address where to download the file.

 static char const* const pdf = "http://photos.state.gov/libraries/unitedkingdom/164203/cons-visa/admin_processing_dates.pdf"; 

And the mock-implementation of the controller class.

 @implementation ViewController 

The test callback appendStatus: is called when the next request update is detected. Here we just log in, and in the full application we will update the screen form.

 - (void) appendStatus:(NSString*)status { NSLog(@"appendStatus(): '%@'", [status stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"]); // Some code is skipped here because not required for the command line mode. } 

Test callback setProgress: called when, after accepting the next piece of data, you need to update the download indicator.

 - (void) setProgress:(float)progress { // Some code is skipped here because not required for the command line mode. } 

setCompleteDate: test callback setCompleteDate: called when the PDF parsing is complete. Here, again, we just log.

 - (void) setCompleteDate:(NSString*)date { NSLog(@"setCompleteDate(): '%@'", date); // Some code is skipped here because not required for the command line mode. } 

Well, the final method that runs everything, updateBatchStatus: In the full program it will be called when you click on the form. Here it is called from 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


Run from the command line.

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

Let's try to collect all this and run it?


Makefile for Mac:

 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 

GNUmakefile GNUstep Makefile:

 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 

Type 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 ... 

You can run to check the real application:

 make run 

I got the following:

 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' 

So, everything works: download and parser PDF. Now let's do a version for iOS. Alas, only for Mac users.

Screen Layout


I made the application extremely simple: one form with an input field, a button and a place to display updates.



The download indicator and spinner appear temporarily.

ViewController.h


Now it is a complete controller implementation. Through the TESTING macro, I made a separation between the simplified and full versions.

 #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]; } 

Now the callback appendStatus: not only logs, but also updates the screen form.

 - (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: updates the download indicator.

 - (void) setProgress:(float)progress { #ifndef TESTING updateProgressView.progress = progress; #endif } 

setCompleteDate: displays the date of the update in a text field on the screen.

 - (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]; } 

Now a few calls specific to iOS. The viewDidLoad: method viewDidLoad: called by the system when the screen form is loaded and ready to use. Here we manually create a spinning slider and correct the heights of the two elements, buttons and input fields, as for some reason Xcode Interface Builder does not allow changing them when designing a form.

 #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 called when the form becomes inactive.

 - (void)viewDidUnload { [super viewDidUnload]; // Release any retained subviews of the main view. } 

The method shouldAutorotateToInterfaceOrientation:allows you to control the behavior to change the orientation of the device. Here we only allow portrait position, not upside down.

 - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { return (interfaceOrientation == UIInterfaceOrientationPortrait); } #endif 

The method launchUpdate:calls when you click on the button Updateon the form. We block the button from pressing again, displaying the download indicator and spinning slider.

 - (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 } 

Methods saveProperties:and loadProperties:save and restore the contents of the form when you start and stop the application. Please note that in order to save the data in a file, you need to ask the system for the position of the intended for this directory.

 - (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 

Everything!We reviewed all the main files. The application is completely ready. You can collect and fill in the device (do not forget to buy a developer license from Apple).

I posted the full project on GitHub - usvisa-app . Comments and thoughts are accepted.

You can check out the video:



And further!


If you are thinking about selling your application for a million copies, you should start with a beautiful icon. For applications, you usually need several: 57x57 and 114x114 for the application itself, and 512x512 and 1024x1024 for publishing in the AppStore.

We will proceed easier and take the icon from open sources - The Great Seal of the United States .



PS


I decided to write a post about this application after the AppStore censors “wrapped” it, referring to the clause in the rules, which states that applications with minimal functional load that can be implemented via HTML5 will not be allowed. Apparently, they no longer want to see applications that fart or simply display a static image of applications. One could argue with the censors on the minimum functional load or implementation through HTML5, but I scored. Firstly, I personally like the fact that Apple is trying not to miss useless and low-quality applications, and secondly, I already got a lot of pleasure from learning Objective-C, and at the moment I am working on two more applications.

Pps


Soon there will be another article about the development of applications for iOS for beginners, so stay tuned .

Source: https://habr.com/ru/post/150959/


All Articles