How to display Game Center leaderboard with SwiftUI
Asked Answered
A

3

13

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.

Asshur answered 6/11, 2019 at 2:5 Comment(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-uikitForspent
You can still get UIViewController used internally by swiftui anywhere in you code by using this method: let mainVC = UIApplication.shared.windows.first?.rootViewController and after that just present GKGameCenterViewController as usual from that mainVC.Withhold
A
10

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.

For SwiftUI 1.0 and iOS 13

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)
    }
   
}

Update: For SwiftUI 2.0 and iOS 14 the code is lot easier

import GameKit

enum Authenticate
{
   static func user() {
       let localPlayer = GKLocalPlayer.local
       localPlayer.authenticateHandler = { _, error in
           guard error == nil else {
               print(error?.localizedDescription ?? "")
               return
           }
           GKAccessPoint.shared.location = .topLeading
           GKAccessPoint.shared.isActive =      
                            localPlayer.isAuthenticated
       }
   }
}

import SwiftUI

// content you want your app to display goes here
struct ContentView: View { 
     
var body: some View { 
      Text( "Start Game") 
        // The authentication popup will appear when you first enter
        // the view            
        }.onAppear() { Authenticate.user()}
    }
}
Aubine answered 22/11, 2019 at 14:29 Comment(4)
Thanks for our help. I tried your way of doing it. However I could not for the life of me get the button to show up in ContentView. I initialized the gameKitHelper and everything and made it so it authenticated the player but it still would not show. Do you have any ideas?Asshur
I updated the code to publish the enabled state through a @EnvironmentObject otherwise the button won't appear until the view is refreshed and I removed anything that might do that to simplify the code. Sorry my bad.Aubine
Now that the enabled state is published. Shortly after the view appears the the authorisation dialog should popup and after you sign in an alert saying Welcome Back! should popup. Alternatively if you have already signed in you should get Welcome Back popup straight away. If you don't, go to Settings & check you have enabled Game Center. Sorry if this is teaching u to suck eggs, but the readers skills will vary.Aubine
The button show appear straight after the Welcome Back notification.Aubine
G
5

EDIT 2023: as mentioned in comments, GKScore is now deprecated. I don't have an updated solution to present.

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
            }
        }
    }
}
Glean answered 20/11, 2019 at 3:39 Comment(1)
'GKScore' was deprecated in iOS 14.0: Replaced by GKLeaderboardScoreTimbering
R
2

An alternative solution is to create a UIViewControllerRepresentable for GameCenter which takes a leaderboard ID to open. This makes it simple to open a specific leader board.

public struct GameCenterView: UIViewControllerRepresentable {
    let viewController: GKGameCenterViewController
    
    public init(leaderboardID : String?) {
        
        if leaderboardID != nil {
            self.viewController = GKGameCenterViewController(leaderboardID: leaderboardID!, playerScope: GKLeaderboard.PlayerScope.global, timeScope: GKLeaderboard.TimeScope.allTime)
        }
        else{
            self.viewController = GKGameCenterViewController(state: GKGameCenterViewControllerState.leaderboards)
        }
        
    }
    
    public func makeUIViewController(context: Context) -> GKGameCenterViewController {
        let gkVC = viewController
        gkVC.gameCenterDelegate = context.coordinator
        return gkVC
    }
    
    public func updateUIViewController(_ uiViewController: GKGameCenterViewController, context: Context) {
        return
    }
    
    public func makeCoordinator() -> GKCoordinator {
        return GKCoordinator(self)
    }
}

public class GKCoordinator: NSObject, GKGameCenterControllerDelegate {
    var view: GameCenterView
    
    init(_ gkView: GameCenterView) {
        self.view = gkView
    }
    
    public func gameCenterViewControllerDidFinish(_ gameCenterViewController: GKGameCenterViewController) {
        gameCenterViewController.dismiss(animated: true, completion: nil)
    }
}

To use just add the below wherever it is needed to display a leaderboard.

GameCenterView(leaderboardID: "leaderBoardID")
Roberts answered 9/12, 2022 at 20:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.