PluginBench
Skill
Pass
Audit score 90

gamekit

dpearson2699/swift-ios-skills

Integrate Game Center features: authentication, leaderboards, achievements, and real-time/turn-based multiplayer.

What is gamekit?

GameKit provides Apple's Game Center integration for iOS games, handling player authentication, leaderboards, achievements, matchmaking, and social features. Use it when you need Game Center sign-in, score submission, achievement tracking, or multiplayer connectivity.

  • Authenticate GKLocalPlayer and check player restrictions (age, multiplayer, communication)
  • Submit and load leaderboard scores with ranking and formatting
  • Report and track achievement progress with completion banners
  • Implement real-time peer-to-peer multiplayer via GKMatch
  • Implement turn-based multiplayer with match state management
  • Display Game Center dashboard and access point UI controls

How to install gamekit

npx skills add https://github.com/dpearson2699/swift-ios-skills --skill gamekit
Prerequisites
  • Xcode project with GameKit framework imported
  • App Store Connect configuration: leaderboards and achievements set up
  • iOS 14+ (iOS 18+ for specific access-point leaderboard triggers)
  • Game Center account for testing
Claude Code
Cursor
Windsurf
Cline

How to use gamekit

  1. 1.Set GKLocalPlayer.local.authenticateHandler early in app lifecycle to sign in the player
  2. 2.Check GKLocalPlayer.local.isAuthenticated before calling any GameKit API
  3. 3.Configure GKAccessPoint.shared location and visibility for the Game Center control
  4. 4.Submit scores via GKLeaderboard.submitScore() after gameplay
  5. 5.Report achievements with GKAchievement and percentComplete (0–100)
  6. 6.Present GKMatchmakerViewController to initiate real-time or turn-based matches
  7. 7.Implement GKMatchDelegate to handle incoming match data and player state changes
  8. 8.Use GKGameCenterViewController to show dashboard, leaderboards, or achievements UI

Use cases

Good for
  • Submit high scores to leaderboards after gameplay ends
  • Unlock achievements when players reach milestones
  • Connect 2–4 players for simultaneous real-time matches
  • Show Game Center dashboard from a pause menu or settings screen
  • Restrict multiplayer or explicit content for underage players
Who it's for
  • iOS game developers using Swift and UIKit/SwiftUI
  • Games requiring Game Center leaderboards or achievements
  • Multiplayer games needing peer-to-peer or turn-based matchmaking
  • Apps integrating Apple's native social gaming features

gamekit FAQ

Do I need to configure leaderboards and achievements in App Store Connect?

Yes. All leaderboards and achievements must be created and configured in App Store Connect before you can submit scores or report progress in code.

Can I submit scores to multiple leaderboards at once?

Yes. GKLeaderboard.submitScore() accepts an array of leaderboardIDs, allowing you to submit a single score to multiple leaderboards in one call.

What happens if a player is underage or has restrictions?

Check GKLocalPlayer.local.isUnderage, isMultiplayerGamingRestricted, and isPersonalizedCommunicationRestricted after authentication, then disable or hide those features accordingly.

How do I reset achievements during testing?

Call GKAchievement.resetAchievements() to clear all progress. This is useful for development and QA testing.

What is the difference between real-time and turn-based multiplayer?

Real-time multiplayer uses GKMatch for simultaneous peer-to-peer gameplay; turn-based multiplayer exchanges moves asynchronously and is managed separately in GameKit.

Full instructions (SKILL.md)

Source of truth, from dpearson2699/swift-ios-skills.


name: gamekit description: "Integrate Game Center features using GameKit. Use when authenticating GKLocalPlayer, checking player restrictions, submitting leaderboard scores, reporting achievements, implementing real-time or turn-based matchmaking, handling GKMatch data, showing the Game Center dashboard or access point, adding challenges and friend invitations, saving game data, or verifying player identity on a server."

GameKit

Use GameKit for Game Center authentication, competition, matchmaking, social surfaces, and saved-game handoffs; keep rendering, board logic, and full SharePlay group-activity design in their owning framework skills.

Contents

Authentication

All GameKit features require the local player to authenticate first. Set the authenticateHandler on GKLocalPlayer.local early in the app lifecycle. GameKit calls the handler multiple times during initialization.

