🎮

[iOSDC Japan 2025] SwiftUIと秘密の修飾子を支えたGameKit

に公開

はじめに

iOSDC Japan 2025のピクシブ株式会社スポンサーブースでは、「SwiftUIと秘密の修飾子」というSwiftUIのModifierを題材にしたクイズアプリをTestFlightで配信しました。遊んでくださった皆さま、誠にありがとうございました!

このアプリではクイズを解くとスコアを獲得でき、そのスコアをランキング形式で確認できるようになっています。これらのソーシャル機能を実現するために GameKitフレームワーク(Game Center) を利用しました。

本記事では、「SwiftUIと秘密の修飾子」に活用したGame Centerの機能とその実装方法を紹介します。

Game Centerで実現できること

「SwiftUIと秘密の修飾子」では、Game Centerの主要な機能のうち下記を利用しました。

  • Leaderboard
  • 達成項目
  • ゲームデータ同期

Leaderboard

他の参加者とスコアを競えるランキング機能です。
サーバーサイドでの集計やランキング画面を自作する必要なく、友達間や全世界のプレイヤーとスコアを競い合うソーシャルな体験を手軽に実現できます。

「SwiftUIと秘密の修飾子」においては、競い合いや自身の現在地点の確認として使っていただけたのではないでしょうか。

達成項目

名前の通り、アプリ内での特定の行動達成を記録する実績機能です。
自身の達成状況を確認するのはもちろん、グローバルプレイヤーにおけるその項目の達成率も確認することができます。
達成率が可視化されることで、問題ごとの難易度がわかるようになることを目的にしていました。

...が、集計のためには200人程度では数が少なかったのか、はたまたApp Storeに正式に出していない状態ではうまく動かないのか、最後まで各項目の達成率を確認することはできませんでした。
これは知見として次回に活かします。

ゲームデータ同期

複数デバイス間でのデータ同期機能です。同じApple Accountを使っている別のデバイスで、ゲームを続きから再開することができるようになります。

その他の機能

今回は利用しませんでしたが、次のような機能もGame Centerで実現できるようです。

  • 友達が自分のスコアを超えた際に通知する
  • マッチメイキング
  • ターン制ゲーム

今後、同様のアプリを作る際にはこれらも使ってみたいですね!

実装方法

ここまで紹介したGame Centerの機能を使うための実装方法を解説します。

1. App Store Connectを設定する

App Store Connect上でGame Centerの機能を有効化していきます。
「SwiftUIと秘密の修飾子」ではLeaderboard達成項目を設定しました。

上記画像ではLeaderboardは1つしか存在しませんが、開発時にはテスト用のLeaderboardも用意していました。Leaderboardの新規作成や削除、同IDでの作り直しは簡単にできたので、検証は気兼ねなく行えました。
ただし、作成したLeaderboardはアプリからそのIDを参照していなくても、ユーザーから見えてしまうので、不要になったものは削除しておきましょう。

達成項目は反映までに少し時間がかかる印象でした。項目の作成がアプリの配信直前にならないように気をつけましょう。

2. Entitlementsを設定する

Xcodeプロジェクト上でGameKitを有効にしましょう。プロジェクト設定の「Signing & Capabilities」タブを開き、「+ Capability」でGame Centerを追加します。

また、データ同期機能を利用するために、同様にiCloudも追加しておきましょう。iCloud Documentsにチェックを入れ、任意のコンテナを作成します。

3. 認証

Game Centerの各種機能を利用するため、プレイヤーの認証を行う必要があります。
認証とはいってもApple Accountに紐づいたGame Centerの仕組みを使えるので、ユーザーはパスワード等を入力する必要はなく、開発者側の実装も簡単です。

GKLocalPlayer.local.authenticateHandler = { viewController, error in
    // 認証にユーザーの操作が必要な時に`viewController`が渡ってくる
    // ユーザーの操作が不要な場合には`nil`
    if let viewController {
        presentingViewController.present(viewController, animated: true)
        return
    }

    if let error {
        // エラーハンドリング
        return        
    }
}

認証にユーザーの操作が必要な際には上記のようにUIViewControllerが渡ってきます。
SwiftUIを使用している場合、UIWindowSceneからrootViewControllerを取得するなどしてpresentしてあげましょう。

4. Leaderboard

ランキングに反映させるために点数を送信します。

let leaderboardID: String = "App Store Connectで設定したLeaderboardのID"

try await GKLeaderboard.submitScore(
    100,
    context: 0,
    player: GKLocalPlayer.local,
    leaderboardIDs: [Self.leaderboardID]
)

