Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.
71
89%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Advisory
Suggest reviewing before use
Advanced GameKit patterns for voice chat, saved games, custom matchmaking UI, leaderboard images, challenge handling, and rule-based matchmaking.
Verify the local player on a backend server using a cryptographic signature
from fetchItems(forIdentityVerificationSignature:):
func verifyPlayerOnServer() async throws {
let (publicKeyURL, signature, salt, timestamp) =
try await GKLocalPlayer.local.fetchItems(forIdentityVerificationSignature: nil)
// Send publicKeyURL, signature, salt, timestamp, playerID, and bundleID
// to your server for verification against Apple's public key.
}The server fetches the public key from the URL Apple provides, then verifies the signature over the concatenation of: playerID + bundleID + timestamp + salt.
GameKit provides built-in voice chat between players in a real-time match
through GKVoiceChat. Each named channel supports independent volume and mute
controls.
Add NSMicrophoneUsageDescription to Info.plist and configure an audio session
before starting voice chat.
import AVFoundation
func configureAudioSession() throws {
let session = AVAudioSession.sharedInstance()
try session.setCategory(.playAndRecord, mode: .gameChat,
options: [.defaultToSpeaker, .allowBluetooth])
try session.setActive(true)
}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.isActive = true
voiceChat.volume = 0.8
voiceChat.start()
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
}
}
}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
}func mutePlayer(_ player: GKPlayer, in voiceChat: GKVoiceChat) {
voiceChat.setPlayer(player, muted: true)
}
func unmutePlayer(_ player: GKPlayer, in voiceChat: GKVoiceChat) {
voiceChat.setPlayer(player, muted: false)
}func stopVoiceChat(_ voiceChat: GKVoiceChat) {
voiceChat.isActive = false
voiceChat.stop()
}GameKit stores game data in the player's iCloud account, accessible from any
device signed in to the same Game Center account. Saved games are managed
through GKLocalPlayer and represented by GKSavedGame.
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)
}Multiple saves with the same name create separate entries. The most recent save is returned first from fetch calls.
func fetchSavedGames() async throws -> [GKSavedGame] {
try await GKLocalPlayer.local.fetchSavedGames() ?? []
}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.
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
)
}Implement GKSavedGameListener 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)func deleteSavedGame(name: String) async throws {
try await GKLocalPlayer.local.deleteSavedGames(withName: name)
}Build a custom interface for finding players instead of using
GKMatchmakerViewController. Use GKMatchmaker directly.
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()
}
}func addPlayers(to match: GKMatch, request: GKMatchRequest) async throws {
try await GKMatchmaker.shared().addPlayers(
to: match,
matchRequest: request
)
}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)
}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 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 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() ?? []
}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
}Use context to store additional metadata with a score, such as the level
where the score was achieved:
try await GKLeaderboard.submitScore(
score,
context: UInt64(levelID),
player: GKLocalPlayer.local,
leaderboardIDs: ["com.mygame.scores"]
)Players can challenge friends to beat their scores or complete achievements.
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)
}
if let vc { present(vc, animated: true) }
}func loadChallengeableFriends() async throws -> [GKPlayer] {
try await GKLocalPlayer.local.loadChallengableFriends() ?? []
}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) ?? []
}GKAccessPoint.shared.triggerForChallenges { }Configure matchmaking rules in App Store Connect to refine player matching based on game-specific criteria. Rules evaluate player properties to determine compatible matches.
func findRuleBasedMatch(skill: Int, region: String) async throws -> GKMatch {
let request = GKMatchRequest()
request.minPlayers = 2
request.maxPlayers = 4
request.queueName = "competitive" // Matches the queue in App Store Connect
request.properties = [
"skill": skill,
"region": region
]
let match = try await GKMatchmaker.shared().findMatch(for: request)
GKMatchmaker.shared().finishMatchmaking(for: match)
return match
}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)")
}
}Set properties for invited recipients:
func inviteWithRules(
players: [GKPlayer],
properties: [String: Any]
) async throws -> GKMatch {
let request = GKMatchRequest()
request.minPlayers = 2
request.maxPlayers = 4
request.queueName = "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.
Use player groups and attributes for simple matchmaking without rules.
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.
Use a bitmask to specify the role a player wants. GameKit ensures all bits are covered across participants:
// Define roles as bitmask values
let roleAttacker: UInt32 = 0x0001
let roleDefender: UInt32 = 0x0002
let roleSupport: UInt32 = 0x0004
let request = GKMatchRequest()
request.minPlayers = 3
request.maxPlayers = 3
request.playerAttributes = roleAttacker // This player wants the attacker roleGameKit ORs the attributes of all matched players and verifies all bits are filled. This guarantees each role is represented.
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
}func findPlayersWithRules() async throws -> GKMatchedPlayers {
let request = GKMatchRequest()
request.minPlayers = 2
request.maxPlayers = 8
request.queueName = "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
}Exchange data between participants in a turn-based match without waiting for turns. Useful for trading items, sending gifts, or requesting actions.
func sendExchange(
match: GKTurnBasedMatch,
to recipients: [GKTurnBasedParticipant],
data: Data
) async throws -> GKTurnBasedExchange {
try await match.sendExchange(
to: recipients,
data: data,
localizableMessageKey: "EXCHANGE_REQUEST",
arguments: [],
timeout: GKExchangeTimeoutDefault
)
}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]
)
}
}
}exchangeDataMaximumSize: maximum size per exchange payloadexchangeMaxInitiatedExchangesPerPlayer: maximum concurrent outgoing exchangesSubmit 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
)
}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() ?? []
}func sendFriendRequest(from viewController: UIViewController) async {
guard !GKLocalPlayer.local.isPresentingFriendRequestViewController else {
return
}
try? await GKLocalPlayer.local.presentFriendRequestCreator(
from: viewController
)
}func loadRecentPlayers() async throws -> [GKPlayer] {
try await GKLocalPlayer.local.loadRecentPlayers() ?? []
}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()
}Start a multiplayer game through FaceTime using SharePlay:
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.
skills
accessorysetupkit
references
activitykit
references
adattributionkit
references
alarmkit
references
app-clips
app-intents
references
app-store-optimization
app-store-review
apple-on-device-ai
appmigrationkit
references
audioaccessorykit
references
authentication
references
avkit
references
background-processing
references
browserenginekit
references
callkit
references
carplay
references
cloudkit
references
contacts-framework
references
core-bluetooth
references
core-data
core-motion
references
core-nfc
references
coreml
references
cryptokit
references
cryptotokenkit
references
debugging-instruments
device-integrity
references
dockkit
references
energykit
references
eventkit
references
financekit
references
focus-engine
gamekit
references
healthkit
references
homekit
references
ios-accessibility
ios-localization
ios-networking
ios-simulator
references
mapkit
metrickit
references
musickit
references
natural-language
references
paperkit
references
passkit
references
pdfkit
references
pencilkit
references
permissionkit
references
photokit
push-notifications
realitykit
references
relevancekit
references
scenekit
references
sensorkit
references
speech-recognition
spritekit
references
storekit
swift-api-design-guidelines
swift-architecture
swift-charts
references
swift-codable
swift-concurrency
swift-formatstyle
swift-language
swift-security
references
swift-testing
swiftdata
swiftlint
swiftui-animation
swiftui-gestures
references
swiftui-layout-components
swiftui-liquid-glass
references
swiftui-patterns
swiftui-performance
swiftui-uikit-interop
swiftui-webkit
tabletopkit
references
tipkit
references
vision-framework
weatherkit
references
widgetkit
references