/dev/trouble
Eric Roller's Development Blog

I like Jonathan Penn's screen shooter scripts to automate the recording of screen shots for different iOS devices (screen sizes) as well as for multiple localisations (languages). Having said that, creating the automation scripts can be a pain and maintaining them with every change of the UI can be a real nuisance. …Breathe In… Sigh!

The trouble that I am addressing today, however, is about the resulting images. In my scripts, the screen shots are generated by calling either UIATarget.captureScreenWithName() or its sister function: UIATarget.captureRectWithName(). Sometimes, the images can look as if only partial, corrupted screens have been rendered.

Here is an example of a bad screen shot. You can see that most of the screen is missing. Note that I reduced the image to 25% of its size and added a border to highlight the issue:

bad screen shot

So, what is going on?

I am not sure who is to blame: iOS Simulater, Instruments (whose Automation plugin is used to drive the UI), or the timing of my scripts, which may interrupt some of the simulator's rendering methods.

Anyway, for now, I got the problem to disappear after setting the view size to 100% in the simulator: Window > Scale > 100%. Also, you will have to set this for each of the devices for which you take screen shots!

While some of the simulated devices are far too large to be seen properly on a small screen, the captured images are complete and fully rendered!


P.S.

BTW, UIATarget.captureRectWithName() is a handy function to capture the screen without the title bar:

var target = UIATarget.localTarget();
var rect = target.rect();
target.captureRectWithName({origin:{x:0.0,y:20.0}, size:{width:rect.size.width, height:rect.size.height-20.0}}, "screen3");

Update 2: Automatically Resetting the View Scale

When I wrote "the simulator remembers the last setting" (below), I realised that the solution is to change the preferences. So, to have the view scale to be reset to 100%, automatically, I added this to _reset_all_sims within ui_screen_shooter.sh:

# Reset all device window zoom scales to "100%"
for device in "${simulators[@]}"; do
  # Convert "iPhone 6 Plus (9.1)" -> "iPhone-6-Plus"
  key=$(echo $device | sed 's/ (.*//; s/ /-/g;')
  key="SimulatorWindowLastScale-com.apple.CoreSimulator.SimDeviceType.$key"

  # Check if the key exists.
  scale=$(defaults read com.apple.iphonesimulator $key)
  if [[ $? == 0 && $scale != 1.0 ]]; then
    echo "Resetting Simulator scale for: $device"
    defaults write com.apple.iphonesimulator $key 1.0
  fi
done

Update 1: Script to set View Scale

**Don't use this; use update 2 (above).

A quick search on the Internet pointed to a solution to use AppleScript to select the view scale in the simulator. While this probably works, I chose to use a simpler solution, namely to use an AppleScript to type "Cmd-1" (the menu shortcut for setting the scale to 100%).

function _resize_sim_window {
  osascript -e 'tell application "Simulator" to activate'
  osascript -e 'tell application "System Events" to keystroke "1" using command down'
}

N.B. Depending on your version of Xcode, the simulator may be called "iPhone Simulator", "iOS Simulator", or just "Simulator".

One final hurdle remains: Since this needs to happen after Instruments has opened the simulator with the selected device, it is necessary to run Instruments twice. Therefore, in ui_screen_shooter.sh, where unix_instruments.sh is called, I inserted:

 if (( $do_resize )); then
   # Launch the simulator with an empty automation script.
   touch $trace_results_dir/nop.js \
   "$DIR"/unix_instruments.sh \
  -w "$simulator" \
  -D "$trace_results_dir/nop" \
  -t "$tracetemplate" \
  "$bundle_dir" \
  -e UIASCRIPT "$trace_results_dir/nop.js" \
  -AppleLanguages "($language)" \
  -AppleLocale "$language"

   _resize_sim_window
 fi

 # Run the automation script.
 until "$DIR"/unix_instruments.sh 
 ...

Since _resize_sim_window activates the simulator, it can be difficult to use the computer at the same time. Hence the $do_resize variable which allows us to skip the resizing step. Typically you only need it once at the beginning or whenever you had changed the view sizes (do_resize=1). For subsequent runs you can skip it (do_resize=0) since the simulator remembers the last setting.

This is what I now use to auto-generate the default Main.strings file for my "Base"-localized storyboard.

Basically, whenever the storyboard file is updated, I re-generate the strings file. Most of the time, the resulting strings file doesn't change, but when it does, I use Xcode's version editor to highlight the changes since my last (version control) check-in. I can then make the appropriate changes to the other strings files for all the languages that the project supports.

