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

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