I found this crash log in Firebase:
Crashed: com.apple.main-thread
0 libdispatch.dylib 0x4b20 dispatch_semaphore_signal + 8
1 GameCenterUI 0x9ebd8 __56-[GKNotificationBannerViewController hideBannerQuickly:]_block_invoke_2 + 40
2 libdispatch.dylib 0x3f88 _dispatch_client_callout + 20
3 libdispatch.dylib 0x7418 _dispatch_continuation_pop + 504
4 libdispatch.dylib 0x1aa58 _dispatch_source_invoke + 1588
5 libdispatch.dylib 0x12748 _dispatch_main_queue_drain + 756
6 libdispatch.dylib 0x12444 _dispatch_main_queue_callback_4CF + 44
7 CoreFoundation 0x9a6c8 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 16
8 CoreFoundation 0x7c02c __CFRunLoopRun + 2036
9 CoreFoundation 0x80eb0 CFRunLoopRunSpecific + 612
10 GraphicsServices 0x1368 GSEventRunModal + 164
11 UIKitCore 0x3a1668 -[UIApplication _run] + 888
12 UIKitCore 0x3a12cc UIApplicationMain + 340
13 libswiftUIKit.dylib 0x35308 UIApplicationMain(_:_:_:_:) + 104
14 BinMinesweeper 0x7050 main + 4345163856 (AppSceneDelegate.swift:4345163856)
15 ??? 0x1e6d6c960 (Missing)
The above example came from iOS 16.3.1, iPhone XR. I can’t reproduce this crash. It happens pretty rarely (less than 1% of users).
I assume GKNotificationBannerViewController
is the top banner "Welcome user_abc" presented when you launch the app.
The only time I interact with Game Center is when user click a leaderboard button, I show the game center VC. Here are some code:
import GameKit
public enum GameCenterUtil {
private static var g_isEnabled: Bool = false
public static func setupIfNeeded() {
GKLocalPlayer.local.authenticateHandler = { loginVC, error in
if loginVC != nil {
// Nothing. Do not present login.
// User generally don't use game center. It's annoying.
return
}
if error != nil {
g_isEnabled = false
return
}
g_isEnabled = true
}
}
public static func reportScore(_ score: Int, category: String) {
guard g_isEnabled else { return }
GKLeaderboard.submitScore(score, context: 0, player: GKLocalPlayer.local, leaderboardIDs: [category]) { error in
// nothing
}
}
public static func presentLeaderboard(in vc: UIViewController) {
// when disabled, this will be an alert.
let gameCenterVC = GKGameCenterViewController()
gameCenterVC.gameCenterDelegate = LeaderboardDismisser.shared
vc.present(gameCenterVC, animated: true, completion: nil)
}
}
final class LeaderboardDismisser: NSObject, GKGameCenterControllerDelegate {
static let shared = LeaderboardDismisser()
func gameCenterViewControllerDidFinish(_ gameCenterViewController: GKGameCenterViewController) {
// This is required. When user is not logged in, the alert prompts up. If we don't dismiss it, the game vc will be not responding.
gameCenterViewController.dismiss(animated: true, completion: nil)
}
}
Then in didFinishLaunching
, i call GameCenterUtil.setupIfNeeded()
, and in leaderboard button callback, I call GameCenterUtil.presentLeaderboard(in: vc)
.
Edit:
There is also a very similar crash, also unable to repro and happens rarely:
Crashed: com.apple.GameKit.banner
0 libdispatch.dylib 0x4a60 dispatch_semaphore_wait + 8
1 GameCenterUI 0x9abe4 __42+[GKNotificationBannerWindow enqueBanner:]_block_invoke_2 + 60
2 libdispatch.dylib 0x2320 _dispatch_call_block_and_release + 32
3 libdispatch.dylib 0x3eac _dispatch_client_callout + 20
4 libdispatch.dylib 0xb534 _dispatch_lane_serial_drain + 668
5 libdispatch.dylib 0xc0d8 _dispatch_lane_invoke + 436
6 libdispatch.dylib 0x16cdc _dispatch_workloop_worker_thread + 648
7 libsystem_pthread.dylib 0xddc _pthread_wqthread + 288
8 libsystem_pthread.dylib 0xb7c start_wqthread + 8
EDIT 2:
This is how I present/dismiss the game VC:
In button click, I simply call the present function:
let button = MySpriteButton(texture: GAME_TEXTURE(.leaderboardBig))
{
PLAY_APP_SOUND(.click)
GameCenterUtil.presentLeaderboard(in: vc)
}
Then I auto dismiss it when user enter background. This is a workaround for a bug earlier (refer to the comment in the code):
// MySceneDelegate.swift
open func sceneDidEnterBackground(_ scene: UIScene) {
// When we background the app with game center VC presented,
// Then a while later when we foreground the app, sometimes the game center VC becomes transparent for some reason,
// and covers the whole screen, making whole app not interactive
// This could be a bug in game center
// A naive solution is simply dismiss the game center VC when entering background.
if
let topVC = NAV_VC?.topViewController,
let gameCenterVC = topVC.presentedViewController as? GKGameCenterViewController
{
gameCenterVC.dismiss(animated: false, completion: nil)
}
}
Now I have a feeling that this workaround may be the reason. Because the doc (https://developer.apple.com/documentation/gamekit/gknotificationbanner) says:
If the game is in the foreground, the banner appears immediately. If the game is in the background, the banner appears when the game becomes active.
This sounds like a queue structure. And one of my crash log has GKNotificationBannerWindow enqueBanner:
. The 2 sound related.
I think I still want to keep this workaround, but not sure how it caused the crash. I have tried to delay the dismiss for 1 seconds in my latest release, but it doesn’t fix the issue:
// A helper function
func fetchTopGameCenterVC() -> GKGameCenterViewController? {
if
let topVC = NAV_VC?.topViewController,
let gameCenterVC = topVC.presentedViewController as? GKGameCenterViewController
{
return gameCenterVC
}
return nil
}
// MySceneDelegate.swift
open func sceneDidEnterBackground(_ scene: UIScene) {
if let vcBeforeDelay = fetchTopGameCenterVC() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
if let vcAfterDelay = fetchTopGameCenterVC(),
vcAfterDelay === vcBeforeDelay
{
vcAfterDelay.dismiss(animated: false, completion: nil)
}
}
}
}
2
Answers
Maybe the reason for these crashes is a deadlock: In both cases, the crashed thread tries to get a semaphore, but cannot get it before the watchdog cancels the app.
I don’t know
GameCenterUI
, but since it is UI related, maybe it can only be called on the main thread. However, your 2nd stack trace shows that it is called on another thread.So, please check if it is OK to call
GameCenterUI
on other threads.Based on the crash logs you have provided, it seems that this issue might be connected to the internal GameKit logic. Both crash logs mention the presence of
dispatch_semaphore
.As discussed in How to properly deallocate an object that contains a DispatchSemaphore, your workaround might break internal object lifecycle connected with
dispatch_semaphore
balancing.I suggest you to try the A/B Testing in this case.
Try to setup a user group with 10-15% of your users, disabling this auto-dismiss feature for them, based on results you can react correspondently.
Firebase A/B Tutorial
Kodeco (aka Raywenderlich) A/B Testing Tutorial