import GameKit

func authenticatePlayer() {
    GKLocalPlayer.local.authenticateHandler = { viewController, error in
        if let viewController {
            // Present so the player can sign in or create an account.
            present(viewController, animated: true)
            return
        }
        if let error {
            // Player could not sign in. Disable Game Center features.
            disableGameCenter()
            return
        }

        // Player authenticated. Check restrictions before starting.
        let player = GKLocalPlayer.local

        if player.isUnderage {
            hideExplicitContent()
        }
        if player.isMultiplayerGamingRestricted {
            disableMultiplayer()
        }
        if player.isPersonalizedCommunicationRestricted {
            disableInGameChat()
        }

        configureAccessPoint()
    }
}

Guard on GKLocalPlayer.local.isAuthenticated before calling any GameKit API. For server-side identity verification, see references/gamekit-patterns.md.

Access Point

GKAccessPoint displays a Game Center control in a corner of the screen. When tapped, it opens the Game Center dashboard. Configure it after authentication.

func configureAccessPoint() {
    GKAccessPoint.shared.location = .topLeading
    GKAccessPoint.shared.showHighlights = true
    GKAccessPoint.shared.isActive = true
}

Hide the access point during gameplay and show it on menu screens:

GKAccessPoint.shared.isActive = false  // Hide during active gameplay
GKAccessPoint.shared.isActive = true   // Show on pause or menu

Open the dashboard to a specific state programmatically. Specific leaderboard access-point triggers require iOS 18+.

// Open directly to a leaderboard
GKAccessPoint.shared.trigger(
    leaderboardID: "com.mygame.highscores",
    playerScope: .global,
    timeScope: .allTime
) { }

// Open directly to achievements
GKAccessPoint.shared.trigger(state: .achievements) { }

Dashboard

Present the Game Center dashboard using GKGameCenterViewController. The presenting object must conform to GKGameCenterControllerDelegate.

final class GameViewController: UIViewController, GKGameCenterControllerDelegate {

    func showDashboard() {
        let vc = GKGameCenterViewController(state: .dashboard)
        vc.gameCenterDelegate = self
        present(vc, animated: true)
    }

    func showLeaderboard(_ leaderboardID: String) {
        let vc = GKGameCenterViewController(
            leaderboardID: leaderboardID,
            playerScope: .global,
            timeScope: .allTime
        )
        vc.gameCenterDelegate = self
        present(vc, animated: true)
    }

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

Dashboard states include .dashboard, .leaderboards, .achievements, .challenges, .localPlayerProfile, and .localPlayerFriendsList.

Leaderboards

Configure leaderboards in App Store Connect before submitting scores. Supports classic (persistent) and recurring (time-limited, auto-resetting) types.

Submitting Scores

Submit to one or more leaderboards using the class method:

func submitScore(_ score: Int, leaderboardIDs: [String]) async throws {
    try await GKLeaderboard.submitScore(
        score,
        context: 0,
        player: GKLocalPlayer.local,
        leaderboardIDs: leaderboardIDs
    )
}

Loading Entries

func loadTopScores(
    leaderboardID: String,
    count: Int = 10
) async throws -> (GKLeaderboard.Entry?, [GKLeaderboard.Entry]) {
    let leaderboards = try await GKLeaderboard.loadLeaderboards(
        IDs: [leaderboardID]
    )
    guard let leaderboard = leaderboards.first else { return (nil, []) }

    let (localEntry, entries, _) = try await leaderboard.loadEntries(
        for: .global,
        timeScope: .allTime,
        range: 1...count
    )
    return (localEntry, entries)
}

GKLeaderboard.Entry provides player, rank, score, formattedScore, context, and date. For recurring leaderboard timing, leaderboard images, and leaderboard sets, see references/gamekit-patterns.md.

Achievements

Configure achievements in App Store Connect. Each achievement has a unique identifier, point value, and localized title/description.

Reporting Progress

Set percentComplete from 0...100. The property type is Double, but Apple requires an integer value. GameKit only accepts increases.

func reportAchievement(identifier: String, percentComplete: Int) async throws {
    let achievement = GKAchievement(identifier: identifier)
    achievement.percentComplete = Double(min(max(percentComplete, 0), 100))
    achievement.showsCompletionBanner = true
    try await GKAchievement.report([achievement])
}

// Unlock an achievement completely
func unlockAchievement(_ identifier: String) async throws {
    try await reportAchievement(identifier: identifier, percentComplete: 100)
}

Loading Player Achievements

func loadPlayerAchievements() async throws -> [GKAchievement] {
    try await GKAchievement.loadAchievements()
}

If an achievement is not returned, the player has no progress on it yet. Create a new GKAchievement(identifier:) to begin reporting. Use GKAchievement.resetAchievements() to reset all progress during testing.

Real-Time Multiplayer

Real-time multiplayer connects players in a peer-to-peer network for simultaneous gameplay. Players exchange data directly through GKMatch.

Matchmaking with GameKit UI

Use GKMatchmakerViewController for the standard matchmaking interface:

func presentMatchmaker() {
    let request = GKMatchRequest()
    request.minPlayers = 2
    request.maxPlayers = 4
    request.inviteMessage = "Join my game!"

    guard let matchmakerVC = GKMatchmakerViewController(matchRequest: request) else {
        return
    }
    matchmakerVC.matchmakerDelegate = self
    present(matchmakerVC, animated: true)
}

Implement GKMatchmakerViewControllerDelegate:

extension GameViewController: GKMatchmakerViewControllerDelegate {
    func matchmakerViewController(
        _ viewController: GKMatchmakerViewController,
        didFind match: GKMatch
    ) {
        match.delegate = self
        viewController.dismiss(animated: true)
        startGame(with: match)
    }

