No, I swear, my iOS app doesn’t crash on startup! A simple lesson I learned as an enterprise architect about indie iOS game development

Although I’m focusing on enterprise software architecture (a.k.a server side development) by day, I still write Swift-based iOS apps on the side. Though the two software engineering disciplines can be quite different, they sometimes share more in common than I give them credit for.

The other day I submitted a minor update of my Word Wad iOS game to the App Store. The changes were mostly cosmetic: adding back an image that I’d discovered had gone missing, and introducing auto layout in the app’s storyboard. Other than that, I hadn’t made much in the way of code changes.

I tested my changes out on a number of simulators, as well as on my physical iPhone. Everything looked great, so I upped my app’s version number, created a build, validated the build, and ultimately submitted it. Expecting a quick win, I was surprised when, a few days later, I received a rejection from Apple stating, among other things, “Your app crashed when we launch the app”.

Word Wad screenshot
My app, Word Wad, in its non-crashed form.

Feeling a bit foolish for somehow submitting such a buggy app to Apple, I quickly launched Word Wad in a simulator. It ran fine. Figuring that maybe a fresh install would crash, I reset the simulator’s settings and tried again. No crash.

I tried a few different simulators, all with the same result.

Growing a bit concerned, I then connected my iPhone to my MacBook, deleted the previous copy of Word Wad, and launched Word Wad on my phone out of Xcode. Still no problems! I tried my iPad. Same thing.

So far, I couldn’t reproduce the crash. So how was I to fix it? As it turns out, Apple included a crashlog along with the rejection notice. So I opened it up to see how helpful it would be. (Letter Shuffle, you might guess from the crashlog, was Word Wad‘s original name).

{"app_name":"Letter Shuffle","timestamp":"2019-07-31 11:32:24.34 -0700","app_version":"1.3","slice_uuid":"b4a1df49-91b3-3d1d-a64f-a8d6bc44f649","adam_id":1219410678,"build_version":"6","bundleID":"com.taubler.lettershuffle","share_with_app_devs":false,"is_first_party":false,"bug_type":"109","os_version":"iPhone OS 12.4 (16G77)","incident_id":"DAB41DAD-9143-42B0-9115-7AB25B8FC81F","name":"Letter Shuffle"}
Incident Identifier: DAB41DAD-9A48-42B0-9115-7AB25B8FC81F
CrashReporter Key:   2b4341567d2bf6ee1c2ada8d72b26769dd6a74ff
Hardware Model:      xxx
Process:             Letter Shuffle [57833]
Path:                /private/var/containers/Bundle/Application/98F17500-4B54-4D89-8E2A-8B4F4A6B7D163/Letter Shuffle.app/Letter Shuffle
Identifier:          com.taubler.lettershuffle
Version:             6 (1.3)
AppStoreTools:       10G3
Code Type:           ARM-64 (Native)
Role:                Non UI
Parent Process:      launchd [1]
Coalition:           com.taubler.lettershuffle [5459]
Date/Time:           2019-07-31 11:32:24.3585 -0700
Launch Time:         2019-07-31 11:32:24.1648 -0700
OS Version:          iPhone OS 12.4 (16G77)
Baseband Version:    7.80.04
Report Version:      104
Exception Type:  EXC_BREAKPOINT (SIGTRAP)
Exception Codes: 0x0000000000000001, 0x000000010024f19c
Termination Signal: Trace/BPT trap: 5
Termination Reason: Namespace SIGNAL, Code 0x5
Terminating Process: exc handler [57833]
Triggered by Thread:  0
Thread 0 name:  Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0   Letter Shuffle                0x000000010024f19c 0x100228000 + 160156
1   Letter Shuffle                0x00000001002474e0 0x100228000 + 128224
2   libdispatch.dylib             0x00000001c64cc7d4 0x1c646c000 + 395220
3   libdispatch.dylib             0x00000001c646feb8 0x1c646c000 + 16056
4   libswiftCore.dylib            0x00000001f44b3e40 0x1f420d000 + 2780736
5   Letter Shuffle                0x0000000100245798 0x100228000 + 120728
6   Letter Shuffle                0x000000010024563c 0x100228000 + 120380
7   Letter Shuffle                0x00000001002453f4 0x100228000 + 119796
8   Letter Shuffle                0x0000000100245520 0x100228000 + 120096
9   UIKitCore                     0x00000001f2b23224 0x1f2810000 + 3224100
10  UIKitCore                     0x00000001f2b23628 0x1f2810000 + 3225128

<snip>

Binary Images:
0x100228000 - 0x10026bfff Letter Shuffle arm64  <a4b1de4970b3301da64458d6bc22f513> /var/containers/Bundle/Application/96F17500-2B52-4D89-8E2A-B4F4A6B7D76E/Letter Shuffle.app/Letter Shuffle
0x100688000 - 0x1006dffff dyld arm64  <06f3d9add3a233cea57df42b73686817> /usr/lib/dyld
0x100748000 - 0x100753fff libobjc-trampolines.dylib arm64  <065bd8006d513c358dc14e2a8ff1ba31> /usr/lib/libobjc-trampolines.dylib
0x1c5bf6000 - 0x1c5bf7fff libSystem.B.dylib arm64  <8a05d5f48f0a376abe6bd1caf4fc8138> /usr/lib/libSystem.B.dylib

