/dev/trouble
Eric Roller's Development Blog

iOS

On the development for iPhone using iOS.

When creating screenshots with fastlane, there is support for checking at runtime within the app whether fastlane is being used by looking at a user default:

if UserDefaults.standard.bool(forKey: "FASTLANE_SNAPSHOT") {
    // runtime check that we are in snapshot mode
}

Unfortunately, that does not work from within the test code since the tests are run in a separate helper app!

In the test code, I would like to use something like the following such that I can test and tweak the code within Xcode before running fastlane on it:

#if FASTLANE
    snapshot("1_init", timeWaitingForIdle: 0)
#else
    let attach = XCTAttachment(screenshot: XCUIScreen.main.screenshot())
    attach.lifetime = .keepAlways
    add(attach)
#endif

One would think the FASTLANE_SNAPSHOT=YES build setting would be suitable for this task, but I have not found any way to detect it from within Swift, possibly because values like YES are not supported by the compiler.

This, however, seems to do the trick: Add this line to your Snapfile:

# Add a define for xcodebuild
xcargs "SWIFT_ACTIVE_COMPILATION_CONDITIONS=FASTLANE"

While creating UI testing scripts to create screenshots, it became necessary to detect which iOS device is currently loaded in the Simulator, specifically whether its is an iPhone or an iPad. This can be done by inspecting environment variables like SIMULATOR_DEVICE_NAME:

// Assume iPad as default
var deviceIsPhone = false

override func setUp() {
    super.setup()

    // The variable is only present in the simulator
    if let simDeviceName = ProcessInfo().environment["SIMULATOR_DEVICE_NAME"] {
        print("Testing Device: \(simDeviceName)")
        deviceIsPhone = simDeviceName.hasPrefix("iPhone")
    }
}

Other helpful variables are:

"SIMULATOR_MODEL_IDENTIFIER": "iPad5,4"
"SIMULATOR_DEVICE_NAME": "iPad Air 2"
"SIMULATOR_MAINSCREEN_HEIGHT": "2048"
"SIMULATOR_MAINSCREEN_WIDTH": "1536"
"SIMULATOR_MAINSCREEN_SCALE": "2.000000"

Also, to list all variables in your simulator log, try this:

print(ProcessInfo().environment.debugDescription.split(separator: ",").joined(separator: "\n"))

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.

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

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.

UIScrollView vs. AutoLayout

- Posted in iOS by

Setting up a scroll view with multiple subviews (one for each page), I found myself unable to scroll. Everything looked fine. I even had the AutoLayout constraints defined for each of the subviews: pinned width, pinned height, top 0 pt, left x pt (depending on which page).

It took several debugging sessions to work out that the contentSize of the scroll view had been set (through AutoLayout) (after viewDidAppear:) to the size of a single page. Despite my constraints.

The fix is not obvious: there must be constraints for both the top-left corner as well as the bottom-right corner. Once I added an additional constraint for the last view to the right side, the contentSize was correctly calculated to contain all pages.

This tip came from a comment on StackOverflow.

For a while now, I have been puzzled by the fact that Mobile Safari, the iOS version of Safari, opened any page on my web site at a much larger width than the CSS value for the main content area (570px).

It turns out, there are a number of Apple-Specific Meta Tags that can be used to provide background information for your web page, like how to scale it, or providing scaling limits.

To set the initial scaling of a window when presented on the iPhone or iPad, there is the viewport meta tag. If missing, a default width of 960 pixels will be used, or more if the web content is wider. Setting it to 590 pixels, thus adding a 10 pixel padding, works just fine for my particular case:

<meta name="viewport" content="width:590px">