/dev/trouble
Eric Roller's Development Blog

Re-running my unit tests on Xcode 9.4, I got stuck on an uncaught exception without any useful debug messages. All I got in the log was:

[...]
Test Case '-[AppTests.AppScriptTests testScriptFiles]' started.
libc++abi.dylib: terminating with uncaught exception of type NSException

and nothing more!

Certainly, I tried re-writing my Swift do-catch-blocks to be able to catch an NSException (Objective-C), but whichever catch line I added, it was never called.

I also tried stepping though the code in the debugger but since the test commands are read from a separate script, and code is spawned on separate threads, this is rather difficult. After trying for a couple of hours yesterday, I gave up.

Today, I remembered a different trick:

Adding an exception breakpoint.

In Xcode,

  1. open the Breakpoint Navigator (Cmd-8);
  2. scroll to the bottom and click "+";
  3. select "Exception Breakpoint…";
  4. since it was an NSException, I chose Exception: "Objective-C", Break: "On Throw", Action: (none);

With that exception breakpoint active, I could re-run my unit tests. There are quite a few legitimate exception throws in my project. For these, it was just a question of selecting "continue" in debugger until I finally landed on a line that was not meant to crash (the last command shown here):

if let allSelectedIndexPaths = self.tableView.indexPathsForSelectedRows {
    for selectedPath in allSelectedIndexPaths {
        self.tableView.deselectRow(at: selectedPath, animated: true)
    }

    self.tableView.reloadRows(at: [ allSelectedIndexPaths ], with: .automatic)
}

So what is wrong with this code?

Answer: A different thread had removed rows from the table view, resulting in me trying to refresh non-existing rows.

MacOS: Internet Recovery

- Posted in macOS by

Starting off with a replaced internal SSD on a MacBook Air, it doesn’t boot, obviously. It is necessary to boot into “Internet Recovery Mode” by selecting Command-R at startup, or, as I did, Option-Command-R to recover onto High Sierra.

I erase the disk in Disk Utility and select to install macOS High Sierra. After a few seconds I am greeted with the error:

The Recovery Server Could Not Be Contacted

A number of possible solutions are listed on the Mac OS X Blog. I used the first one, except that using sudo was not possible (command not found ?); I ran date in the Terminal which confirmed that the date was wrong, then I updated it using Apple’s time server (without sudo):

% ntpdate -u time.euro.apple.com

The run date once more to check that the date & time has been updated.

Back in the High Sierra installer, I get asked to select a destination Disk, but my internal SSD cannot be selected, because:

This disk doesn’t use the GUID Partition Table Scheme

The solution is to use Disk Utility to erase the drive, but in the High-Sierra version of Disk Utility, I had no such option to select the partitioning scheme!!! The Partition button is disabled.

I eventually solved it by rebooting into the old recovery mode (Command-R) and using Lion’s Disk Utility to reformat the drive, before returning to the new recovery mode (Option-Command-R). Checking again in Disk Utility looks fine.

Setup

The following steps are:

  • Create a default (admin) user as part of the installation, but don’t use the usual name to avoid conflicts later. Try "Migrator".
  • Do NOT create a standard user.
  • Connect the Time Machine Backup drive and enter its password.
  • Launch Migration Assistant and restore from the Time Machine backup.
  • Restore all files, including those of our standard user (write down the temporary password). Promote the admin user to be admin.
  • Wait for an hour...
  • Re-install applications (which have not been backed-up).
  • Login, change password, re-authenticate iCloud.
  • Correct file ownership of the Things database, from the admin account:

    % sudo chown gollum "/Users/gollum/Library/Containers/com.culturedcode.things/Data/Library/Application Support/Cultured Code/Things/ThingsLibrary.db"
    
  • Delete the Migrator user.

Quite often I find myself in the Terminal and want to open Xcode with the project in the current directory:

% open CurrentProject.xcodeproj

This works just fine, of course, but with the following alias (added to ~/.cshrc), I don't need to know the name of the project, assuming there is always only one project in the current directory:

% alias xcode '\ls | grep xcodeproj | xargs open'
% xcode

N.B. The use of \ls ignores the alias that I have for 'ls'. Instead of ls *.xcodeproj, the pipe via grep avoids errors like "ls: No match.".

