Crash on launch, while in review - but no crash in Xcode or TestFlight

Hi all!

My new iOS app (first one in Swift and SpriteKit, was using ObjectiveC and Cocos2D before, a few years back) - got rejected with the dreaded "We were unable to review your app as it crashed on launch.",

That's especially bad news when it never ever crashed on any on our devices when testing (variety of phones, iPads and all sort of iOS) either in debug or release, before submission.

So no luck repro-ing!

The crashlog text file apple review team sent back, renamed as .crash, seems to open in Xcode (right click on it) when linked to the right project and seems to point in the leaderboard code fetching the local authenticated user's score. This is some code that I shamelessly copy/pasted from a StackOverflow page a few months ago, and seemed to work right away so moved-on back then, but something must be unsafe in there (sorry, bit of a rookie with Swift optionals etc).

Any help would be much appreciated as this code seems to work fine in debug, without warnings or anything, and also works fine in release using TestFlight - so we simply cannot repro the apple QA issue, and we need to fix it blind!

This is the function below that is crashing, right on the line that has the "{ player, _, _ in ":

func sync_hiscore()
{
    if (GKLocalPlayer.local.isAuthenticated)
    {
        if #available(iOS 14.0, *)
        {
            GKLeaderboard.loadLeaderboards( IDs: ["HatStacker_LD01"] ) 
            { leaderboards, _ in  leaderboards?[0].loadEntries(  for: [GKLocalPlayer.local], timeScope: .allTime )     
                { player, _, _ in // <<<<  !!! CRASHES HERE!!

                    if ( player!.score > player_hiscore)
                    {
                        player_hiscore = player!.score
                    }
                }
            }
        }
    }
}

I've also attached the full crashlog if that's of any help!

Many thanks all, sorry for such a Swift rookie question, but to add to the confusion this is also the first time one of our apps gets rejected for such an obvious bad crash at launch, that no one has seen during our weeks of testing!

Any idea why it's only in release and only in the apple HQ/review env that we get that error? Could they have an invalid Game Center account that is confusing the "player" score fetching?

Cheers,

JBB

Replies

The crash report shows this:

Thread 0 name:   Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0   HatStacker           … 0x104bac000 + 76248
1   HatStacker           … 0x104bac000 + 76384
2   GameCenterFoundation … __74-[GKLeaderboard loadEntriesForPlayers:timeScope:locale:completionHandler:]_block_invoke_3 + 196
3   GameCenterFoundation … __39-[GKDispatchGroup notifyOnQueue:block:]_block_invoke.38 + 36

Note how frames 0 and 1 are not symbolicated. If you symbolicate the crash report, it should give you a line number, which would be a good place to start your investigation. See Adding identifiable symbol names to a crash report for more on this.

As to what actually trigger the crash, this:

Exception Type:  EXC_BREAKPOINT (SIGTRAP)

means it’s likely that you hit a Swift trap, that is, Swift detected a problem and deliberately crashed your process. Looking at the code snippet you posted, the most obvious cause of that crash is that you’re ignoring the errors from both loadLeaderboards(IDs:completionHandler:) and loadEntries(for:timeScope:range:completionHandler:). Specifically, if the latter fails then player will be nil and the force unwrap in this code will trap player!.score.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Many thanks Quinn!

I symbolicated the crash report and indeed it takes me to that same function, so this must be it. Here is the extra detail:

Triggered by Thread:  0
Thread 0 name:   Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0   HatStacker                    	       0x104bbe9d8 closure #1 in closure #1 in GameScene.sync_hiscore() (in HatStacker) (GameScene.swift:1882) + 76248
1   HatStacker                    	       0x104bbea60 thunk for @escaping @callee_guaranteed (@guaranteed GKLeaderboardEntry?, @guaranteed [GKLeaderboardEntry]?, @guaranteed Error?) -> () (in HatStacker) (<compiler-generated>:0) + 76384

I found a safer sample of code doing the same thing, could you confirm it should do the trick in a safer manner?

(it functions just as well, my hiscore updates if out of sync like if I reinstalled the game/deleted all save files)

