Fixing Simulator Status Bars for App Store Screenshots With Xcode 11 and iOS 13

Today Apple released the Xcode 11 GM release, meaning that it’s now time to create our builds using the iOS 13 SDK and submit to the App Store! If you’re like me and you use Fastlane’s Snapshot tool to automate the creation of your App Store screenshots using Xcode’s UI testing infrastructure, then you may have noticed something that broke with this new SDK: using SimulatorStatusMagic to clean up the simulator’s status bar. I use it on Landmarked, a side project I have at Detroit Labs, to make the App Store screenshots just a little nicer:

A screenshot of Landmarked

As you can tell, the status bar doesn’t say “Carrier,” the battery is full (important if you’re developing on a laptop), and the time has been set. In this case, since Landmarked is about Detroit, I set the time to 3:13 PM, but you may want to set it to 9:41 AM as per tradition. Doing this with SimulatorStatusMagic is easy:

#if targetEnvironment(simulator)
SDStatusBarManager.sharedInstance().bluetoothState = .hidden
SDStatusBarManager.sharedInstance().batteryDetailEnabled = false
SDStatusBarManager.sharedInstance().timeString = "3:13 PM"
SDStatusBarManager.sharedInstance().enableOverrides()
#endif

I use the #if targetEnvironment(simulator) bit to ensure that this doesn’t run if you’re running the UI tests on a real device, as it isn’t supported. Up until now, this has worked great. Of course, you should clean this up when you’re done—otherwise, these overrides persist across app launches on the running simulator. The above code runs in the test case’s setUp() method, and in the tearDown() method, you can clean up your work:

override func tearDown() {
    super.tearDown()
    #if targetEnvironment(simulator)
    SDStatusBarManager.sharedInstance().disableOverrides()
    #endif
}

So how do we do this on Xcode 11? With SimulatorStatusMagic not working, the first step is to avoid running it on iOS 13:

override func setUp() {
    #if targetEnvironment(simulator)
    if #available(iOS 13.0, *) {
        
    }
    else {
        SDStatusBarManager.sharedInstance().bluetoothState = .hidden
        SDStatusBarManager.sharedInstance().batteryDetailEnabled = false
        SDStatusBarManager.sharedInstance().timeString = "3:13 PM"
        SDStatusBarManager.sharedInstance().enableOverrides()
    }
    #endif
}
override func tearDown() {
    super.tearDown()

    #if targetEnvironment(simulator)
    if #available(iOS 13.0, *) {
        
    }
    else {
        SDStatusBarManager.sharedInstance().disableOverrides()
    }
    #endif
}

Looking at the release notes for Xcode 11, there’s a new simctl command to control the simulator status bar, aptly named status_bar. At the command line, to replicate the above behavior, we’d call it like this:

xcrun simctl status_bar booted override \
    --time "3:13 PM" \
    --dataNetwork wifi \
    --wifiMode active \
    --wifiBars 3 \
    --cellularMode notSupported \
    --batteryState discharging \
    --batteryLevel 100

But if you want to run this automatically, where to put it? You can’t call out to Process on iOS, but you need to call something on your Mac to run this command. Fortunately, Xcode has a way to do this by adding a pre-action to your test scheme:

A screenshot of Xcode editing a scheme’s pre-run actions

And of course, just as before, you can use a post-run action with the clear command to reset everything:

A screenshot of Xcode editing a scheme’s post-run actions

It works! Huzzah! Of course, there are still some things we can do better. If you run these UI tests on a device, and you have a simulator open, then that simulator will get these overrides while it’s running thanks to the booted parameter passed to simctl. Also, this approach doesn’t work when automating with Fastlane. To avoid that, we can use the TARGET_DEVICE_IDENTIFIER parameter to specifically boot and configure the device that the tests will run on (if the device is not booted, then simctl will fail to set the overrides). In your pre-action, make sure you’re inheriting build settings from the UI testing target, then add this snippet:

xcrun simctl boot "${TARGET_DEVICE_IDENTIFIER}"

xcrun simctl status_bar "${TARGET_DEVICE_IDENTIFIER}" override \
    --time "3:13 PM" \
    --dataNetwork wifi \
    --wifiMode active \
    --wifiBars 3 \
    --cellularMode notSupported \
    --batteryState discharging \
    --batteryLevel 100

With that, we’re successfully configuring our status bar on the simulator when it runs. Of course, the value of TARGET_DEVICE_IDENTIFIER won’t make sense to simctl if the destination is a device. Let’s add a quick check that we’re running on a simulator, and if not, exit early:

if [[ "${SDKROOT}" != *"simulator"* ]]; then
    exit 0
fi

Perfect! The value of SDKROOT is something like iphonesimulator13.0 when running on a simulator and iphoneos13 when running on a device, so this if statement will call exit when the SDK isn’t a simulator. Next up, we want to avoid running this code when the destination isn’t running at least iOS 13, so to do that, we’ll need to check the target device’s iOS version. Luckily for me, that code was readily available on StackExchange. The final version of our script looks like this:

function version {
    echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }';
}

# Don’t run on iOS devices.
if [[ "${SDKROOT}" != *"simulator"* ]]; then
    exit 0
fi

# Don’t run on iOS versions before 13.
if [ $(version "${TARGET_DEVICE_OS_VERSION}") -ge $(version "13") ]; then
    xcrun simctl boot "${TARGET_DEVICE_IDENTIFIER}"

    xcrun simctl status_bar "${TARGET_DEVICE_IDENTIFIER}" override \
        --time "3:13 PM" \
        --dataNetwork wifi \
        --wifiMode active \
        --wifiBars 3 \
        --cellularMode notSupported \
        --batteryState discharging \
        --batteryLevel 100
fi

Now, if you run your UI tests on an iOS simulator target running anything earlier than iOS 13, the pre-run action will exit early, and SimulatorStatusMagic will take care of it. This is important for our purposes as Landmarked still supports iOS 9 and the iPhone 4S’s 3.5″ screen, so we need to generate a screenshot for that size to be complete.

With this, our pre-run script is complete! The post-run is much simpler:

function version {
    echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }';
}

# Don’t run on iOS devices.
if [[ "${SDKROOT}" != *"simulator"* ]]; then
    exit 0
fi

# Don’t run on iOS versions before 13.
if [ $(version "${TARGET_DEVICE_OS_VERSION}") -ge $(version "13") ]; then
    xcrun simctl boot "${TARGET_DEVICE_IDENTIFIER}"
    xcrun simctl status_bar "${TARGET_DEVICE_IDENTIFIER}" clear
fi

I was able to use the above code to generate all of the screenshots for the iOS 13 version of Landmarked. Keep an eye out for version 1.1.3 as soon as iOS 13 is released! Here’s what it looks like on the new iPhone 11 Pro Max:

A screenshot of Landmarked on an iPhone 11 Pro Max.

Slides and Sample Code from my 360|iDev Talks

I’ve had the pleasure of speaking at this year’s 360|iDev conference, which so far has been an amazing conference. Here are my slides and sample code:

Developing for Apple Watch With Swift:
Slides: https://speakerdeck.com/slaunchaman/do-i-need-an-apple-watch-app-360-idev-2017
Sample Code: https://github.com/SlaunchaMan/Timeato

Advanced Dates and Times in Swift:
Slides: https://speakerdeck.com/slaunchaman/advanced-dates-and-times-in-swift-360-idev-2017
Sample Code (Swift Playgrounds): https://gist.github.com/SlaunchaMan/c466ca7909c4688fd8b28421d72b78ab
Sample Code (iOS App): https://github.com/SlaunchaMan/DropItLikeItsClock

Enjoy!

Running Real Tests on watchOS

Since the first release of watchOS, it’s been a unique platform among Apple’s in that Xcode doesn’t support running unit test targets for it. This is of course a hindrance to writing maintainable code for the watch; with no tests, there’s a lot of manual testing involved. It’s hard to even debug some aspects of watch code—since the debugger requires the watch to be connected to the iPhone, which is in turn connected to the Mac via USB, you can’t debug a watch app’s behavior when it’s disconnected from the phone.

There have been efforts to allow limited testing of WatchKit code, but so far everything I’ve seen has one crucial flaw: the tests are actually running on iOS, either by testing a shared framework or by simulating WatchKit on iOS. This is all well and good, and certainly better than nothing, but it doesn’t allow you to test any platform-specific code. Inspired by other efforts to make cross-platform frameworks well-tested, I wondered what it would take to run actual tests on watchOS.

A Tale of Two XCTests

Much like PivotalCoreKit re-implements some of WatchKit to let an iOS target use it, my first thought was to re-implement some of XCTest to get existing test code to build under watchOS. As I dug through XCTest, however, I realized that it’s actually a pretty complex framework, and a complete reimplementation doesn’t make sense when Apple has already begun the task and has a perfectly good open-source repository just sitting there waiting to be used.

To use Apple’s XCTest reimplementation, I first had to create a podspec file to allow CocoaPods to set up my Xcode project. It looks like so:

Pod::Spec.new do |s|
  s.name         = "XCTest"
  s.version      = "3.0.1"
  s.summary      = "A watchOS compilation of Apple’s open-source XCTest."

  s.description  = <<-DESC
            A watchOS compilation of Apple’s open-source XCTest.
                   DESC

  s.homepage     = "https://github.com/apple/swift-corelibs-xctest"
  s.license      = "Apache License, Version 2.0"
  s.author    = "Apple"

  s.watchos.deployment_target = "3.0"

  s.source       = { :git => "https://github.com/apple/swift-corelibs-xctest.git", :tag => "swift-" + s.version.to_s + "-RELEASE" }

  s.source_files  = "Sources/**/*.swift"

  s.framework  = "Foundation"

  s.prepare_command = <<-CMD
                        find Sources/ -type f -name "*.swift" | xargs sed -e 's/import SwiftFoundation/import Foundation/g' -i ""
                        sed -i "" -e 's/usingBlock:/using:/' Sources/XCTest/Public/XCTestCase+Asynchronous.swift
                      CMD
