skills/gamekit/SKILL.md
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.
npx skillsauth add dpearson2699/swift-ios-skills gamekitInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Integrate Game Center services into iOS 26+ games using GameKit and Swift 6.3: authentication, leaderboards, achievements, multiplayer matchmaking, access point, dashboard, challenges, and saved games. Keep SpriteKit rendering, SceneKit 3D, TabletopKit board logic, and full SharePlay group-activity design in their framework domains; use GameKit only for Game Center handoff points.
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.
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) { }
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.
Configure leaderboards in App Store Connect before submitting scores. Supports classic (persistent) and recurring (time-limited, auto-resetting) types.
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
)
}
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.
Configure achievements in App Store Connect. Each achievement has a unique identifier, point value, and localized title/description.
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)
}
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 connects players in a peer-to-peer network for
simultaneous gameplay. Players exchange data directly through GKMatch.
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)
}
}
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 games store match state on Game Center servers. Players take turns asynchronously and do not need to be online simultaneously.
let request = GKMatchRequest()
request.minPlayers = 2
request.maxPlayers = 4
let matchmakerVC = GKTurnBasedMatchmakerViewController(matchRequest: request)
matchmakerVC.turnBasedMatchmakerDelegate = self
present(matchmakerVC, animated: true)
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
)
}
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)
}
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)
}
}
Check the match object's matchDataMaximumSize before ending a turn. Store larger state externally and keep only compact references in match data.
// 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"]
)
}
// 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
// DON'T
func showMultiplayerMenu() { presentMatchmaker() }
// DO
func showMultiplayerMenu() {
guard !GKLocalPlayer.local.isMultiplayerGamingRestricted else { return }
presentMatchmaker()
}
// 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)
}
// 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)
// DON'T
func returnToMenu() { showMainMenu() }
// DO
func returnToMenu() {
currentMatch?.disconnect()
currentMatch?.delegate = nil
currentMatch = nil
showMainMenu()
}
GKLocalPlayer.local.authenticateHandler set once at app launchisAuthenticated checked before any GameKit API callisUnderage, isMultiplayerGamingRestricted, isPersonalizedCommunicationRestricted)GKGameCenterControllerDelegate dismisses dashboard in gameCenterViewControllerDidFinishfinishMatchmaking(for:) called for programmatic matches; disconnect() and nil delegate on exitmatch.matchDataMaximumSizeendMatchInTurnGKLocalPlayer.local.register(_:).reliable for state, .unreliable for frequent updatesGKVoiceChat has NSMicrophoneUsageDescriptiondevelopment
Implement, review, or improve data visualizations using Swift Charts. Use when building bar, line, area, point, pie, donut, or iOS 26 3D charts; when adding chart selection, scrolling, annotations, axes, scales, legends, or foregroundStyle grouping; when plotting functions with BarPlot, LinePlot, AreaPlot, PointPlot, Chart3D, or SurfacePlot; or when creating heat maps, Gantt charts, grouped bars, sparklines, threshold lines, or spatial visualizations.
data-ai
Select, implement, or migrate between app architecture patterns for Apple platform apps. Use when choosing between MV (Model-View with @Observable), MVVM, MVI, TCA (The Composable Architecture), Clean Architecture, VIPER, or Coordinator patterns; when evaluating architecture fit for a feature's complexity; when migrating from one pattern to another; or when reviewing whether an app's current architecture is appropriate. Scoped to Apple-platform patterns using Swift 6.3, SwiftUI, and UIKit.
development
Apply Swift API Design Guidelines to name, label, and document Swift APIs. Covers argument label rules (prepositional phrase rule, grammatical phrase rule, first-label omission), mutating/nonmutating pair naming (-ed/-ing participle pattern, form- prefix, sort/sorted, formUnion/union), side-effect naming (noun for pure, verb for mutating), documentation comment structure (summary by declaration kind, O(1) complexity rule), clarity at call site, role-based naming, protocol naming (-able/-ible/-ing), default arguments over method families, casing conventions, and terminology. Use when designing new Swift APIs, reviewing naming and argument labels, writing documentation comments, or refactoring for call site clarity.
development
Implement, review, or improve in-app purchases and subscriptions using StoreKit 2. Use when building paywalls with SubscriptionStoreView or ProductView, processing transactions with Product and Transaction APIs, verifying entitlements, handling purchase flows (consumable, non-consumable, auto-renewable), implementing offer codes or promotional/win-back/introductory offers, managing subscription status and renewal state, setting up StoreKit testing with configuration files, or integrating Family Sharing, Ask to Buy, refund handling, and billing retry logic.