10
votes

I created a tester app to test adding a GameCenter leaderboard to a simple SwiftUI game I am creating. I have been unable to figure out how to display the Game Center leaderboard with all the scores.

I have created a class containing all the Game Center functions (authentication and adding score to the leaderboard. This is called from the main ContentView view. I can't figure out how to make it show the leaderboard (or even the gamecenter login screen if the player isn't already logged in.)

This is my GameCenterManager class:

class GameCenterManager {
        var gcEnabled = Bool() // Check if the user has Game Center enabled
        var gcDefaultLeaderBoard = String() // Check the default leaderboardID
        var score = 0
        let LEADERBOARD_ID = "grp.colorMatcherLeaderBoard_1" //Leaderboard ID from Itunes Connect

       // MARK: - AUTHENTICATE LOCAL PLAYER
       func authenticateLocalPlayer() {
        let localPlayer: GKLocalPlayer = GKLocalPlayer.local

           localPlayer.authenticateHandler = {(ViewController, error) -> Void in
               if((ViewController) != nil) {
                   print("User is not logged into game center")
               } else if (localPlayer.isAuthenticated) {
                   // 2. Player is already authenticated & logged in, load game center
                   self.gcEnabled = true

                   // Get the default leaderboard ID
                   localPlayer.loadDefaultLeaderboardIdentifier(completionHandler: { (leaderboardIdentifer, error) in
                    if error != nil { print(error ?? "error1")
                       } else { self.gcDefaultLeaderBoard = leaderboardIdentifer! }
                   })
                    print("Adding GameCenter user was a success")
               } else {
                   // 3. Game center is not enabled on the users device
                   self.gcEnabled = false
                   print("Local player could not be authenticated!")
                print(error ?? "error2")
               }
           }
       } //authenticateLocalPlayer()

        func submitScoreToGC(_ score: Int){
            let bestScoreInt = GKScore(leaderboardIdentifier: LEADERBOARD_ID)
            bestScoreInt.value = Int64(score)
            GKScore.report([bestScoreInt]) { (error) in
                if error != nil {
                    print(error!.localizedDescription)
                } else {
                    print("Best Score submitted to your Leaderboard!")
                }
            }
        }//submitScoreToGc()
    }

and here is the ContentView struct:

    struct ContentView: View {

        //GameCenter
        init() {
            self.gameCenter = GameCenterManager()
            self.gameCenter.authenticateLocalPlayer()
        }

        @State var score = 0
        var gcEnabled = Bool() //Checks if the user had enabled GameCenter
        var gcDefaultLeaderboard = String() //Checks the default leaderboard ID
        let gameCenter: GameCenterManager

        /*End GameCenter Variables */



        var body: some View {

            HStack {
                Text("Hello, world!")
                Button(action: {
                    self.score += 1
                    print("Score increased by 10. It is now \(self.score)")
                    self.gameCenter.submitScoreToGC(self.score)

                }) {
                    Text("Increase Score")

                }
            }
        }
    }

Would greatly appreciate any help in fixing the problem.

2
I have not gotten into this yet, but I suspect you need to start with looking at UIViewControllerRepresentable. Apple's tutorial documentation is here: developer.apple.com/tutorials/swiftui/interfacing-with-uikitbg2b

2 Answers

8
votes

I have a fix.

I use Game Center successfully in my SwiftUI App Sound Matcher. Code snippets to follow.

The code doesn't exactly follow the SwiftUI declarative philosophy but it works perfectly. I added snippets to SceneDelegate and ContentView plus used I used a GameKitHelper class similar to the one Thomas created for his test app. I based my version on code I found on raywenderlich.com.

I actually tried using a struct conforming to UIViewControllerRepresentable as my first attempt, following the same line of thought as bg2b, however it kept complaining that the game centre view controller needed to be presented modally. Eventually I gave up and tried my current more successful approach.

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    let contentView = ContentView()
        .environmentObject(GameKitHelper.sharedInstance) // publish enabled state

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, 
            options connectionOptions: UIScene.ConnectionOptions) {

    if let windowScene = scene as? UIWindowScene {
        let window = UIWindow(windowScene: windowScene)

        window.rootViewController = UIHostingController(rootView: contentView)
        // new code to create listeners for the messages
        // you will be sending later
        PopupControllerMessage.PresentAuthentication
             .addHandlerForNotification(
                 self, 
                 handler: #selector(SceneDelegate
                     .showAuthenticationViewController))

        PopupControllerMessage.GameCenter
            .addHandlerForNotification(
                self, 
                handler: #selector(SceneDelegate
                   .showGameCenterViewController))

        // now we are back to the standard template
        // generated when your project was created
        self.window = window
        window.makeKeyAndVisible()
    }
}
// pop's up the leaderboard and achievement screen
@objc func showGameCenterViewController() {
         if let gameCenterViewController =
             GameKitHelper.sharedInstance.gameCenterViewController {
                    self.window?.rootViewController?.present(
                         gameCenterViewController,
                         animated: true,
                         completion: nil)
         }

}
// pop's up the authentication screen
@objc func showAuthenticationViewController() {
    if let authenticationViewController =
        GameKitHelper.sharedInstance.authenticationViewController {

           self.window?.rootViewController?.present(
                authenticationViewController, animated: true)
                { GameKitHelper.sharedInstance.enabled  = 
                  GameKitHelper.sharedInstance.gameCenterEnabled }
    }
  }
}

