What to do when creating a PDF crashes your iOS device.

If you create PDF’s using the iOS SDK, and UIKit as mentioned in my previous post, you may find that while the PDF’s generate perfectly on the simulator, they crash your app on a real device. The reason it works on the simulator and not on the iOS device, usually has something to do with how much memory each has. The simulator has as much as your development machine, while the iOS device has… well, considerably less.

If you find your app crashing, what I’ve found helps the most is slicing your large images into smaller images to remove the whitespace in your images. When you create a PDF using UIKit, each image is loaded into memory as RGBA to be pasted into the PDF. If you’re using the x4 method to get high resolution PDF’s, that means a full page ‘background’ would take up almost 8 MB. Much of this is waste, since you’re probably going to have quite a bit of whitespace.

Creating, printing and emailing high resolution PDF’s in iOS

Many kinds of business software require some form of PDF output. The easiest way in iOS to create PDF’s is using Interface Builder and the UIKit.

Generate a PDF using UIKit

First, create a blank new empty user interface file in XCode. I’ll assume you called it PDFView.xib. Make whichever view controller will be generating the PDF the owner.

Add the main pdf view, and make it 612×792, which Apple says is the standard size for a full 8.5×11 sheet of paper. (You’ll need it a standard size if you want to print, since AirPrint doesn’t allow you to shrink larger pdf’s to fit, instead cropping off the sides that are too big or printing the page tiled across multiple pages.) Assign the main view to an UIView IBOutlet in your owner view controller; I’ll assume its called PDFView1.

In the main pdf view, lay out all your UI elements as UIViews (to group), UIImageViews, and UILabels. You can add other UI elements if you want, but most standard documents can be created with only those two.

You can add more views to the file if you want to add more pages to your PDF.

When you want to create your PDF, you would do something like this:

- (NSData*)generatePDF {
   NSMutableData * pdfData=[NSMutableData data];

   [[NSBundle mainBundle] loadNibNamed:@"PDFView" owner:self options:nil];

   // by default, the UIKit will create a 612x792 page size (8.5 x 11 inches)
   // if you pass in CGRectZero for the size
   UIGraphicsBeginPDFContextToData(pdfData, CGRectZero,nil);
   CGContextRef pdfContext=UIGraphicsGetCurrentContext();

   // repeat the code between the lines for each pdf page you want to output
   // ======================================================================
   UIGraphicsBeginPDFPage();

   // add code to update the UI elements in the first page here
	
   // use the currently being outputed view's layer here	
   [self.PDFView1.layer renderInContext:pdfContext];

   // end repeat code
   // ======================================================================

   // finally end the PDF context.
   UIGraphicsEndPDFContext();

   // and return the PDF data.
   return pdfData;
}

Pretty simple eh?

Saving/Printing/Emailing the PDF.

Once you’ve generated the PDF, its easy to save/print/email.

Saving is just a matter of writing it to a file:

- (BOOL)savePDF:(NSData*)pdf toFile:(NSString*)filePath {
   if(![data writeToFile:filePath atomically:NO]){
      NSLog(@"Failed to pdf to file '%@'", filePath);
      return NO;
   }
   return YES;
}

Printing is not much harder. We simply grab the shared print controller, set up the orientation, job name, etc, set the printer’s printingItem to our PDF data, and present the printer controller to the user, which lets them choose which printer to use. iOS takes care of the rest.

- (void)printPDF:(NSData*)pdfData {
   UIPrintInteractionController *printer=[UIPrintInteractionController sharedPrintController];
   UIPrintInfo *info = [UIPrintInfo printInfo];
   info.orientation = UIPrintInfoOrientationPortrait;
   info.outputType = UIPrintInfoOutputGeneral;
   info.jobName=@"CadabraCorp.pdf";
   info.duplex=UIPrintInfoDuplexLongEdge;
   printer.printInfo = info;
   printer.showsPageRange=YES;
   printer.printingItem=pdfData;
		
   UIPrintInteractionCompletionHandler completionHandler =
      ^(UIPrintInteractionController *pic, BOOL completed, NSError *error) {
         if (!completed && error)
         NSLog(@"FAILED! error = %@",[error localizedDescription]);
      };
   [printer presentAnimated:YES completionHandler:completionHandler];
}

Emailing is similarly simple. We just create a new MFMailComposeViewController, add the data as a pdf attachment, and present it to the user to fill in email address, etc. (We could put in defaults before we present it, but I leave that to your imagination.):