end

While this is a fairly straightforward podspec, I did two interesting things in its prepare_command to get it to work:

  • Because the version I’m targeting uses the also-open-source SwiftFoundation instead of just Foundation, I use sed to change the import statement to point at regular Foundation. This has since been fixed, so this part will become unnecessary.
  • I used sed again to fix a method that had been renamed since this tag was created.

The prepare_command part of a podspec is often-overlooked as a way to fix up a pod without making a fork of your own to maintain.

Now that I had a version of XCTest that would build for watchOS, I set up a new target in my Xcode project called “WatchTests Test Runner WatchKit App” (with a corresponding WatchKit Extension target). This target does what it says on the tin: run it to run the tests. The Xcode project also has an iOS unit test target, and the goal is to share those tests with watchOS, so I simply linked the test files with the new WatchKit extension. When those files use import XCTest, they’ll be pulling from the Swift version automatically.

Running The Tests

To run the tests, you call XCTMain() with an array of XCTestCaseEntry objects that represent the test classes you’d like to test. As of right now, however, it’s still a goal of the Swift XCTest project to enable test method discovery without the Objective-C runtime, which means we need to do it. The easiest way is for each test class to implement a property called allTests, wherein you manually enumerate test methods. The final Swift test class might look something like this:

import XCTest

class WatchTestsTests: XCTestCase {
    
    #if os(watchOS)
    static var allTests = {
        return [
            ("testPassingTest", testPassingTest),
        ]
    }()
    #endif
    
    func testPassingTest() {
        XCTAssertTrue(true)
    }
    
}

This test can be compiled on iOS and watchOS. The extension delegate of the test runner WatchKit App then runs the tests:

import WatchKit
import XCTest

class ExtensionDelegate: NSObject, WKExtensionDelegate {

    func applicationDidFinishLaunching() {
        XCTMain([testCase(WatchTestsTests.allTests)])
    }
    
}

XCTMain will call exit with an exit status, so you can see the results in Xcode’s console:

Test Suite 'All tests' started at 22:36:14.852
Test Suite 'WatchTestsTestRunner WatchKit Extension.appex.xctest' started at 22:36:14.854
Test Suite 'WatchTestsTests' started at 22:36:14.854
Test Case 'WatchTestsTests.testPassingTest' started at 22:36:14.854
Test Case 'WatchTestsTests.testPassingTest' passed (0.001 seconds).
Test Suite 'WatchTestsTests' passed at 22:36:14.856
     Executed 1 test, with 0 failures (0 unexpected) in 0.001 (0.001) seconds
Test Suite 'WatchTestsTestRunner WatchKit Extension.appex.xctest' passed at 22:36:14.856
     Executed 1 test, with 0 failures (0 unexpected) in 0.001 (0.001) seconds
Test Suite 'All tests' passed at 22:36:14.856
     Executed 1 test, with 0 failures (0 unexpected) in 0.001 (0.001) seconds
Program ended with exit code: 0

As you can see, this is exactly what you’d get out of XCTest on iOS, except it’s running in a watchOS target!

Limitations

This trivial example works well, but there are some limitations with this approach:

  • Because I’m using the Swift implementation of XCTest, the only existing tests that could possibly run are those written in Swift. You have to subclass XCTestCase to use XCTest, and you can’t make an Objective-C subclass of a Swift class, so for now at least, no Objective-C tests can run on watchOS. If Swift and Objective-C interoperability improves to the point where you can subclass a Swift class in Objective-C, look for this to improve. Alternatively, XCTestCase could be a protocol exposed to Objective-C, which would allow more flexibility in that regard.
  • Since we’re just running the tests directly, we don’t get any fancy Xcode integration. Failed tests won’t color a line red, nor will they run when you press ⌘U.
  • Our XCTestCase subclasses are in a unique position: they are subclasses of an Objective-C class on iOS, and subclasses of a Swift class on watchOS. This can confuse the compiler and leave you without code completion.

Nevertheless, I’m excited to start tinkering with tests on watchOS. I have a couple areas I’d like to explore:

  • Integrating these tests with CI of some sort. If I can get a shell script to return the watch target’s exit status, then I can get a Jenkins or Travis build to fail if the watch tests fail.
  • Testing the watch UI further. Right now the shared WKExtension’s rootInterfaceController property is nil during testing. Waiting for the UI to load before calling XCTMain() will probably help here.

I’d love to hear suggestions or feedback on this. The more tests are written for watchOS, the better and more maintainable our watch apps will be! If you want to poke around with my code, you can find it on GitHub.