    func matchmakerViewControllerWasCancelled(
        _ viewController: GKMatchmakerViewController
    ) {
        viewController.dismiss(animated: true)
    }

    func matchmakerViewController(
        _ viewController: GKMatchmakerViewController,
        didFailWithError error: Error
    ) {
        viewController.dismiss(animated: true)
    }
}

Exchanging Data

Send and receive game state through GKMatch and GKMatchDelegate:

extension GameViewController: GKMatchDelegate {
    func sendAction(_ action: GameAction, to match: GKMatch) throws {
        let data = try JSONEncoder().encode(action)
        try match.sendData(toAllPlayers: data, with: .reliable)
    }

    func match(_ match: GKMatch, didReceive data: Data, fromRemotePlayer player: GKPlayer) {
        guard let action = try? JSONDecoder().decode(GameAction.self, from: data) else {
            return
        }
        handleRemoteAction(action, from: player)
    }

    func match(_ match: GKMatch, player: GKPlayer, didChange state: GKPlayerConnectionState) {
        switch state {
        case .connected:
            checkIfReadyToStart(match)
        case .disconnected:
            handlePlayerDisconnected(player)
        default:
            break
        }
    }
}

Data modes: .reliable sends until delivery succeeds or the connection times out; .unreliable sends once and may arrive out of order. Use .reliable for critical state and .unreliable for small, time-sensitive updates. Treat received match data as untrusted input. Register the local player as a listener (GKLocalPlayer.local.register(self)) to receive invitations. For programmatic matchmaking and custom match UI, see references/gamekit-patterns.md.

Turn-Based Multiplayer

Turn-based games store match state on Game Center servers. Players take turns asynchronously and do not need to be online simultaneously.

Starting a Match

let request = GKMatchRequest()
request.minPlayers = 2
request.maxPlayers = 4

let matchmakerVC = GKTurnBasedMatchmakerViewController(matchRequest: request)
matchmakerVC.turnBasedMatchmakerDelegate = self
present(matchmakerVC, animated: true)

Taking Turns

Encode game state into Data, end the turn, and specify the next participants:

func endTurn(match: GKTurnBasedMatch, gameState: GameState) async throws {
    let data = try JSONEncoder().encode(gameState)

    // Build next participants list: remaining active players
    let nextParticipants = match.participants.filter {
        $0.status != .done && $0 != match.currentParticipant
    }

    try await match.endTurn(
        withNextParticipants: nextParticipants,
        turnTimeout: GKTurnTimeoutDefault,
        match: data
    )
}

Ending the Match

Set outcomes for all participants, then end the match:

func endMatch(_ match: GKTurnBasedMatch, winnerIndex: Int, data: Data) async throws {
    for (index, participant) in match.participants.enumerated() {
        participant.matchOutcome = (index == winnerIndex) ? .won : .lost
    }
    try await match.endMatchInTurn(withMatch: data)
}

Listening for Turn Events

Register as a listener. Prefer GKLocalPlayerListener when one object handles multiple Game Center event categories.

GKLocalPlayer.local.register(self)

extension GameViewController: GKLocalPlayerListener {
    func player(_ player: GKPlayer, receivedTurnEventFor match: GKTurnBasedMatch,
                didBecomeActive: Bool) {
        // Load match data and update UI
        loadAndDisplayMatch(match)
    }

