Updating apps in iOS – Retina Display

I have a bunch of small icons that I use in my apps and for all of them, I just doubled the size of the image and added @2x to the name. For most of the icons that was all I needed to do because the frame I created for the images was a fixed number of points. iOS scaled the images appropriately. For some images I determined the frame size by looking at the size of the image. For those images I had to divide by the scale factor or the images would be twice as big as I wanted. e.g.


CGFloat deviceScale = [UIScreen mainScreen].scale;
cButton.frame = CGRectMake(0, 0, cImage.size.width/deviceScale, cImage.size.height/deviceScale);

All of my games rely heavily on graphics and they are large—too large to include both a normal size and @2x version. I can get the device to display the images as if they were labeled @2x by a simple conversion.


if ([[Utilities deviceType] isEqual:@"iPhone Retina4"] || [[Utilities deviceType] isEqual:@"iPhone Retina35"] ) {
        pictLeft  = [UIImage imageWithCGImage:pictLeft.CGImage  scale:2 orientation:pictLeft.imageOrientation];
        pictRight = [UIImage imageWithCGImage:pictRight.CGImage scale:2 orientation:pictRight.imageOrientation];
    }

This works on the iPhone because the images are way bigger than they need to be. On the iPad they aren’t more than twice the number of pixels that are displayed, so it doesn’t work.

Updating apps to iOS6 – Background images

The new iPhone is taller than the current phones. The default background images need to have a height (in pixels) of 1136 instead of 960 in the original. background images in the game need to be 176 pixels taller. The actual size depends on what the app does for top and bottom toolbars. Landscape backgrounds need to be 1136 pixels wide.

Since I have lots of games, I wrote a simple shell script to make copies the @2x images for the portrait and landscape and the default. You are required to name the default image ‘Default-568h@2x.png’ and iOS will automatically use it when launching on the new iPhone. In fact, that’s how the phone can tell that an app has been updated for the new dimension—otherwise it runs letterboxes. I kept the naming convention for the rest just to make things consistent.


#!/bin/bash
cd /Users/jscarry/Documents/Words/Words/BG\ Default-ArticIV/
cp BG@2x.png BG-568h@2x.png
cp BGLandscape@2x.png BGLandscape-568h@2x.png
cp Default@2x.png Default-568h@2x.png
exit

iOS will not automatically look for background images that fit the new phone’s dimensions. I needed to write a conditional statement to load the correct image if a new iPhone was detected.


UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation]; // iPad doesn't work with device orientation
    if(orientation == UIInterfaceOrientationPortrait || orientation == UIInterfaceOrientationPortraitUpsideDown) {
        if ([[Utilities deviceType] isEqualToString:@"iPhone Retina4"]) {
            self.view.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"BG-568h"]];
        } else {
            self.view.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"BG"]];
        }
    } else {
        if ([[Utilities deviceType] isEqualToString:@"iPhone Retina4"]) {
            self.view.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"BGLandscape-568h"]];
        } else {
            self.view.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"BGLandscape"]];
        }
    }

Note that it will automatically detect the retina display, so you don’t put @2x in the name. I redraw the screen when the device rotates, so I copied this code to the -(void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)orientation duration:(NSTimeInterval)duration method.

I added a method to my Utilities.m to detect device type. That’s why you see this line:


[Utilities deviceType] isEqualToString:@"iPhone Retina4"]

But all you need to test for is the height:


if (screenSize.height == 568) {
            device = @"iPhone Retina4";
}

Sound formats for iOS

This link is useful for understanding sound formats.

For all the sounds that I record, I use .caf format. For pre-recorded sounds I converted them all from .aiff to .m4a using afconvert—a developer tool from the command line. You can also use iTunes.

Here’s the script I use for afconvert. I adopted if from one I found on Sparrow.org. afconvert will convert to lots of formats, but the Apple Lossless makes the smallest file size for me.


#!/bin/bash
# http://www.sparrow-framework.org/2010/06/sound-on-ios-best-practices/
# Reset the field separator to : instead of space so all of the file will be converted
IFS=:
for i in ~/Sounds/*; do
  
  # creates sound.aifc (IMA4 compression, MONO)
  #afconvert -f AIFC -d ima4 -c 1 $i

  # creates sound.m4a (Apple Lossless compression)
  afconvert -f m4af -d aac -q 127 -c 1 $i
done

Copy a file to the Documents directory in iOS

This is a simple method that I use to copy a file from the app bundle to the Documents directory. You can’t modify files in the app bundle, but you can modify them if they are in the Documents directory.


- (void)createResultsDocument {
    
    NSString *documentsDirectory = [self applicationDocumentsDirectory];
    NSString *sourcePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"HTML_header.html"];
    
    NSDateFormatter* dateFormatter = [[NSDateFormatter alloc] init];
    [dateFormatter setDateFormat:@"yyyy-MM-dd"];
    NSString *todaysDate = [dateFormatter stringFromDate:[NSDate date]];
    NSString *resultsFileName = [NSString stringWithFormat:@"MPResults_%@.html",todaysDate];
    NSString *destinationPath = [documentsDirectory stringByAppendingPathComponent:resultsFileName];
    NSError *error;
    
    [[NSFileManager defaultManager] copyItemAtPath:sourcePath 
                                            toPath:destinationPath
                                             error:&error];
    NSLog(@"The copy error is: %@", error);
    
    // Check that it worked.
    NSURL *resultsURL = [NSURL fileURLWithPath:destinationPath];
    NSLog(@"The resultsURL is: %@", resultsURL);
}

Finding Directories in iOS

I created a Utilities singleton to keep things I refer to often. These are the methods I use for referring to directories.


#pragma mark - Application's Documents directory
// Directory locations
+ (NSString *)applicationCachesDirectory {
    return [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
}

+ (NSString *)applicationDocumentsDirectory {
    return [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
}

+ (NSString *)applicationLibraryDirectory {
    
    return [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) lastObject];
}

Here’s one for retrieving the contents of a file stored in the Caches Directory. Notice how it uses [self applicationCachesDirectory] to get the location of the directory.


// Cached files for results
+ (NSString *)cachedFilePath:(NSString *)fileName {
    NSString *pathComponent = [NSString stringWithFormat:@"%@.txt",fileName];
    NSString *filePath = [[self applicationCachesDirectory] stringByAppendingPathComponent:pathComponent];
    return filePath;
}

If I want to store the results in the Documents directory, I’ll use this to construct the path.


        NSString *resultsFilePath = [ [Utilities applicationDocumentsDirectory] stringByAppendingPathComponent:[self formattedHTMLFileName:@"Results"] ];

Here’s a slightly more complicated example where I retrieve the contents of a cached file. Note that it makes use of cachedFilePath:fileName as described above.


+ (NSString *)cachedFileContents:(NSString *)fileName {
    NSStringEncoding encoding; NSError* error = nil;
    NSString *text = [NSString stringWithContentsOfFile:[self cachedFilePath:fileName] usedEncoding:&encoding error:&error];
    return text;
}

Here’s another example. This time I’m loading a string with the contents of the FullResults file.


 NSStringEncoding encoding;
    NSError* error = nil;
    NSString *resultsText = [NSString stringWithContentsOfFile:[Utilities cachedFilePath:@"FullResults"] usedEncoding:&encoding error:&error];