<snip>

Completely unsymbolicated, and nearly useless as-is. So I went online to figure out how to make the logs more useful. I found this Apple tech note which discusses a command-line tool called atos. atos  helps symbolicate crashlogs, via one long terminal command.

So I opened up a text editor and got to work in crafting the command. The tech note provides the details, but based on my crashlog, I created the following:

atos -arch arm64 -o /Users/dave/Library/Developer/Xcode/Archives/2019-07-31/Letter\ Shuffle\ 7-31-19\,\ 10.32\ PM.xcarchive/dSYMs/Letter\ Shuffle.app.dSYM/Contents/Resources/DWARF/Letter\ Shuffle -l 0x100228000 0x000000010024f19c

To figure out the path to the executable, I went to Xcode and opened the Organizer (Window > Organizer). Under the Archives tab, I control-clicked the troublesome build and selected Show in Finder. Then I control-clicked the .xarchive file that appeared in the Finder and selected Open With > Terminal. A that point, simply entering the pwd command in the terminal got me the path to the file; I needed to add the escape characters myself, unfortunately.

Running the above atos command yielded this:

GameRound.init(number:letters:phrase:numRows:numCols:mixMoves:) (in Letter Shuffle) (<compiler-generated>:0)

Okay. So I had the offending function (GameRound.init) but not the exact location within that function. At that point, it was a matter of looking through the code and guessing where the problem could be. One aspect struck me in particular: an array of items is passed to the init method, along with a number of rows and columns used to turn the array into a matrix. However unlikely, it was conceivable that the array length wouldn’t match the number of rows times the number of columns, which would result in an array out of bounds exception. Figuring that could be the problem, I added some defensive code, and throughly tested my changes in a number of simulators and devices. Encountering no errors, I created a new build and submitted it.

Apple reviewed the build a few days later. And they encountered the exact same thing:“Your app crashed when we launch the app”.

sigh

At this point, I was flying blind. I had limited insight as to where the problem was occurring, with no insight as to what the problem actually was. Worse, try as I might, I was unable to reproduce the problem myself. I was worried that I’d have to use Apple’s review process to test out my blind attempts at solving the problem. Which, clearly, was not a sustainable approach.

The it occurred to me: rather than plugging my iPhone into my laptop and then launching an executable from Xcode, maybe I ought to run the build itself that I’d submitted to Apple on my device. So I did, via the following steps:

  1. Open up Xcode’s Organizer, as described above
  2. Under the Archives tab, select the troublesome build and click the Distribute App button
  3. Choose Ad Hoc as the option
  4. Follow the remaining prompts, ultimately saving the archive file locally
  5. Plug the iPhone into the laptop, then open the Xcode’s devices window (Window > Devices and Simulators)
  6. Select the iPhone in the left-hand pane.
  7. Drag the .ipa file from the Finder into the INSTALLED APPS section in the right-hand pane.

Doing that installed the specific build on my iPhone. I launched it, and… bam! The app crashed.

Woo hoo! I could now get my app to crash reliably.

So the problem seemed to be not with the code itself, but with the compilation process.

Now that I could get my app to crash on my own device, the next step was to look at the newly-generated crashlogs. Although I was able to access the new crashlog directly from my phone, it didn’t provide me with any more information as to the precise cause of the crash. However, I would be able to take some guesses at how to fix the problem, and test those fixes out locally before submitting the new builds to the App Store.

The crash still seemed to be occurring around the GameRound.init method, so I went back through that code. It turns out I was performing some superfluous operations that I really didn’t need to be doing, so I removed that code, streamlining it a bit. Then I created a new build, and distributed it locally as a new Ad Hoc archive. I installed the archive on my phone again, launched it, and… it crashed again.

But looking at the new crashlog, a different class—NullGameRound—was referenced at the top of the call stack. NullGameRound is an implementation of the “null object” design pattern, extending my app’s GameRound class and effectively overriding certain functions with no-op implementations.

Was there something about this pattern that the compiler didn’t like?

Although it’s a handy pattern, I was easily able to remove the use of NullGameRound in my game. So I did that, created and installed a new build on my phone, and ran it.

And it worked.

The moral of this story isn’t to avoid using the null object pattern in your Swift projects (although… really? Why should that have caused a problem?) It’s that sometimes, the Swift compiler will introduce problems into your distribution builds that won’t be present in your test builds.

For me, Swift and iOS development is something I do for fun, after hours. As I mentioned earlier, I work on enterprise services by day. With the advent of container technologies (e.g. Docker and Kubernetes) with which to deploy our backend services, it’s become a no-brainer that we test the same exact executable that will ultimately be deployed to production.

In hindsight it seems obvious, but the same should be true of mobile development, even for part-time indie developers like me. In retrospect, I was probably lucky that the compiler introduced a full-blown crash scenario that the Apple review team ran into, rather than a less apparent but more insidious bug that would’ve passed App Store approval, only to plague my app’s users later down the line.

I’d since submitted the fixed build to Apple, and the reviewed and approved it. But if I’d run the same build that I’d planned to submit to Apple before actually submitting it, I could’ve saved a lot of time and stress, and not inadvertently treated Apple’s reviewers as beta-testers of my app.