0

I have a SwiftUI app that shows a GKGameCenterViewController. I am hitting a problem which is specific to iPads running iPadOS 16.1 and above in light mode only. Under these circumstances, the child views of GKGameCenterViewController appear as black text on a dark background, which is very difficult to read - see screenshot for the leaderboard "Test" below.

Game Center blackout

The app runs with versions of iOS/iPadOS 14.0 and above. The problem is very specific to iPads running iPadOS 16.1 and above in light mode. The top-level parent view of GKGameCenterViewController (showing the title "Game Center" and a navigation menu) works fine, only the child views are affected. The problem is not seen on iPhones, nor on iPads running versions of iPadOS 14 or 15. The problem is also not seen on iPads running iPadOS 16.1 and above if dark mode is in operation when GKGameCenterViewController is first shown.

The GKGameCenterViewController is being presented using a UIViewControllerRepresentable as a layer in a ZStack. From the examples I could find, using UIViewControllerRepresentable seems to be the standard way to do it, as also described in How to display Game Center leaderboard with SwiftUI.

It is interesting to note that the problem is not seen when the Game Center dashboard is shown by tapping the Game Center access point. I assume that the access point shows the GKGameCenterViewController in a different way.

The following stripped-down code illustrates how the GKGameCenterViewController is being shown. This is a standalone app that can be used to reproduce the problem. However, for Game Center authentication to work and for the overview of leaderboards to show, the following needs to be done too:

  • in Xcode, Game Center needs to be added as a capability to the app target
  • the app needs to be added to App Store Connect
  • Game Center needs to be enabled for the app version in App Store Connect
  • a leaderboard needs to be added.
import SwiftUI
import GameKit

@main
struct GameCenterBlackoutApp: App {
    var body: some Scene {
        WindowGroup {
            MyContentView()
        }
    }
}

struct MyContentView: View {

    @State private var showingGameCenter = false

    var body: some View {
        ZStack {
            Button("Show Game Center leaderboards") {
                showingGameCenter = true
            }
            if showingGameCenter {
                MyGameCenterView(showingGameCenter: $showingGameCenter)
                    .ignoresSafeArea()
            }
        }
        .onAppear {

            // Authenticate the local player
            GKLocalPlayer.local.authenticateHandler = handleAuthenticationOutcome
        }
    }

    private func handleAuthenticationOutcome(vc: UIViewController?, error: Error?) {
        if let error {
            #if DEBUG
            print("Failed to authenticate player: \(error)")
            #endif
        }
        // Prepare and show the GameCenter access point.
        // If authentication failed then the access point takes
        // the form of a button to sign in
        GKAccessPoint.shared.location = .topTrailing
        GKAccessPoint.shared.showHighlights = false
        GKAccessPoint.shared.isActive = true
    }
}

/// A Bridge between the Game Center view controller and its wrapper
final class MyCoordinator : NSObject, GKGameCenterControllerDelegate {

    @Binding private var showingGameCenter: Bool

    init(showingGameCenter: Binding<Bool>) {
        self._showingGameCenter = showingGameCenter
    }

    func gameCenterViewControllerDidFinish(
        _ gameCenterViewController: GKGameCenterViewController
    ) {
        gameCenterViewController.dismiss(animated:false)
        showingGameCenter = false
    }
}

/// A wrapper for GKGameCenterViewController
struct MyGameCenterView: UIViewControllerRepresentable {
    typealias Coordinator = MyCoordinator

    /// Binding to the state variable that controls the visibility of the Game Center layer
    @Binding private var showingGameCenter: Bool

    init(showingGameCenter: Binding<Bool>) {
        self._showingGameCenter = showingGameCenter
    }

    /// Factory function for the Bridge between the GKGameCenterViewController and this wrapper view
    func makeCoordinator() -> Coordinator {
        MyCoordinator(showingGameCenter: $showingGameCenter)
    }

    /// Creates the GKGameCenterViewController
    func makeUIViewController(
        context: UIViewControllerRepresentableContext<MyGameCenterView>
    ) -> GKGameCenterViewController {
        let result = GKGameCenterViewController(state: .leaderboards)
        result.gameCenterDelegate = context.coordinator
        return result
    }

    /// Stub implementation of protocol method
    func updateUIViewController(
        _ gameCenterViewController: GKGameCenterViewController,
        context: UIViewControllerRepresentableContext<MyGameCenterView>
    ) {
        // NOP
    }
}

I would be really grateful for any workaround that will resolve the black text issue as described and allow the child views of GKGameCenterViewController to be shown in a normal, readable foreground color. I have tried all of the following, none of which made any difference:

  • setting .dark or .light as .overrideUserInterfaceStyle on the view controller
  • applying .environment(\.colorScheme, .dark) to the UIViewControllerRepresentable or to a higher-level parent
  • applying .dark or .white as .preferredColorScheme on the UIViewControllerRepresentable
  • using modifier .toolbarColorScheme(all combinations of parameters) on the UIViewControllerRepresentable
  • applying Color.white as .foregroundColor on the UIViewControllerRepresentable
  • using black or white background behind the UIViewControllerRepresentable
  • showing the UIViewControllerRepresentable as an overlay instead of as a ZStack layer
  • not enabling the Game Center access point
  • using other GKGameCenterViewControllerState values in the init call
  • using other init functions for GKGameCenterViewController
  • using deprecated init functions and setters on GKGameCenterViewController.

BTW, I also reported the issue to Apple, but they have not been able to help.

Benzy Neez
  • 1,546
  • 2
  • 3
  • 10
  • Here is an update, in case anyone else is interested. The problem still exists with iPadOS 16.4 running a build created with Xcode 14.3. I have not been able to find any workaround. For my productive app I have resorted to providing a replacement leaderboard view that fetches the leaderboard entries using ```GKLeaderboard``` functions (see [Retrieve all scores in a GKLeaderboard](https://stackoverflow.com/a/75565400)). I give players the option of reverting to the standard Game Center view, if they want it. – Benzy Neez Mar 31 '23 at 09:19
  • Another update: I have tested using iPadOS 17.0 Public Beta and it seems that the problem is fixed in this version. So although no workaround was ever found, a real solution is now close. – Benzy Neez Jul 31 '23 at 19:24

0 Answers0