
Gamekit
- 2.1k installs
- 944 repo stars
- Updated July 15, 2026
- dpearson2699/swift-ios-skills
gamekit is an agent skill that Integrate Game Center features using GameKit. Use when authenticating GKLocalPlayer, checking player restrictions, submitting leaderboard scores, reporting achievements, .
About
The gamekit skill. 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. 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. Set the on early in the app lifecycle. GameKit calls the handler multiple times during initialization. Guard on before calling any GameKit API. For server-side identity verification, see [references/gamekit-patterns.md](references/gamekit-patterns.md). When tapped, it opens the Game Center dashboard. The workflow follows the source SKILL.md contract with progressive reference loading, clear trigger phrases, and practical steps developers can apply directly in agent sessions.
- [Authentication](#authentication)
- [Access Point](#access-point)
- [Dashboard](#dashboard)
- [Leaderboards](#leaderboards)
- [Achievements](#achievements)
Gamekit by the numbers
- 2,091 all-time installs (skills.sh)
- +113 installs in the week ending Jul 29, 2026 (Skillselion tracking)
- Ranked #102 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
gamekit capabilities & compatibility
- Capabilities
- [authentication](#authentication) · [access point](#access point) · [dashboard](#dashboard) · [leaderboards](#leaderboards) · [achievements](#achievements)
- Use cases
- testing · debugging · ci cd
What gamekit says it does
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.
GameKit calls the handler multiple times during initialization.
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill gamekitAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.1k |
|---|---|
| repo stars | ★ 944 |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 15, 2026 |
| Repository | dpearson2699/swift-ios-skills ↗ |
How do I apply gamekit correctly using the SKILL.md workflows and reference files?
Integrate Game Center features using GameKit. Use when authenticating GKLocalPlayer, checking player restrictions, submitting leaderboard scores, reporting achievements, implementing real-time or turn
Who is it for?
Developers and software engineers working with gamekit patterns from the skill documentation.
Skip if: Skip when cached docs are empty, boilerplate-only, or outside the skill documented scope.
When should I use this skill?
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,
What you get
Grounded gamekit guidance with highlights, triggers, and evidence quotes from SKILL.md.
- GameKit auth module
- leaderboard integration
- multiplayer matchmaking code
By the numbers
- Targets iOS 26+ with Swift 6.3 GameKit APIs
Files
GameKit
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.
Contents
- Authentication
- Access Point
- Dashboard
- Leaderboards
- Achievements
- Real-Time Multiplayer
- Turn-Based Multiplayer
- Common Mistakes
- Review Checklist
- References
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 menuOpen 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 lifecycleIgnoring 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.authenticateHandlerset once at app launch - [ ]
isAuthenticatedchecked 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
- [ ]
GKGameCenterControllerDelegatedismisses dashboard ingameCenterViewControllerDidFinish - [ ] 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:
.reliablefor state,.unreliablefor frequent updates - [ ] New voice/social audio uses SharePlay; legacy
GKVoiceChathasNSMicrophoneUsageDescription - [ ] Error handling for all async GameKit calls
References
- See references/gamekit-patterns.md for identity verification, legacy voice chat, saved games, custom match UI, leaderboard images, challenge handling, and rule-based matchmaking.
- GameKit documentation
- GKLocalPlayer
- GKAccessPoint
- GKLeaderboard
- GKAchievement
- GKMatch
- GKTurnBasedMatch
{
"skill_name": "gamekit",
"evals": [
{
"id": 0,
"prompt": "Review this GameKit integration plan: authenticate with Game Center, send fetchItems(forIdentityVerificationSignature: nil), playerID, timestamp, salt, and signature to our backend, store several save slots with the same name, fetch saved games with `try await GKLocalPlayer.local.fetchSavedGames() ?? []`, and cap turn data at a hard 64 KB. Correct the plan for iOS 26+ and call out server, saved-game, and match-data risks.",
"expected_output": "A correction-focused review that uses the current async identity verification API, scoped identifiers and bundle ID in server verification, iCloud saved-game prerequisites and overwrite/conflict semantics, non-optional async return handling, and object-reported match data size limits.",
"files": [],
"expectations": [
"Uses `fetchItemsForIdentityVerificationSignature()` and does not call the obsolete or callback-shaped `fetchItems(forIdentityVerificationSignature:)` form in async code.",
"Explains that server verification covers `teamPlayerID` or Apple Arcade `gamePlayerID`, bundle ID, big-endian timestamp, and salt, and rejects stale timestamps.",
"States that saved games require the player's iCloud account, iCloud Drive, and app iCloud capability/container configuration.",
"Explains that saving with an existing filename overwrites and duplicate same-name saves are conflicts to resolve, not normal independent slots.",
"Does not add `?? []` to async GameKit array-returning APIs such as `fetchSavedGames()`.",
"Uses `match.matchDataMaximumSize` or related object limits instead of hard-coding 64 KB."
]
},
{
"id": 1,
"prompt": "I'm adding advanced GameKit matchmaking and social features. Show how to use rule-based matchmaking with queueName, properties, recipientProperties, playerAttributes for roles, achievement progress updates, and a challenge compose controller. Include availability and gotchas that would keep this from shipping with stale GameKit assumptions.",
"expected_output": "A modern GameKit implementation outline that scopes rule-based matchmaking to supported OS versions and property constraints, avoids stale playerAttributes role masks, reports integer achievement progress, and presents challenge compose UI with the current return shape.",
"files": [],
"expectations": [
"Marks `queueName`, request properties, recipient properties, and match/player properties as iOS 17.2+ or equivalent platform release features.",
"Uses reverse-DNS-style queue names and notes JSON-serializable properties, recipient-property key coverage, and the reserved `gc` key.",
"Explains that nonzero `playerAttributes` must combine across players with bitwise OR equal to `0xFFFFFFFF`, or recommends rule-based matchmaking for modern role logic.",
"States that `queueName` causes `playerGroup` and `playerAttributes` to be ignored.",
"Reports achievement progress as an integer value in the 0...100 range even though `percentComplete` is a `Double` property.",
"Treats `challengeComposeController(withMessage:players:completion:)` as returning a view controller to present, not an optional to unwrap."
]
},
{
"id": 2,
"prompt": "I want one iOS game architecture note covering Game Center authentication, leaderboards, real-time and turn-based matches, SpriteKit rendering, SceneKit 3D board scenes, TabletopKit rules, SharePlay party chat, and old GameKit voice chat. What belongs in GameKit, what should be handed to sibling frameworks, and what legacy caveats matter?",
"expected_output": "A boundary-aware GameKit architecture note that keeps GameKit focused on Game Center services, routes rendering and table/game-session domains to sibling frameworks, prefers SharePlay for new social audio, and documents legacy `GKVoiceChat` requirements accurately.",
"files": [],
"expectations": [
"Keeps GameKit ownership to Game Center authentication, restrictions, access point/dashboard, leaderboards, achievements, matchmaking, match data, turns, invites, challenges, saved games, and server identity verification.",
"Routes SpriteKit rendering, SceneKit 3D scenes, TabletopKit board/rules logic, and full GroupActivities session design to sibling framework guidance instead of expanding GameKit scope.",
"Marks `GKVoiceChat` as deprecated and recommends SharePlay for new voice or social audio work.",
"For legacy `GKVoiceChat`, includes microphone usage description, audio session activation, `GKVoiceChat.isVoIPAllowed()`, channel start, and `isActive` microphone control.",
"Checks local player restrictions before multiplayer or personalized communication features.",
"Treats received `GKMatch` data as untrusted input and chooses reliable versus unreliable data modes by message criticality."
]
}
]
}
GameKit Patterns
Advanced GameKit patterns for player identity verification, legacy voice chat, saved games, custom matchmaking UI, leaderboard images, challenge handling, and rule-based matchmaking.
Contents
- Server-Side Identity Verification
- Voice Chat
- Saved Games
- Custom Matchmaking UI
- Leaderboard Images and Sets
- Challenge Handling
- Rule-Based Matchmaking
- Player Groups and Attributes
- Hosted Matches
- Turn-Based Data Exchanges
- Friend Management
- Nearby Player Discovery
- SharePlay Integration
Server-Side Identity Verification
Verify the local player on a backend server using a cryptographic signature from the async identity-verification API:
enum GameKitIdentityError: Error {
case missingBundleIdentifier
}
func verifyPlayerOnServer() async throws {
let (publicKeyURL, signature, salt, timestamp) =
try await GKLocalPlayer.local.fetchItemsForIdentityVerificationSignature()
// Send these values plus the bundle ID and a scoped player identifier.
// Use teamPlayerID for most games, or gamePlayerID for Apple Arcade games.
let playerID = GKLocalPlayer.local.teamPlayerID
guard let bundleID = Bundle.main.bundleIdentifier else {
throw GameKitIdentityError.missingBundleIdentifier
}
sendToServer(publicKeyURL, signature, salt, timestamp, playerID, bundleID)
}The server fetches the public key from the URL Apple provides, then verifies that Apple signed it. Verify the signature over this byte sequence: teamPlayerID (or Apple Arcade gamePlayerID) as UTF-8, bundle ID as UTF-8, timestamp as big-endian UInt64, then salt. Reject stale timestamps and trust only fields covered by the signature.
Voice Chat
GKVoiceChat is deprecated. Prefer SharePlay for new voice or social audio work. Keep this section only for maintaining existing GameKit voice chat. Each named channel supports independent volume and mute controls.
Prerequisites
Add NSMicrophoneUsageDescription to Info.plist, activate an audio session, and check GKVoiceChat.isVoIPAllowed() before creating channels.
import AVFoundation
func configureAudioSession() throws {
let session = AVAudioSession.sharedInstance()
try session.setActive(true)
}Creating and Starting a Channel
Create voice chat channels from a GKMatch object:
func startVoiceChat(in match: GKMatch) {
guard GKVoiceChat.isVoIPAllowed() else { return }
guard let voiceChat = match.voiceChat(withName: "teamChat") else { return }
voiceChat.volume = 0.8
voiceChat.start()
voiceChat.isActive = true
voiceChat.playerVoiceChatStateDidChangeHandler = { player, state in
switch state {
case .connected:
print("\(player.displayName) joined voice chat")
case .disconnected:
print("\(player.displayName) left voice chat")
case .speaking:
// Update UI to show speaking indicator
self.showSpeakingIndicator(for: player)
case .silent:
self.hideSpeakingIndicator(for: player)
case .connecting:
break
@unknown default:
break
}
}
}Multiple Channels
Create separate channels for different purposes, such as team chat and global chat. A player can only have their microphone active in one channel at a time:
let teamChat = match.voiceChat(withName: "team")
let allChat = match.voiceChat(withName: "all")
// Start both but activate only one microphone at a time
teamChat?.start()
allChat?.start()
teamChat?.isActive = true
allChat?.isActive = false
// Switch active channel
func switchToAllChat() {
teamChat?.isActive = false
allChat?.isActive = true
}Muting Players
func mutePlayer(_ player: GKPlayer, in voiceChat: GKVoiceChat) {
voiceChat.setPlayer(player, muted: true)
}
func unmutePlayer(_ player: GKPlayer, in voiceChat: GKVoiceChat) {
voiceChat.setPlayer(player, muted: false)
}Stopping Voice Chat
func stopVoiceChat(_ voiceChat: GKVoiceChat) {
voiceChat.isActive = false
voiceChat.stop()
}Saved Games
GameKit stores game data in the player's iCloud account, accessible from devices using the same Game Center account. The player must have an iCloud account and iCloud Drive enabled, and the app needs the iCloud capability with an iCloud container identifier. Saved games are managed through GKLocalPlayer and represented by GKSavedGame.
Saving Game Data
Encode game state and save with a descriptive name:
func saveGame(state: GameState, name: String) async throws {
let data = try JSONEncoder().encode(state)
try await GKLocalPlayer.local.saveGameData(data, withName: name)
}Saving with an existing filename overwrites that file. Use unique filenames for multiple save slots. Duplicate filenames from multiple devices are conflicts that your game must resolve.
Fetching Saved Games
func fetchSavedGames() async throws -> [GKSavedGame] {
try await GKLocalPlayer.local.fetchSavedGames()
}Loading Saved Game Data
func loadSavedGame(_ savedGame: GKSavedGame) async throws -> GameState {
let data = try await savedGame.loadData()
return try JSONDecoder().decode(GameState.self, from: data)
}GKSavedGame properties: name, modificationDate, deviceName.
Resolving Conflicts
When the same save name exists from multiple devices, GameKit may report conflicts. Resolve them by choosing the authoritative data:
func resolveConflicts(_ conflicts: [GKSavedGame], using data: Data) async throws {
try await GKLocalPlayer.local.resolveConflictingSavedGames(
conflicts, with: data
)
}Listening for Saved Game Events
Implement GKSavedGameListener, or GKLocalPlayerListener if the same object handles multiple Game Center events, to respond to save events from other devices:
extension GameManager: GKSavedGameListener {
func player(_ player: GKPlayer, didModifySavedGame savedGame: GKSavedGame) {
// Another device modified a save. Refresh local data.
Task { await refreshSavedGames() }
}
func player(_ player: GKPlayer,
hasConflictingSavedGames savedGames: [GKSavedGame]) {
// Resolve conflicts using game-specific merge logic.
Task { await resolveConflictingGames(savedGames) }
}
}Register the listener:
GKLocalPlayer.local.register(gameManager)Deleting Saved Games
func deleteSavedGame(name: String) async throws {
try await GKLocalPlayer.local.deleteSavedGames(withName: name)
}Custom Matchmaking UI
Build a custom interface for finding players instead of using GKMatchmakerViewController. Use GKMatchmaker directly.
Finding a Match Programmatically
actor MatchManager {
private var currentMatch: GKMatch?
func findMatch(minPlayers: Int, maxPlayers: Int) async throws -> GKMatch {
let request = GKMatchRequest()
request.minPlayers = minPlayers
request.maxPlayers = maxPlayers
let match = try await GKMatchmaker.shared().findMatch(for: request)
GKMatchmaker.shared().finishMatchmaking(for: match)
currentMatch = match
return match
}
func cancelMatchmaking() {
GKMatchmaker.shared().cancel()
}
}Adding Players to an Existing Match
func addPlayers(to match: GKMatch, request: GKMatchRequest) async throws {
try await GKMatchmaker.shared().addPlayers(
to: match,
matchRequest: request
)
}Querying Matchmaking Activity
Check how many players are currently looking for matches:
func checkActivity() async throws -> Int {
try await GKMatchmaker.shared().queryActivity()
}
func checkGroupActivity(group: Int) async throws -> Int {
try await GKMatchmaker.shared().queryPlayerGroupActivity(group)
}Inviting Specific Players
func invitePlayers(_ players: [GKPlayer]) async throws -> GKMatch {
let request = GKMatchRequest()
request.minPlayers = 2
request.maxPlayers = 4
request.recipients = players
request.inviteMessage = "Play a round?"
request.recipientResponseHandler = { player, response in
switch response {
case .accepted:
print("\(player.displayName) accepted")
case .declined:
print("\(player.displayName) declined")
default:
break
}
}
return try await GKMatchmaker.shared().findMatch(for: request)
}Leaderboard Images and Sets
Loading Leaderboard Images
Leaderboard images configured in App Store Connect are not loaded with the leaderboard data. Fetch them separately:
func loadLeaderboardImage(leaderboardID: String) async throws -> UIImage? {
let leaderboards = try await GKLeaderboard.loadLeaderboards(
IDs: [leaderboardID]
)
guard let leaderboard = leaderboards.first else { return nil }
return try await leaderboard.loadImage()
}Leaderboard Sets
Leaderboard sets group related leaderboards together. Load sets and then load the leaderboards within each set:
func loadLeaderboardSets() async throws -> [GKLeaderboardSet] {
try await GKLeaderboardSet.loadLeaderboardSets()
}
func loadLeaderboards(in set: GKLeaderboardSet) async throws -> [GKLeaderboard] {
try await set.loadLeaderboards()
}Leaderboard Entry Properties
GKLeaderboard.Entry provides these properties for display:
func displayEntry(_ entry: GKLeaderboard.Entry) {
let playerName = entry.player.displayName
let rank = entry.rank
let score = entry.score
let formatted = entry.formattedScore
let context = entry.context // Game-defined value submitted with the score
let date = entry.date
}Submitting Scores with Context
Use context to store additional metadata with a score, such as the level where the score was achieved:
try await GKLeaderboard.submitScore(
score,
context: levelID,
player: GKLocalPlayer.local,
leaderboardIDs: ["com.mygame.scores"]
)Challenge Handling
Players can challenge friends to beat their scores or complete achievements.
Achievement Challenges
func challengeFriends(
achievementID: String,
message: String,
players: [GKPlayer]
) {
let achievement = GKAchievement(identifier: achievementID)
let vc = achievement.challengeComposeController(
withMessage: message,
players: players
) { composeVC, issued, sentPlayers in
composeVC.dismiss(animated: true)
}
present(vc, animated: true)
}Finding Challengeable Players
func loadChallengeableFriends() async throws -> [GKPlayer] {
try await GKLocalPlayer.local.loadChallengableFriends()
}Selecting Players Who Can Earn an Achievement
Filter players who haven't already completed an achievement:
func findEligiblePlayers(
for achievementID: String,
from players: [GKPlayer]
) async throws -> [GKPlayer] {
let achievement = GKAchievement(identifier: achievementID)
return try await achievement.selectChallengeablePlayers(players)
}Opening the Challenges View
GKAccessPoint.shared.triggerForChallenges { }Rule-Based Matchmaking
Configure matchmaking rules in App Store Connect to refine player matching based on game-specific criteria. Rules evaluate player properties to determine compatible matches. queueName, properties, recipientProperties, GKMatch.properties, and GKMatch.playerProperties require iOS 17.2+ or the equivalent platform releases.
queueName must be a case-sensitive reverse-DNS-style identifier using only letters, numbers, hyphens, and periods. properties and recipientProperties must be JSON-serializable, and the key gc is reserved by GameKit.
Setting Up Rule-Based Matching
func findRuleBasedMatch(skill: Int, region: String) async throws -> GKMatch {
let request = GKMatchRequest()
request.minPlayers = 2
request.maxPlayers = 4
request.queueName = "com.mygame.competitive"
request.properties = [
"skill": skill,
"region": region
]
let match = try await GKMatchmaker.shared().findMatch(for: request)
GKMatchmaker.shared().finishMatchmaking(for: match)
return match
}Accessing Match Properties
After a match is found, read the properties that matchmaking rules evaluated:
func inspectMatchProperties(_ match: GKMatch) {
// Local player's properties (includes rule additions)
let myProps = match.properties
// Other players' properties
for (player, props) in match.playerProperties ?? [:] {
print("\(player.displayName): \(props)")
}
}Rule-Based Matching with Invited Players
Set properties for invited recipients. Every key in recipientProperties must also be present in recipients.
func inviteWithRules(
players: [GKPlayer],
properties: [String: Any]
) async throws -> GKMatch {
let request = GKMatchRequest()
request.minPlayers = 2
request.maxPlayers = 4
request.queueName = "com.mygame.competitive"
request.recipients = players
request.properties = properties
var recipientProps: [GKPlayer: [String: Any]] = [:]
for player in players {
recipientProps[player] = ["skill": 1000] // Default skill for invitees
}
request.recipientProperties = recipientProps
return try await GKMatchmaker.shared().findMatch(for: request)
}When queueName is set, playerGroup and playerAttributes are ignored.
Player Groups and Attributes
Use player groups and attributes for simple matchmaking without rules.
Player Groups
Restrict matching to players in the same group. Groups are identified by an integer value:
let request = GKMatchRequest()
request.minPlayers = 2
request.maxPlayers = 4
request.playerGroup = 42 // Only matches players in group 42Use groups to separate players by game mode, difficulty, or map.
Player Attributes
Use playerAttributes only for simple non-rule-based matchmaking. If the value is nonzero, GameKit tries to combine players so the bitwise OR of all participants' masks equals 0xFFFFFFFF:
let attackerMask: UInt32 = 0x000000FF
let defenderMask: UInt32 = 0x0000FF00
let supportMask: UInt32 = 0xFFFF0000
let request = GKMatchRequest()
request.minPlayers = 3
request.maxPlayers = 3
request.playerAttributes = attackerMaskUse matchmaking rules for modern skill, region, version, party-code, and team assignment logic. When queueName is set, GameKit ignores playerGroup and playerAttributes.
Hosted Matches
For server-hosted games, use GKMatchmaker to find players but handle networking through your own infrastructure.
func findPlayersForHostedMatch() async throws -> [GKPlayer] {
let request = GKMatchRequest()
request.minPlayers = 2
request.maxPlayers = 8
let players = try await GKMatchmaker.shared().findPlayers(
forHostedRequest: request
)
// Connect players through your game server
return players
}Hosted Match with Matchmaking Rules
func findPlayersWithRules() async throws -> GKMatchedPlayers {
let request = GKMatchRequest()
request.minPlayers = 2
request.maxPlayers = 8
request.queueName = "com.mygame.ranked"
request.properties = ["elo": 1500]
let matchedPlayers = try await GKMatchmaker.shared().findMatchedPlayers(
request
)
// matchedPlayers.players - the matched players
// matchedPlayers.properties - the local player's properties
// matchedPlayers.playerProperties - other players' properties
return matchedPlayers
}Turn-Based Data Exchanges
Exchange data between participants in a turn-based match without waiting for turns. Useful for trading items, sending gifts, or requesting actions.
Sending an Exchange
enum TurnExchangeError: Error {
case dataTooLarge
}
func sendExchange(
match: GKTurnBasedMatch,
to recipients: [GKTurnBasedParticipant],
data: Data
) async throws -> GKTurnBasedExchange {
guard data.count <= match.exchangeDataMaximumSize else {
throw TurnExchangeError.dataTooLarge
}
try await match.sendExchange(
to: recipients,
data: data,
localizableMessageKey: "EXCHANGE_REQUEST",
arguments: [],
timeout: GKExchangeTimeoutDefault
)
}Handling Exchange Events
extension GameManager: GKTurnBasedEventListener {
func player(
_ player: GKPlayer,
receivedExchangeRequest exchange: GKTurnBasedExchange,
for match: GKTurnBasedMatch
) {
// Process the exchange request and reply
let responseData = buildResponse(for: exchange)
Task {
try await exchange.reply(
withLocalizableMessageKey: "EXCHANGE_REPLY",
arguments: [],
data: responseData
)
}
}
func player(
_ player: GKPlayer,
receivedExchangeReplies replies: [GKTurnBasedExchangeReply],
forCompletedExchange exchange: GKTurnBasedExchange,
for match: GKTurnBasedMatch
) {
// All recipients replied. Merge exchange data into match state.
Task {
let mergedData = mergeExchangeData(exchange, replies: replies)
try await match.saveMergedMatch(
mergedData,
withResolvedExchanges: [exchange]
)
}
}
}Exchange Limits
exchangeDataMaximumSize: maximum size per exchange payloadexchangeMaxInitiatedExchangesPerPlayer: maximum concurrent outgoing exchanges
Ending a Turn-Based Match with Scores
Submit leaderboard scores and achievements when the match ends:
func endMatchWithScores(
match: GKTurnBasedMatch,
data: Data,
scores: [GKLeaderboardScore],
achievements: [GKAchievement]
) async throws {
for participant in match.participants {
participant.matchOutcome = determineOutcome(for: participant)
}
try await match.endMatchInTurn(
withMatch: data,
leaderboardScores: scores,
achievements: achievements
)
}Friend Management
Loading Friends
Requires the NSGKFriendListUsageDescription key in Info.plist:
func loadFriends() async throws -> [GKPlayer] {
let status = try await GKLocalPlayer.local.loadFriendsAuthorizationStatus()
guard status == .authorized else { return [] }
return try await GKLocalPlayer.local.loadFriends()
}Presenting Friend Request UI
func sendFriendRequest(from viewController: UIViewController) async {
guard !GKLocalPlayer.local.isPresentingFriendRequestViewController else {
return
}
try? await GKLocalPlayer.local.presentFriendRequestCreator(
from: viewController
)
}Loading Recent Players
func loadRecentPlayers() async throws -> [GKPlayer] {
try await GKLocalPlayer.local.loadRecentPlayers()
}Nearby Player Discovery
Find players on the same local network or via Bluetooth for local multiplayer:
func startBrowsingForNearbyPlayers() {
GKMatchmaker.shared().startBrowsingForNearbyPlayers { player, reachable in
if reachable {
self.addNearbyPlayer(player)
} else {
self.removeNearbyPlayer(player)
}
}
}
func stopBrowsing() {
GKMatchmaker.shared().stopBrowsingForNearbyPlayers()
}SharePlay Integration
Use GameKit's SharePlay bridge when a FaceTime or Messages SharePlay session should add players to a GameKit match. Keep full GroupActivities session design outside this GameKit reference.
func startSharePlayMatch() {
GKMatchmaker.shared().startGroupActivity { player in
// A player from the FaceTime call joined.
// Connect them to the game session.
self.addSharePlayPlayer(player)
}
}
func stopSharePlayMatch() {
GKMatchmaker.shared().stopGroupActivity()
}This creates a group activity on behalf of the player. Combine with GroupActivities framework for full SharePlay integration in your game UI.
Related skills
FAQ
Who is gamekit for?
Developers and software engineers working with gamekit patterns from the skill documentation.
When should I use gamekit?
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
Is gamekit safe to install?
Review the Security Audits panel on this page before installing in production.