😽

GameKit でのランキング表示(SwiftUI)

2025/01/10に公開

はじめに

GameKit と SwiftUI を使ってランキングを表示する方法についてです。
こういうやつ。

IMG_E8939407F52D-1

App Store Connect の設定

まずは App Store Connect でいろいろ設定する必要があります。
左のメニューの GameCenter > Leaderboard を追加で Leaderboard を追加します。

l_1

Leaderboard タイプで適したやつを選択(今回は標準 Leaderboard にしました)。

スコアフォーマットをそれぞれ設定し作成をクリック。

l_2

ローカリゼーションは 1 つ以上必要なのでローカリゼーションを追加から追加します。

l_3

これで App Store Connect での設定は完了です。

GameCenter 対応

続いてアプリ側の設定です。

TARGETS > Signing & Capabilities > + Capability から GameCenter を追加。

l_4

下記のように実装して認証をおこないます。

import SwiftUI
import GameKit

struct ContentView: View {

    @State var isAuthenticated = false

    var body: some View {
        VStack {
            Text("Hoge")
        }
        .onAppear {
            let localPlayer = GKLocalPlayer.local
            localPlayer.authenticateHandler = { viewController, error in
                if let error = error {
                    print(error)
                }
                if let viewController = viewController {
                    let windowScene = UIApplication.shared
                        .connectedScenes
                        .compactMap { $0 as? UIWindowScene }
                        .filter { $0.activationState == .foregroundActive }
                        .first
                    windowScene?.windows.first?.rootViewController?.present(viewController, animated: true)
                }

                self.isAuthenticated = localPlayer.isAuthenticated
            }
        }
    }
}

rootViewController 取得の処理はマルチウィンドウのときなどによくない気もしますが大抵の場合は問題ないのでよしとしましょう。サインインしていない場合は下記のような画面が表示されます。

l_5

スコアの送信

下記の処理でスコアの送信ができます。

var score: Double
GKLeaderboard.submitScore(Int(score * 100),
                          context: 0,
                          player: GKLocalPlayer.local,
                          leaderboardIDs: ["Piyo"]) { error in
    if let error = error {
        print(error)
    }
}

スコアは Int なので変換して送信します。今回は Leaderboard の設定を小数点以下 2 桁までにしているので ✕ 100 をして送信しています(これで 100 を送信すると Leaderboard では 1.00 と表示されます)。

スコアの取得

下記のようにすれば自分の順位とスコアが取得できます。

Task {
    do {
        let boards = try await GKLeaderboard.loadLeaderboards(IDs: ["Piyo"])
        let entries = try? await boards.first?.loadEntries(for: [GKLocalPlayer.local], timeScope: .allTime)
        let rank = entries?.0?.rank
        let score = entries?.0?.score
        print(rank)
        print(score)
    } catch let error {
        print(error)
    }
}

ランキングの表示

ランキングの表示は GKGameCenterViewController を使うので下記のように UIViewControllerRepresentable で View を作ります。

import SwiftUI
import GameKit

struct GameCenterView: UIViewControllerRepresentable {

    func makeUIViewController(context: Context) -> GKGameCenterViewController {
        let viewController = GKGameCenterViewController(state: .leaderboards)
        viewController.gameCenterDelegate = context.coordinator
        return viewController
    }

    func updateUIViewController(_ uiViewController: GKGameCenterViewController, context: Context) {
    }

    class Coordinator: NSObject, GKGameCenterControllerDelegate {

        func gameCenterViewControllerDidFinish(_ gameCenterViewController: GKGameCenterViewController) {
            gameCenterViewController.dismiss(animated: true, completion: nil)
        }
    }

    func makeCoordinator() -> Coordinator {
        return Coordinator()
    }
}

表示したい View で下記のように実装すればランキングを表示できます。

@State var isPresented = false

Button("ranking") {
    if isAuthenticated {
        isPresented = true
    } else {
        print("サインインしてない")
    }
}.fullScreenCover(isPresented: $isPresented) {
    GameCenterView()
}

全部実装したらこんな感じです。

import SwiftUI
import GameKit

struct ContentView: View {

    @State private var isAuthenticated = false
    @State private var rank = 0
    @State private var score = 0
    @State private var isPresented = false

    var body: some View {
        VStack {
            if rank > 0 {
                Text("順位:\(rank)位")
            }

            if score > 0 {
                Text("スコア:\(String(format: "%.2f", Double(score)/100))")
            }

            Button("send") {
                send(score: 60.00)
            }

            Button("fetch") {
                fetch()
            }

            Button("ranking") {
                if isAuthenticated {
                    isPresented = true
                } else {
                    print("サインインしてない")
                }
            }.fullScreenCover(isPresented: $isPresented) {
                GameCenterView()
            }
        }
        .onAppear {
            let localPlayer = GKLocalPlayer.local
            localPlayer.authenticateHandler = { viewController, error in
                if let error = error {
                    print(error)
                }
                if let viewController = viewController {
                    let windowScene = UIApplication.shared
                        .connectedScenes
                        .compactMap { $0 as? UIWindowScene }
                        .filter { $0.activationState == .foregroundActive }
                        .first
                    windowScene?.windows.first?.rootViewController?.present(viewController, animated: true)
                }

                self.isAuthenticated = localPlayer.isAuthenticated
            }
        }
    }

    private func fetch() {
        guard isAuthenticated else {
            return
        }

        Task {
            do {
                let boards = try await GKLeaderboard.loadLeaderboards(IDs: ["Piyo"])
                let entries = try? await boards.first?.loadEntries(for: [GKLocalPlayer.local], timeScope: .allTime)
                if let rank = entries?.0?.rank {
                    self.rank = rank
                }
                if let score = entries?.0?.score {
                    self.score = score
                }
            } catch let error {
                print(error)
            }
        }
    }

    private func send(score: Double) {
        guard isAuthenticated else {
            return
        }

        GKLeaderboard.submitScore(Int(score * 100),
                                  context: 0,
                                  player: GKLocalPlayer.local,
                                  leaderboardIDs: ["Piyo"]) { error in
            if let error = error {
                print(error)
            }
        }
    }
}

テスト後の処理

テストの際にいろいろデータを登録してありえない高得点のスコアを送信してしまうこともあるかと思いますが App Store Connect でデータの削除ができます。

l_6

ただこれはすべてのデータの削除になるのでリリース後に変なデータを入れるとそれだけ削除とかはできないと思います。。。

おわりに

GameKit に関しては記事も少なくちょこちょこ API も変わったりするのでちょっとだけめんどくさいですがこれでランキング表示ができるようになりました!

17 言語にローカライズして世界中の人と競える 100 マス計算アプリを作ったのでよかったらアプリダンロードしてください!!

https://apps.apple.com/jp/app/fast100math/id6739807220

Discussion