Distilled iOS development tips

A disparate collection of iOS development tips.

How to get more accurate Xcode test coverage

A next step from testing your code is knowing how much of your code is covered by these tests. To do that, you have to first open the Edit Scheme and tell Xcode to Gather coverage data. (This setting is as of Xcode 8.2.1 disabled by default.) Gather coverage data setting in Xcode

Running unit tests as is would give you inaccurate coverage, as it would take into account AppDelegate and everything it touches. (Because app process hosts these unit tests and they are executed after receiving notification for UIApplicationDidFinishLaunching.) This is easily seen on a new project, using the Single View Application template that comes with AppDelegate.swift and ViewController.swift files. As you see below, I’ve also added AnIntegration.swift that contains only one empty function which gets called from AppDelegate and so, by definition, has 100% coverage: Inaccurate coverage

To get accurate code coverage first add a AppDelegate barebones replica to your test-target:

import UIKit

class TestAppDelegate: UIResponder, UIApplicationDelegate {
  var window: UIWindow?
  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    return true
  }
}

Then add main.swift to the app-target in and finally tell Xcode to, when testing, replace AppDelegate with the above TestAppDelegate:

import Foundation
import UIKit

let argc = CommandLine.argc
let argv = UnsafeMutableRawPointer(CommandLine.unsafeArgv).bindMemory(to: UnsafeMutablePointer<Int8>.self, capacity: Int(argc))
let appDelegateClass: AnyClass = ProcessInfo.isUnitTesting
  ? NSClassFromString("MyAppTests.TestAppDelegate")!
  : AppDelegate.self

UIApplicationMain(argc, argv, nil, NSStringFromClass(appDelegateClass))

The ProcessInfo.isUnitTesting above is an extension on ProcessInfo:

import Foundation

extension ProcessInfo {
  static var isUnitTesting: Bool {
    return processInfo.environment["XCTestConfigurationFilePath"] != nil
  }
}

Since you supplied main.swift file, you now also have to remove the @UIApplicationMain attribute that’s right above your AppDelegate:

@UIApplicationMain // remove thisclass AppDelegate: UIResponder, UIApplicationDelegate {

Finally, run the tests again and coverage of AnIntegration is now at the actual value of 0%: Accurate coverage with a test AppDelegate

How to make Xcode downloads robuster

This comes handy especially when on an unreliable network. Sign in to AppStoreConnect, open Chrome DevTools, and click the download link. The last request you’ll see in the DevTools’ Network tab will be the download request you just made. Right click on it, choose ‘Save as cURL’, and wrap the copied request in a loop:

for ((i=1;i<=100;i++)); do
curl 'https://download.developer.apple.com/Developer_Tools/Xcode_12_beta_3/Xcode_12_beta_3.xip' \
  -H 'authority: download.developer.apple.com' \
  -H 'pragma: no-cache' \
  -H 'cache-control: no-cache' \
  -H 'upgrade-insecure-requests: 1' \
  -H 'user-agent: {your-agent}' \
  -H 'accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9' \
  -H 'sec-fetch-site: same-site' \
  -H 'sec-fetch-mode: navigate' \
  -H 'sec-fetch-user: ?1' \
  -H 'sec-fetch-dest: document' \
  -H 'referer: https://developer.apple.com/download/' \
  -H 'accept-language: en-US,en;q=0.9,sl;q=0.8,de;q=0.7' \
  -H 'cookie: {your-cookie}' \
  --compressed \
  -o Xcode-beta.xip \
  -L \
  --continue-at -sleep 1
done

The {your-cookie} part should be filled in for you. The cookie can be also be fetched through other means, from Chrome DevTools or a Chrome extension. The important part above is the highlighted --continue-at flag. It instructs curl to remember the offset of already-transferred bytes and, if needed, resume the transfer of the same file from this offset.

How to streamline Xcode upgrades

It seems unavoidable for new Xcode versions to override any custom keybindings. Ditto for a custom, brighter text-cursor, which is pretty much a necessity when using a darker background. And let’s not forget the plugins. Since Xcode updates aren’t that rare, here is a script of all the steps:

update_xcode {
  # Change if not using default naming, "Xcode" and "Xcode-beta"
  _update_xcode "Xcode"
  [ -d "/Applications/Xcode-beta.app/" ] && _update_xcode "Xcode-beta"
}