submitScore(_:context:player:completionHandler:)がclass funcとして用意してもらえているのは扱いやすくて助かります。インスタンスメソッドとしてもほぼ同じ関数が用意されているので、都合の良い方を使いましょう。

Game Centerのランキング画面を表示するには次のように実装します。

let gameCenterViewController = GKGameCenterViewController(
    leaderboardID: Self.leaderboardID, 
    playerScope: .global, 
    timeScope: .allTime
)

presentingViewController.present(gameCenterViewController, animated: true)

また、GKAccessPointを設定することで、ランキングを含むGame CenterのダッシュボードにアクセスすることができるFloating Action ButtonのようなUIを表示することができますが、「SwiftUIと秘密の修飾子」では非表示にしました。

5. 達成項目

アプリ内での特定の行動達成を記録します。

let achievementID: String = "App Store Connectで設定した達成項目のID"
let achievement = GKAchievement(identifier: achievementID, player: GKLocalPlayer.local)
achievement.percentComplete = 100.0
achievement.showsCompletionBanner = true

try await GKAchievement.report([achievement])

アプリ側で達成項目を独自に表示するために読み込んだり、全ての達成項目をリセットすることも可能です

// ロード
GKAchievement.loadAchievements { achievements, error in
    if let error {
        // エラーハンドリング
    }
    // ロードした達成項目を処理
}

// リセット
GKAchievement.resetAchievements { error in
    if let error {
        // エラーハンドリング
    }
}

6. ゲームデータ同期

ゲームデータをiCloudを通じて複数のデバイス間で同期できます。
同期させるデータはCodableに準拠させる必要があり、またEncode・Decodeの形式はjsonではなくplistです。tatsubeeはplist形式への変換をこれで初めて使いました。

struct GameData: Codable {
    var items: [Item]
}

// セーブ処理
let localData = GameData(
    items: [
        //...
    ]
)
let plist = try PropertyListEncoder().encode(localData)
try await GKLocalPlayer.local.saveGameData(plist, withName: Self.name)

// ロード処理
let games = try await GKLocalPlayer.local.fetchSavedGames()
    guard let game = games.last else {
        // 例外処理
        return
    }

let plist = try await game.loadData()
let saveData = try PropertyListDecoder().decode(GameData.self, from: plist)

複数端末でデータを保存するとき、コンフリクトが発生することがあります。
GameKitにはコンフリクトを通知するprotocolGKSavedGameListenerと、コンフリクトを解決する処理を行うための関数resolveConflictingSavedGames(_:with:)が用意されています。
GKSavedGameListenerplayer(_ player:hasConflictingSavedGames:)が発火した際に、resolveConflictingSavedGames(_:with:)で正しいデータを渡してコンフリクトを解消してあげる必要があります。さもないと新しいデータが一生反映されません。

class GameCenterManager: NSObject {
    func setupLocalPlayerListener() {
        GKLocalPlayer.local.register(self)
    }

    // ...
}

// `GKSavedGameListener`を内包している`GKLocalPlayerListener`に準拠させる
extension GameCenterManager: GKLocalPlayerListener {
    func player(_: GKPlayer, hasConflictingSavedGames savedGames: [GKSavedGame]) {
        Task {
            var conflictingGameData: [GameData] = []

            await withTaskGroup(of: GameData?.self) { group in
                for game in savedGames {
                    group.addTask {
                        guard let data = try? await game.loadData() else {
                            return nil
                        }
                        return try? PropertyListDecoder().decode(GameData.self, from: data)
                    }
                }

                for await result in group {
                    if let result = result {
                        conflictingGameData.append(result)
                    }
                }
            }

            let mergedData: GameData? = // 複数のゲームデータをいい感じにマージ

            if let mergedData, let plist = try? PropertyListEncoder().encode(mergedData) {
                try await GKLocalPlayer.local.resolveConflictingSavedGames(savedGames, with: plist)
            }
        }
    }

「SwiftUIと秘密の修飾子」はクイズ形式ゆえに、正答している/していないの分かりやすいデータ構造なので、それぞれのセーブデータから、「正答している」ことを優先してデータをマージしてあげれば良さそうですね。

最後に

「SwiftUIと秘密の修飾子」に活用したGameKitフレームワークの機能とその実装方法を紹介してきました。
Game CenterはAppleプラットフォーム限定ではありますが、見ての通り簡単に実装できるので、ちょっと競争要素や実績解除の要素を加えたい際には検討してみてください。

改めて、「SwiftUIと秘密の修飾子」を遊んでくださった皆さま、誠にありがとうございました!

Ref

https://developer.apple.com/jp/game-center/

Discussion