    func player(_ player: GKPlayer, matchEnded match: GKTurnBasedMatch) {
        showMatchResults(match)
    }
}

Match Data Size

Check the match object's matchDataMaximumSize before ending a turn. Store larger state externally and keep only compact references in match data.

Common Mistakes

Not authenticating before using GameKit APIs

// DON'T
func submitScore() {
    GKLeaderboard.submitScore(100, context: 0, player: GKLocalPlayer.local,
                              leaderboardIDs: ["scores"]) { _ in }
}

// DO
func submitScore() async throws {
    guard GKLocalPlayer.local.isAuthenticated else { return }
    try await GKLeaderboard.submitScore(
        100, context: 0, player: GKLocalPlayer.local, leaderboardIDs: ["scores"]
    )
}

Setting authenticateHandler multiple times

// DON'T: Set handler on every scene transition
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    GKLocalPlayer.local.authenticateHandler = { vc, error in /* ... */ }
}

// DO: Set the handler once, early in the app lifecycle

Ignoring multiplayer restrictions

// DON'T
func showMultiplayerMenu() { presentMatchmaker() }

// DO
func showMultiplayerMenu() {
    guard !GKLocalPlayer.local.isMultiplayerGamingRestricted else { return }
    presentMatchmaker()
}

Not setting match delegate immediately

// DON'T: Set delegate in dismiss completion -- misses early messages
func matchmakerViewController(_ vc: GKMatchmakerViewController, didFind match: GKMatch) {
    vc.dismiss(animated: true) { match.delegate = self }
}

// DO: Set delegate before dismissing
func matchmakerViewController(_ vc: GKMatchmakerViewController, didFind match: GKMatch) {
    match.delegate = self
    vc.dismiss(animated: true)
}

Not calling finishMatchmaking for programmatic matches

// DON'T
let match = try await GKMatchmaker.shared().findMatch(for: request)
startGame(with: match)

// DO
let match = try await GKMatchmaker.shared().findMatch(for: request)
GKMatchmaker.shared().finishMatchmaking(for: match)
startGame(with: match)

Not disconnecting from match

// DON'T
func returnToMenu() { showMainMenu() }

// DO
func returnToMenu() {
    currentMatch?.disconnect()
    currentMatch?.delegate = nil
    currentMatch = nil
    showMainMenu()
}

Review Checklist

  • GKLocalPlayer.local.authenticateHandler set once at app launch
  • isAuthenticated checked before any GameKit API call
  • Player restrictions checked (isUnderage, isMultiplayerGamingRestricted, isPersonalizedCommunicationRestricted)
  • Game Center capability added in Xcode signing settings
  • Leaderboards and achievements configured in App Store Connect
  • Access point configured and toggled appropriately during gameplay
  • GKGameCenterControllerDelegate dismisses dashboard in gameCenterViewControllerDidFinish
  • Match delegate set immediately when match is found
  • finishMatchmaking(for:) called for programmatic matches; disconnect() and nil delegate on exit
  • Turn-based match data stays under match.matchDataMaximumSize
  • Turn-based participants have outcomes set before endMatchInTurn
  • Invitation or turn listener registered with GKLocalPlayer.local.register(_:)
  • Data mode chosen appropriately: .reliable for state, .unreliable for frequent updates
  • Error handling for all async GameKit calls

References