- (void)emailPDF:(NSData*)pdfData {
   MFMailComposeViewController *picker = [[MFMailComposeViewController alloc] init];

   picker.mailComposeDelegate = self;
   [picker setSubject:@"Sending PDF"];
   [picker addAttachmentData:pdfData mimeType:@"application/pdf" 
                    fileName:[NSString stringWithFormat:@"CadabraCorp.pdf"]];
   [picker setMessageBody:@"Here's the PDF you wanted." isHTML:YES];

   [self presentModalViewController:picker animated:YES];
   [picker release];
}

#pragma mark = MFMailComposeViewControllerDelegate function

- (void)mailComposeController:(MFMailComposeViewController*)controller 
          didFinishWithResult:(MFMailComposeResult)result error:(NSError*)error {
   [self dismissModalViewControllerAnimated:YES];
   if (result==MessageComposeResultSent) {
      NSLog(@"PDF sent");
   } else {
      NSLog(@"PDF send error %@",error);
   }
}

Why does it look so pixelated?

Unfortunately, using the standard way of creating PDF’s with UIKit results in a bit of a problem… By default, UIKit uses 72dpi, and that leaves quite a bit of it looking pixelated. The only solution I’ve found is to make the View’s larger (I usually do 4x larger to make the math easy) and then shrink it when generating the PDF. This results in a higher resolution PDF that is still a full page and prints properly at 100%.

First, resize each UI element by multiplying its x/y/width/height by 4. Any fonts should have their point sizes multiplied by 4 as well. You may have to create new images for the UIImageView’s you use.

Next, modify the creation of the output PDF to scale the layer’s:

- (NSData*)generatePDF {
   NSMutableData * pdfData=[NSMutableData data];

   [[NSBundle mainBundle] loadNibNamed:@"PDFView" owner:self options:nil];

   // by default, the UIKit will create a 612x792 page size (8.5 x 11 inches)
   // if you pass in CGRectZero for the size
   UIGraphicsBeginPDFContextToData(pdfData, CGRectZero,nil);
   CGContextRef pdfContext=UIGraphicsGetCurrentContext();

   // repeat the code between the lines for each pdf page you want to output
   // ======================================================================
   UIGraphicsBeginPDFPage();

   // add code to update the UI elements in the first page here
	
   CGContextSaveGState(pdfContext);
   CGContextConcatCTM(pdfContext,CGAffineTransformMakeScale(.25, .25));

   // use the currently being outputed view's layer here	
   [self.PDFView1.layer renderInContext:pdfContext];

   CGContextRestoreGState(pdfContext);

   // end repeat code
   // ======================================================================

   // finally end the PDF context.
   UIGraphicsEndPDFContext();

   // and return the PDF data.
   return pdfData;
}

That’s it! The end result is PDF’s of exactly the same size (8.5×11) but with higher resolution text and images.

Grabbing a web file’s last modified date

If you rely on updating files stored on a server, you want a way to find out when a file has been updated on that server, so you can download the latest version.

The easiest way I’ve found is using NSURLConnection’s sendSynchronousRequest:returningResponse:error method, but using a HEAD http method instead of GET. Here’s a simple example:

+ (NSDate*)getFileDate:(NSString*)httpFilePath {
    NSURL *url=[NSURL URLWithString:httpFilePath];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    
    [request setHTTPMethod:@"HEAD"];
    NSHTTPURLResponse *response;
    [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:NULL];
    if (response==nil) return nil;
	
    NSDate *lastModifiedDate=nil;
    NSString *lastModified=[[response allHeaderFields] objectForKey:@"Last-Modified"];
    @try {
        NSDateFormatter *df = [[[NSDateFormatter alloc] init] autorelease];
        df.dateFormat = @"EEE',' dd MMM yyyy HH':'mm':'ss 'GMT'";
        df.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"];
        lastModifiedDate = [df dateFromString:lastModified];
    }
    @catch (NSException * e) {
        NSLog(@"Error formatting Last-Modified date: %@ (%@)", lastModified, [e description]);
    }
    return lastModifiedDate;
}

For completeness sake, to grab a file, we’d use code like this:

+ (NSData*)getFileData:(NSString*)httpFilePath {
    NSURL *url=[NSURL URLWithString:httpFilePath];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    
    [request setHTTPMethod:@"GET"];
    NSHTTPURLResponse *response;
    NSData *result=[NSURLConnection sendSynchronousRequest:request returningResponse:&response error:NULL];
    if (response==nil || response.statusCode>400) return nil;

    return result;
}

You can do something similar asynchronously, but its a bit more complicated.

Fixing blurry UIButton’s.