P.S. Knowing the name of the project is actually not the problem; since there are multiple items named "Current" or "CurrentProject" in the same folder, the issue is that I can't just type open SHIFT+C<TAB><CR> (where pressing TAB autocompletes the name); instead, I have to type: open SHIFT+C<TAB>SHIFT+P<TAB>.x<TAB><CR>.

Puzzled by a crash in my application, on a line of code that looked reasonable, it became necessary to dig a little deeper in how some of my Decimal numbers are created:

let newValue = Decimal.init(binary.maskedUInt64Value)

After testing a series of different 64-bit integer values, I found this:

(lldb) po Decimal.init(UInt64.max - 1024)
▿ 18446744073709547520
  ▿ _mantissa : 8 elements
    - .0 : 38912
    - .1 : 39321
    - .2 : 39321
    - .3 : 6553
    - .4 : 0
    - .5 : 0
    - .6 : 0
    - .7 : 0

(lldb) po Decimal.init(UInt64.max - 1023)
error: warning: couldn't get required object pointer (substituting NULL): Couldn't load 'self' because its value couldn't be evaluated

error: Execution was interrupted, reason: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0).
The process has been returned to the state before expression evaluation.

It turns out, any UInt64 value in the range from (UInt64.max - 1023) to UInt64.max crashes Decimal.init(value: UInt64). So, for the time being, I need to prevent values within that range. Here is a possible solution:

let intValue = binary.maskedUInt64Value
var newValue: Decimal
if intValue > UInt64.max - 1024 {
    newValue = Decimal.init(UInt64.max - 1024)    // larger values crash!
    newValue.add(Decimal.init(intValue - (UInt64.max - 1024)))
} else {
    newValue = Decimal.init(intValue)
}

Building on this, one can employ an extension with a custom initializer. In places where such very large numbers may occur, one could then use: Decimal.init(protected: binary.maskedUInt64Value).

extension Decimal {
    init(protected value: UInt64) {
        if value > UInt64.max - 1024 {
            self.init(UInt64.max - 1024)     // larger values crash!
            self.add(Decimal.init(value - (UInt64.max - 1024)))
        } else {
            self.init(value)
        }
    }
}

rdar://30857559

Similar to what I have been doing with subversion, I now also create backups for my git repositories.

Note that the git repositories on the backup host are local repositories (not cloned ones) and that they are named ".git".

From /etc/periodic/monthly/mine.monthly:

if [ -d /Library/Repositories ]; then
  cd /Library/Repositories
  for i in *.git ; do
    # Not a local git repository? Ignore it.
    if [ ! -f "$i/HEAD" ]; then
        continue
    fi
    line="/tmp/monthly-$i.out"
    # File: /Volumes/Backups/Attic/git-file-2016-08-10.bundle
    name="git-`basename $i`-`date +%F`.bundle"
    dump="/Volumes/Backups/Attic/$name"
    if [ ! -f "$dump" ]; then
      echo "Bundling git repository: $name"
      { cd "$i"; git bundle create "$dump" --all; } >& "$line"
      # Change the user for the dump file.
      chown gollum:gollum "$dump"
      # Only output the last stderr line then delete the file.
      tail -1 "$line"
      rm "$line"
    else
      echo "Warning: Dump already exists: $name"
    fi
  done
else
  echo "Error: Repositories directory not found."
fi

The weekly backups are much the same, except that only the three most recent repositories are backed up. Thus, the for loop in /etc/periodic/weekly/mine.weekly changes to:

  for i in `ls -1t *.git | sed '3 q'` ; do
    [...]
    line="/tmp/weekly-$i.out"
    [...]

N.B. I still use TrashLater to keep files for only 6 months in the Attic folder.

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

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.

Blog Reboot: First Post

- Posted in General by

I have decided to switch my blog to a different backend; one that is easier to maintain and doesn't rely on MySQL (which I always found overkill and difficult to backup). Now I only need to extract my old blog posts from that last database dump file...

Update: Having worked through the dump file in a text editor, I found hundreds of duplicate entries marked as 'inherit' and 'nnn-revision-m'. These entries may be the result of multiple edits of a post. This is exactly the kind of data bloat that I strive to avoid. The current solution stores all entries in simple text files. Easy to back up. Easy to index. No duplication.