_update_xcode {
  XCODE_PATH="/Applications/$1.app"
  PLUGINS_PATH="$HOME/Library/Application Support/Developer/Shared/Xcode/Plug-ins"
  SOURCE_PATH="$HOME/path/to/your/keybindings_etc"

  # Update plugin UUIDs
  uuid=$(defaults read $XCODE_PATH/Contents/Info DVTPlugInCompatibilityUUID)
  find "$PLUGINS_PATH" -name Info.plist -maxdepth 3 | xargs -I{} defaults write {} DVTPlugInCompatibilityUUIDs -array-add "$uuid"
  # Restore keybindings
  sudo cp -f $SOURCE_PATH/IDETextKeyBindingSet.plist $XCODE_PATH/Contents/Frameworks/IDEKit.framework/Versions/A/Resources
  # Restore white text-cursor
  sudo cp -f $SOURCE_PATH/DVTIbeamCursor.tiff $XCODE_PATH/Contents/SharedFrameworks/DVTKit.framework/Versions/A/Resources/

  restart "$1"
}

restart {
  osascript -e 'quit app "$1"'
  open -a "$1"
}

Xcode 8+ update

Apple hardened Xcode 8 and later against XcodeGhost and thus broke many plugins. If you value your plugins over safety, then you’ll have to re-sign Xcode:

sudo codesign -f -s "Some certificate e.g. 'iPhone Developer: Your Name (AAAAAAAA)'" /Applications/Xcode.app

Not ideal, but until Xcode supports more than mere text-extensions, I think the tradeoff is worth it.

How to use a git hook to avoid pushing Xcode’s mess onto others

A good boy scout “leaves the campground cleaner than he found it”. Or at least not leave the campground messier than he found it. In iOS this includes those ‘misplaced views’ warnings that Xcode sometimes unhelpfully causes when you merely peek at .xib files.

Future Xcode releases will hopefully fix this. But for now it’s still up to us to fix it. The following Git hook ensures all team members keep the campground clean for others:

#!/usr/bin/env bash
set -e

failed=0

misplaced_pattern='misplaced="YES"'
if git diff-index -p -M --cached HEAD -- '*.xib' '*.storyboard' | grep '^+' | egrep "$misplaced_pattern" >/dev/null 2>&1
then
  echo "PUSH REJECTED, you have misplaced views:" >&2
  echo "$(git --no-pager grep -l --cached "$misplaced_pattern" '*.xib' '*.storyboard')" >&2
  echo ""
  failed=1
fi

# Potentially more checks that raise the 'failed' flag.

exit $failed

How to scrape iOS URL schemas

Say you have to find out URL schemas of 3rd party iOS apps, many of them. First download these apps using the macOS iTunes. Right-click one of the downloaded apps and select Show in Finder.

iTunes Show .ipa in Finder

This will open the folder of .ipa files at ~/Music/iTunes/iTunes Media/Mobile Applications which you then copy to another folder of your choosing. In this folder you now run the following script:

# Usage: $0 <directory of .ipa files> <output file with (app-name URL-scheme) pairs>
for file in ./*; do
  dir="$(basename $file .ipa)";
  unzip -uqd "$dir" $file;
  app_name_path="$(find "$dir/Payload" -maxdepth 1 -name '*.app')"
  app_name="$(basename "$app_name_path")"
  plist=$(plutil -extract CFBundleURLTypes.0.CFBundleURLSchemes.0 xml1 -o - "$app_name_path/Info.plist")
  scheme=$(echo "$plist" | grep \<string\>)
  echo "$app_name $scheme" >> ../schemas
done

How to lessen compiler’s shortcoming

One weird Swift trick to not have to come up with silly and inconsistent names for the unwrapped weak self:

iMightOutliveSelf { [weak self] in
  guard let `self` = self else { return }
  self.doSomething()
}

Fellow developers might hate you for doing this, for it is a Swift compiler bug. Swift does allow backticks to use a otherwise reserved word in other cases though.

Update: This has been fixed in Swift 4.2 and now you can unwrap self as you would any other variable, without backticks:

iMightOutliveSelf { [weak self] in
  guard let self = self else { return }  self.doSomething()
}

Pre-recorded test data

There’s a Test Application Data setting for providing test data with which you can launch tests from a known state. Among other uses, having such known state is pretty much essential for any thorough testing of Core Data migrations.

First go to Window > Devices and download the data container from your device:

Download container from the device

Then open the Edit Scheme... screen and load the just download .xcappdata:

Edit schema

Note that from Xcode 6 to including Xcode 7.3 this might not be working on the iOS Simulator.