Once in a while you’ll notice that your UIButton images on your iOS device and simulator seem a bit blurry, while in XCode they seem fine. One possible reason is that your png images are an odd width and/or height. This seems to be because iOS tries to center the image in your UIButton, and since half of an even number is not an integer, it has to antialias everything to fit nicely.

The fix is simple: Add an extra pixel to the width and/or height to make it an even number.

Storing/Accessing files in your app folder for private or public use.

When you do any kind of file processing in iOS, you probably want to store the original file and/or the new file somewhere.

Public or Private?

You can store files in your apps ‘sandbox’ either for private use, where only your app can access it, or public use, where iTunes can see the files and will let you add/remove files.

For public use, we grab the NSDocumentDirectory path:

	NSArray *publicPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
	NSString *publicDocumentsPath = [publicPaths objectAtIndex:0];

(You’ll also have to add a property called ‘Application supports iTunes file sharing’, set to TRUE in your Custom iOS Target Properties section to get this working.)

For private use, we grab the NSLibraryDirectory path:

    NSArray *privatePaths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
    NSString *privateDocumentsPath = [privatePaths objectAtIndex:0];

Getting a directories contents

Once you’ve got the appropriate path, its easy to iterate through the contents at that location returned by contentsOfDirectoryAtPath:error:

    NSArray *files=[[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:NULL];
    for (NSString *file in files) {
        NSString *filePath=[path stringByAppendingPathComponent:file];
        NSLog(@"Path for file '%@' at '%@'",file,filePath);
    }

Reading, Writing, and ArithmeticDeleting

You can see if a file/directory exists at a given path with fileExistsAtPath:isDirectory:

    BOOL isDir;
    if ([[NSFileManager defaultManager] fileExistsAtPath:filePath isDirectory:&isDir]) {
        NSLog(@"File '%@' exists as %@",filePath,isDir?@"Directory":"File");
    }

You can read a file

    NSData *data;
    if ((data=[[NSFileManager defaultManager] contentsAtPath:filePath])==nil) {
        NSLog(@"Couldn't get contents of file at '%@',filePath);
    }

You can create a directory with createDirectoryAtPath:withIntermediateDirectories:attributes:error:, which optionally creates any necessary intermediate directories for you:

    if ([[NSFileManager defaultManager] createDirectoryAtPath:folderPath
                                  withIntermediateDirectories:YES
                                                   attributes:nil error:NULL]==NO) {
        NSLog(@"Error creating folder '%@'\n",folderPath);
        return NO;
    }

You can write to a file with createFileAtPath:contents:attributes:

    NSData *data=...; // data you want to write to the file
    if ([[NSFileManager defaultManager] createFileAtPath:filePath contents:data attributes:nil]==NO) {
        NSLog(@"Error writing file.");
    }

You rename/move files and directories with moveItemAtPath:toPath:error:

    if ([[NSFileManager defaultManager] moveItemAtPath:oldFile toPath:newFile error:NULL]==NO) {
        NSLog(@"Error moving file from '%@' to '%@'\n",oldFile,newFile);
    }

You delete a file/directory with removeItemAtPath:error:

    if ([[NSFileManager defaultManager] removeItemAtPath:filePath error:NULL]==NO) {
        NSLog(@"Error deleting '%@'\n",filePath);
        return NO;
    }

Caveat

There are many other functions in the NSFileManager class that you can use.

For simplicity sake, I’ve used NULL’s for all the errors. In actual code you should handle errors appropriately.

Keeping your splash screen on-screen longer

Depending on how big your app is, your splashscreen may come and go before the user has a change to see it in all its splashy goodness.  Here’s a simple way to keep it on-screen a bit longer, even if you have tab controllers, navigation controllers, etc on your initial screen.

Just before your [self.window makeKeyAndVisible] call in your app delegate’s didFinishLaunchingWithOptions function, add the following code (changing the Default.png filename to your app’s splash screen png):

    UIImageView *ssIV = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"Default.png"]];
    ssIV.userInteractionEnabled = YES;
    [self.navController.view addSubview:ssIV];
    [ssIV release];
    [self performSelector:@selector(removeSplashScreen:) withObject:ssIV afterDelay:5.0];

Also add the removeSplashScreen: selector to remove it after the 5 second delay:


- (void)removeSplashScreen:(UIImageView *)ssIV {
    [ssIV removeFromSuperview];
}

The code is pretty basic, but what you’re essentially doing is creating a UIImageView and covering your original UI with it, making sure that it doesn’t let any touches go through to the original UI.  After the 5 second delay, it removes the view, and your original UI appears.