Here is the build-phase script to Xcode: [more]

Xcode : (Project Navigator) : (Select your Project) : (Select your Target) : Build Phases : ("+" Button) : New Run Script Phase

Change the title to: Re-generate Storyboard Strings File

Shell: /bin/bash

#!/bin/bash
cd ${PROJECT_DIR}

nib_in=${SRCROOT}/Path/Base.lproj/Main.storyboard
str_out=${SRCROOT}/Path/en.lproj/Main.strings

# If the strings file is missing or empty, or
# if the storyboard is more recent:
if [[ ! -n $str_out || $nib_in -nt $str_out ]]; then
  echo "Re-generating: $str_out"
  ibtool --export-strings-file $str_out $nib_in
fi

[On] Show environment variables in build log [Off] Run script only when installing

Input Files ${SRCROOT}/Path/Base.lproj/Main.storyboard

Output Files ${SRCROOT}/Path/en.lproj/Main.strings

I have a XCTestCase scenario using the following methodology:

- (void)testScript {
    NSError* error = nil;
    for (NSURL *scriptURL in [self findScriptFiles]) {

        // Read the entire file.
        NSString *text = [NSString stringWithContentsOfURL:scriptURL
                encoding:NSUTF8StringEncoding error:&error];

        // Test each line.
        [text enumerateLinesUsingBlock:^(NSString *line, BOOL *stop) {

            XCTAssertTrue([self runInstruction:line]);
        }];
    }
}

This works, but I don't like it that all error messages are placed in the testScript source code, whereas they should really be placed inline in the parsed script.

I did a bit of digging in the XCTestCase framework headers and found the solution, using recordFailureWithDescription:inFile:atLine:expected:

- (void)testScript {
    NSError* error = nil;
    for (NSURL *scriptURL in [self findScriptFiles]) {

        // Read the entire file.
        NSString *text = [NSString stringWithContentsOfURL:scriptURL
                encoding:NSUTF8StringEncoding error:&error];
        __block NSUInteger lineNum = 0;

        // Test each line.
        [text enumerateLinesUsingBlock:^(NSString *line, BOOL *stop) {

            lineNum++;

            if ([self runInstruction:line] == NO) {
                // Place the error marker in the parsed file!
                [self recordFailureWithDescription:@"Instruction failed"
                        inFile:[self sourcePathForURL:scriptURL]
                        atLine:lineNum
                        expected:YES];
            }
        }];
    }
}

NB. The scriptURL points to a file within the resources folder of the test target, i.e. not to the original script file in my Xcode project. I therefore need to locate the original script file, for instance relative to the current source file (whose name can be obtained through the __FILE__ preprocessor macro).

[Update 2019-10-23: I am no longer using this; I have switched to a bash script solution.]

For iOS development, it is necessary to provide a set of app icons of different sizes, all the way from a high-resolution icon for the AppStore to a tiny icon that will be used in Spotlight search results. The trouble is, how to create the icons, again, and again,...

Well, here's how...

Starting off with a 1024 x 1024 pixels source image (in PNG format), one would have to scale it multiple times in an image editing tool, to export it into all the sizes. But can't this be automated with AppleScript?

Well, yes, but it gets better: Use Automator !

I have set up this Automator workflow to create all icon images. It works like this:

  • Launch the workflow in Automator and Run it.
  • You will be prompted to select the 1024x1024 PNG source image.
  • You will be prompted to select a destination folder.
  • Wait...

When the workflow is finished, you will find these files in the output folder:

  • iTunesArtwork@2x (1024x1024)
  • iTunesArtwork (512x512)
  • Icon-1024.png (1024x1024)
  • Icon-512.png (512x512)
  • Icon-60@3x.png (180x180)
  • Icon-76@2x.png (152x152)
  • Icon-72@2x.png (144x144)
  • Icon-60@2x.png (120x120)
  • Icon@2x.png (114x114)
  • Icon-Small-50@2x.png (100x100)
  • Icon-40@2x.png (80x80)
  • Icon-Small@2x.png (58x58)
  • Icon-76.png (76x76)
  • Icon-72.png (72x72)
  • Icon-60.png (60x60)
  • Icon.png (57x57)
  • Icon-Small-50.png (50x50)
  • Icon-40.png (40x40)
  • Icon-Small.png (29x29)

But wait, there's more!

Are you missing an icon size? Just edit the workflow to fit your needs.