func sync_hiscore2()
{
     if (GKLocalPlayer.local.isAuthenticated)
     {
          if #available(iOS 14.0, *)
          {
               GKLeaderboard.loadLeaderboards(IDs: ["HatStacker_LD01"])
               { leaderBoards, error in
                    guard let leaderBoard = leaderBoards?.first else { return }
                    leaderBoard.loadEntries(for: [GKLocalPlayer.local], timeScope: .allTime) 
                    { [weak self] leaderboard, entries, error in
                        guard let self else { return }
                        let hiscore = leaderboard?.score ?? 0
                        if ( hiscore > player_hiscore)
                        {
                            player_hiscore = hiscore
                        }
                   }
             }
       }
 }

One thing that is puzzling me...

That "high score sync" function is only called when the user press "Start game" in the main menu, so isn't in theory reached at "app launch" time.

The apple review team claims they can't even launch the game successfully so I assume weren't reaching the menu and pressing on anything.

Does the Swift trapping scans the whole code and triggers anyway? Unless they simply can't be bothered to give me those details on repro steps and it crashes on "game start" and not "on launch" as they suggest in their generic statement.

Any reason why it only happens at app review time, and never when I try on my devices at home? (even using the very same release binary through TestFlight) / Maybe they have a special setup with a Game Center account that is failing in a special way and triggering the swift trap? But I tried to launch on my devices without being logged in, and it never crashed, even using the very same release build with TestFlight.

My worry is I "blind" fix that one, and resubmit, then my code trigger another trap in another function further down in my code with every submission I make, as I get no warning from Xcode that my code is unsafe, and it doesn't crash my devices at home!? I fear I could alienate the apple review team if I keep using them to crash things one swift trap at a time!

Many thanks again for taking the time to help me with my "swift rookie" questions.

Cheers,

JBB

Does the Swift trapping scans the whole code and triggers anyway?

No. To trap like this you have to run the code. The backtrace in your crash report shows how the code ended up running. Consider this:

Thread 0 name:   Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0   HatStacker           … 0x104bac000 + 76248
1   HatStacker           … 0x104bac000 + 76384
2   GameCenterFoundation … __74-[GKLeaderboard loadEntriesForPlayers:timeScope:locale:completionHandler:]_block…
3   GameCenterFoundation … __39-[GKDispatchGroup notifyOnQueue:block:]_block_invoke.38 + 36
4   libdispatch.dylib    … _dispatch_call_block_and_release + 32
5   libdispatch.dylib    … _dispatch_client_callout + 20
6   libdispatch.dylib    … _dispatch_main_queue_drain + 984
7   libdispatch.dylib    … _dispatch_main_queue_callback_4CF + 44
8   CoreFoundation       … __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 16
9   CoreFoundation       … __CFRunLoopRun + 1996
10  CoreFoundation       … CFRunLoopRunSpecific + 608
11  GraphicsServices     … GSEventRunModal + 164
12  UIKitCore            … -[UIApplication _run] + 888
13  UIKitCore            … UIApplicationMain + 340
14  UIKitCore            … 0x191c85000 + 4547536
15  HatStacker           … 0x104bac000 + 221520
16  dyld                 … start + 2240

Frame 16 is the the system starting up your app. That calls your app startup code in frames 15, which then calls UIApplicationMain to start up your app. Frames 15 through 9 are all UIKit getting to its main event loop. Frame 8 shows that this event loop is servicing the main Dispatch queue. Frames 7 through 4 are standard Dispatch stuff. Frame 3 through 2 are GameKit calling the completion handler. You can tell that because of the symbol in frame 2:

__74-[GKLeaderboard loadEntriesForPlayers:timeScope:locale:completionHandler:]_block_invoke_3

The _block_invoke suffix indicates that this is a block [1] within the -[GKLeaderboard loadEntriesForPlayers:timeScope:locale:completionHandler:] method. The _3 suffix indicates that this is the third [2] block within that routine.

So, something has called -[GKLeaderboard loadEntriesForPlayers:timeScope:locale:completionHandler:] and then, later on, a GK completion handler has fired and that’s called a block within -[GKLeaderboard loadEntriesForPlayers:timeScope:locale:completionHandler:] which has then call your completion handler.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] A block is, roughly speaking, the Objective-C equivalent of a Swift closure.

[2] Or maybe the fourth; counting is hard (-:

Very odd. As far as I can see the only piece of code in the whole of my GameScene.swift - or even the whole project - calling loadEntries (to fetch hiscores) is my sync_hiscore() function. How is it getting called right after the GameKit inits? In theory my sync_hiscore() function is only called when we're well into my game main loop, navigating the main menu, so it will be a few seconds before the user presses the start button. The "app start" frame 16 would be long gone, no?

I added a "fatal error()" in my sync_hiscore2() function to see if it was somehow called at launch time and crash on boot. But no, nothing. But then if I try the press "start game" it does crash right away as it reaches that "fatal error" line, but the crash report looks nothing like the one apple sent, as it is clearly showing it went into my function after I press start etc.

Thread 0 name:   Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0   libswiftCore.dylib            	       0x18166d534 _assertionFailure(_:_:file:line:flags:) + 576
1   HatStacker                    	       0x1028e46cc GameScene.sync_hiscore2() + 224
2   HatStacker                    	       0x1028d66e4 GameScene.start_game() + 1420
3   HatStacker                    	       0x1028b5aa0 GameScene.menu() + 5640
4   HatStacker                    	       0x1028b1d84 GameScene.update(_:) + 308

This confirms the fact the apple review team isn't crashing that way, pressing the button, which syncs the score, calls my LoadEntries with the dodgy completion handler. They are crashing earlier, in a different manner.

So, correct me if I'm wrong here: they seem to be indeed crashing on very early on launch (app start is still within 16 frames) and somehow as the various systems kick in, Swift traps some bad code in my leaderboard completion handler, situated inside a function that is never called, but related to a similar LoadEntries function called by something else?

Sorry for being thick here! I am very confused.

Do I fix my function with a safer completion handler anyway? It cannot hurt I guess, as that way if it gets called for any reason, it will survive?!

I've attached the original symbolicated crash report, in case it can shed some extra light on why the loadEntriesForPlayers gets called, then the completion handler in my function gets called right after?

Sorry for all these questions... Many thanks for you help Quinn, much appreciated!

Cheers,

JBB

BTW, I did blind fix my unsafe completion handlers in the leaderboard code, and resubmitted - and it passed this time!

So it was that. Still puzzled as to why that piece of code normally only called when you pressed "start game" was being called at app launch? Maybe completion handlers are setup and called/scanned/trapped early on in some cases?

But at least I am unstuck. Many thanks Quinn for your help earlier!

Cheers,

JBB