// content you want your app to display goes here
struct ContentView: View {


@EnvironmentObject var gameCenter : GameKitHelper
@State private var isShowingGameCenter = false { didSet { 
                        PopupControllerMessage
                           .GameCenter
                           .postNotification() }}

var body: some View { 
    VStack {
        if self.gameCenter.enabled
             { 
            Button(action:{ self.isShowingGameCenter.toggle()})
                { Text(
                  "Press to show leaderboards and achievements")}
             } 
        // The authentication popup will appear when you first enter
        // the view            
        }.onAppear() {GameKitHelper.sharedInstance
                               .authenticateLocalPlayer()}
    }
}

import GameKit
import UIKit

// Messages sent using the Notification Center to trigger 
// Game Center's Popup screen
public enum PopupControllerMessage : String
{
 case PresentAuthentication = "PresentAuthenticationViewController"
 case GameCenter = "GameCenterViewController"
}

extension PopupControllerMessage
{
  public func postNotification() {
     NotificationCenter.default.post(
        name: Notification.Name(rawValue: self.rawValue),
        object: self)
  }

  public func addHandlerForNotification(_ observer: Any, 
                                        handler: Selector) {
     NotificationCenter.default .
          addObserver(observer, selector: handler, name:
            NSNotification.Name(rawValue: self.rawValue), object: nil)
  }

}

// based on code from raywenderlich.com
// helper class to make interacting with the Game Center easier

open class GameKitHelper: NSObject,  ObservableObject,  GKGameCenterControllerDelegate  {
    public var authenticationViewController: UIViewController?
    public var lastError: Error?


private static let _singleton = GameKitHelper()
public class var sharedInstance: GameKitHelper {
    return GameKitHelper._singleton
}

private override init() {
    super.init()
}
@Published public var enabled :Bool = false

public var  gameCenterEnabled : Bool { 
                     return GKLocalPlayer.local.isAuthenticated }

    public func authenticateLocalPlayer () {
        let localPlayer = GKLocalPlayer.local
        localPlayer.authenticateHandler = {(viewController, error) in

            self.lastError = error as NSError?
             self.enabled = GKLocalPlayer.local.isAuthenticated
            if viewController != nil {
                self.authenticationViewController = viewController                  
                PopupControllerMessage
                   .PresentAuthentication
                   .postNotification()
            }
        }
    }

    public var gameCenterViewController : GKGameCenterViewController? { get {

         guard gameCenterEnabled else {  
                  print("Local player is not authenticated")
                  return nil }

         let gameCenterViewController = GKGameCenterViewController()

         gameCenterViewController.gameCenterDelegate = self

         gameCenterViewController.viewState = .achievements

         return gameCenterViewController
        }}

    open func gameCenterViewControllerDidFinish(_ 
                gameCenterViewController: GKGameCenterViewController) {

        gameCenterViewController.dismiss(
                      animated: true, completion: nil)
    }

}
5
votes

Partial answer for you here. I'm able to download leaderboard scores and display them in a SwiftUI list provided the device (or simulator) is logged into iCloud and has GameCenter already enabled in settings. I have not attempted to make a gameCenter authentication view controller appear if that is not the case.

Thank you for the code in your question. I used your GameCenterManager() but put it in my AppDelegate:

let gameCenter = GameCenterManager()

Below is my ShowRankings.swift SwiftUI View. I'm able to successfully authenticate and get the scores. But I still have "anomalies". The first time I run this (in simulator) I get the expected "User is not logged into Game Center" error indicating the ViewController in your GameCenterManager is not nil (I never even attempt to display it). But then I'm able to successfully get the scores and display them in a list.

import SwiftUI
import GameKit

struct ShowRankings: View {
    let appDelegate = UIApplication.shared.delegate as! AppDelegate

    let leaderBoard = GKLeaderboard()
    @State var scores: [GKScore] = []
    var body: some View {
        VStack {
            Button(action: {
                self.updateLeader()
            }) {
                Text("Refresh leaderboard")
            }
            List(scores, id: \.self) { score in
                Text("\(score.player.alias) \(score.value)")
            }
        }.onAppear() {
            self.appDelegate.gameCenter.authenticateLocalPlayer()
            self.updateLeader()
        }
    }
    func updateLeader() {
        let leaderBoard: GKLeaderboard = GKLeaderboard()
        leaderBoard.identifier = "YOUR_LEADERBOARD_ID_HERE"
        leaderBoard.timeScope = .allTime
        leaderBoard.loadScores { (scores, error) in
            if let error = error {
                debugPrint("leaderboard loadScores error \(error)")
            } else {
                guard let scores = scores else { return }
                self.scores = scores
            }
        }
    }
}