SwiftUI: GameCenter/GameKit Integration
Some Achievements, some leaderboards, some high scores!
SwiftUI: GameCenter/GameKit Integration
Some Achievements, some leaderboards, some high scores!

As you might know from one of my previous blog ***Egg Drift: A Twisted Suica Game. But With Motion. In All Directions***, I decided to make a little game for myself.
Now, I want it to be a little more exciting and make the user a little more motivated, so! I decided that I wanted to integrate in GameCenter/GameKit!
***GameKit*** allows the user to interact with friends, compare leaderboard ranks, earn achievements, and participate in multiplayer games. In case you haven't’ get a chance to try to implement it yourself yet, please let me share with you some of the basic usage here really quick!
Feel free to grab the little demo from my ***GitHub*** and let’s start!
Perquisite
Turn on Game Center and log in to it! On your device!

Acutally, I don’t even remember that I have ever done this for my actual device (probably because I play game so much). But I forgot to turn this on on my simulator (default to off) and I kept getting authentication related error that made me thought for a second that GameCenter just won’t work on simulator…It will. Just remember to turn it on! And log in of course!
Set Up
Add Entitlement
First thing first, let’s add ***Game Center Entitlement*** to our target.

One click! Done!
(MacOS Only) Enable Network Connections
If you are targeting MacOS, and you have Sandbox enabled, make sure to check those little check boxes for Incoming Connections and Outgoing Connections under Network.

Add Configuration File
If you have ever worked with StoreKit/In App Purchase, the idea here is really similar.
- We use a GameKit bundle file to configure achievements, activities, challenges, leaderboards, and leaderboard sets.
- When we are ready to deploy the configuration updates, we can sync them with App Store Connect.
- Or if we have already configured those features in App Store Connect, we can pull those existing remote configuration to the bundle file
Let’s start with a local one here.
File > New > File from Template, and choose GameKit bundle file.

Enter a name, choose a target, and click create.
We can then start add in game resources by clicking on that little + button.

If you want to pull from or push to the remote (App Store Connect), click the three-dots and you will see the options there.

I will add two achievements and one leaderboard here.

For achievements, there are couple parameters we can configure, but let’s just give those some points for now.

For leaderboard, the most important bit of the configuration is the score format.

Optionally, we can also set a range of allowable values in the Score Range fields that matches the score format.

We can also enable recurring to make the leaderboard a recurring leaderboard that ranks player scores based on a schedule can be a nice bonus to organize regular competitions or encourage players to score higher but we will leave that out for now!
Code Time!
Authenticate User
As we all know (hopefully, if you have ever played any game that has GameKit/GameCenter integrated), game center is like a sign in.
So!
The first thing we need to do is to authenticate the local player.
What do I meant by local?
In the GameKit framework, we use player objects to post scores, award achievements, build leaderboards, and start multiplayer games.
- The local player (
[GKLocalPlayer](https://developer.apple.com/documentation/gamekit/gklocalplayer)) represents the user playing your game and - other players (
[GKPlayer](https://developer.apple.com/documentation/gamekit/gkplayer)) may be friends, recent matches, or global players of your and other games.
Now, how do we authenticate our local user?
By setting the property [authenticateHandler](https://developer.apple.com/documentation/gamekit/gklocalplayer/authenticatehandler).
GKLocalPlayer.local.authenticateHandler = { viewController, error in
// .... present controller if there is any, show the error and more
}
I know…It sounds pretty counterintuitive, at least for me.
But, it is more like initializing a listener. With that being said,
- The best time to initialize this is on app launch
- If the user is already signed in the handler might not called.
- Similarly, it might also be called multiple times, for example, when user needs to perform some action, after user successfully signs in or cancelled
Also, Auth can success without the need of presenting a view controller. In that case, both the viewController and the error will be nil.
If you want to receive notifications after GameKit authenticates the local player, there is also a ***GKPlayerAuthenticationDidChangeNotificationName.***
By the way, if you are getting the following error in the handler callback and you were like I have never cancelled any requests (or there isn’t even a viewController ever passed in to the handler so that we can present it), the chances are the Game Center is off in Settings. Turn it on and we are good to go!
Error Domain=GKErrorDomain Code=6 “The requested operation could not be completed because local player has not been authenticated.” UserInfo={NSLocalizedDescription=The requested operation could not be completed because local player has not been authenticated., NSUnderlyingError=0x109675620 {Error Domain=GKErrorDomain Code=2 “The requested operation has been canceled or disabled by the user.” UserInfo={NSLocalizedDescription=The requested operation has been canceled or disabled by the user.}}}
Within the handler, in addition to
- present the view controller if there is any
- display the error if there is any
We will also want to check for restrictions to see whether if the user [isUnderage](https://developer.apple.com/documentation/gamekit/gklocalplayer/isunderage) , [isMultiplayerGamingRestricted](https://developer.apple.com/documentation/gamekit/gklocalplayer/ismultiplayergamingrestricted) or [isPersonalizedCommunicationRestricted](https://developer.apple.com/documentation/gamekit/gklocalplayer/ispersonalizedcommunicationrestricted).
final class GameCenterManager {
var showController: Bool = false {
didSet {
if !showController {
self.gameCenterController = nil
}
}
}
var gameCenterController: UIViewController? {
didSet {
if self.gameCenterController != nil {
self.showController = true
}
}
}
var error: Error?
init() {
self.refreshLocalPlayerStatus()
self.initializeLocalPlayer()
}
func initializeLocalPlayer() {
// to listen for changes: GKPlayerAuthenticationDidChangeNotificationName
localPlayer.authenticateHandler = { viewController, error in
self.refreshLocalPlayerStatus()
if let viewController = viewController {
self.gameCenterController = viewController
// Present the view controller so the player can sign in.
return
}
if error != nil {
// Player is not available
// Disable Game Center in the game.
self.error = error
return
}
}
}
private func refreshLocalPlayerStatus() {
self.isAuthenticated = localPlayer.isAuthenticated
self.isUnderage = localPlayer.isUnderage
self.isMultiplayerGamingRestricted =
localPlayer.isMultiplayerGamingRestricted
self.isPersonalizedCommunicationRestricted =
localPlayer.isPersonalizedCommunicationRestricted
}
}
Testing
Yap, before we move onto actually working with Game Center features, let’s confirm that our user can indeed signed into GameCenter correctly and check out how we make tests locally.
First thing first, turn on Debug Mode for GameKit configuration.
Choose the scheme you want to run, and Edit scheme.
Scroll down to GameKit Configuration, and check the little Enable Debug Mode checkbox.

Now, if we go to Debug > GameKit > Manage Game Progress, we can open the Game Progress Manager where we get to check out achievements, leaderboards, activities, send scores, reset all progresses, and etc. of a physical device. (You know what that means right? No simulators!)

Okay, time to run the app really quick just to see the code above to get executed!
If you have already signed into the game center from the Settings app, the chances are the callback handler will just have nil for both the view controller as well as the error! Hopefully!
Why Hopefully? Because local/sandbox/testing environment sometimes fails randomly! with the following error.
Error Domain=GKErrorDomain Code=15 “The requested operation could not be completed because this application is not recognized by Game Center.” UserInfo={NSLocalizedDescription=The requested operation could not be completed because this application is not recognized by Game Center.}
Just relaunch the app (maybe for couple times)… and it should be gone. If not, clean build folder, clear caches, and try again!
Achievements
Finally, time for some interesting (at least for me) stuff!
Reward the player with some points! When they achieve some achievements!
First thing first, let’s create an enum with the achievements we have added.
enum AchievementID {
static let firstPlay = "itsuki.enjoy.GameKitDemo.firstPlay"
static let tenPlays = "itsuki.enjoy.GameKitDemo.tenPlay"
static let allAchievements: [String] = [
firstPlay, tenPlays,
]
}
Why do we need those? Cannot we just fetch those from GameCenter? Not necessarily. We will see in 0.0001 seconds!
Get All Achievements
As simple as calling [loadAchievements()](https://developer.apple.com/documentation/gamekit/gkachievement/loadachievements(completionhandler:)) ? Not necessarily.
This function will ONLY return achievements that we previously reported the player making progress toward.
For other ones, we will need to initialize a new [GKAchievement](https://developer.apple.com/documentation/gamekit/gkachievement) by ourselves, using [init(identifier:)](https://developer.apple.com/documentation/gamekit/gkachievement/init(identifier:)) for local player, and [init(identifier:player:)](https://developer.apple.com/documentation/gamekit/gkachievement/init(identifier:player:)) for others.
func loadAchievements() {
Task {
do {
// Loads the achievements that you previously reported the player making progress toward.
var achievements = try await GKAchievement.loadAchievements()
let nonExisting = AchievementID.allAchievements.filter({
!achievements.map(\.identifier).contains($0)
})
achievements.append(
contentsOf: nonExisting.map({
GKAchievement(identifier: $0)
})
)
self.achievements = achievements
} catch (let error) {
self.error = error
}
}
}
With A Little More Detail!
If we take a look at the [GKAchievement](https://developer.apple.com/documentation/gamekit/gkachievement) , it has properties such as [identifier](https://developer.apple.com/documentation/gamekit/gkachievement/identifier) , [player](https://developer.apple.com/documentation/gamekit/gkachievement/player) , [percentComplete](https://developer.apple.com/documentation/gamekit/gkachievement/percentcomplete), but obviously (or hopefully), we don’t really want to show the [identifier](https://developer.apple.com/documentation/gamekit/gkachievement/identifier) directly to our user, right?
We need something more descriptive!
The [GKAchievementDescription](https://developer.apple.com/documentation/gamekit/gkachievementdescription) that we can get with [loadAchievementDescriptions()](https://developer.apple.com/documentation/gamekit/gkachievementdescription/loadachievementdescriptions(completionhandler:)).
func loadAchievementDetails() {
Task {
do {
self.achievementDescriptions =
try await GKAchievementDescription
.loadAchievementDescriptions()
} catch (let error) {
self.error = error
}
}
}
Note that this will load ALL, not just the ones we have reported.
With that being said, if you ever plan on calling this function, you will not need to hard code the achievement ids above as the [GKAchievementDescription](https://developer.apple.com/documentation/gamekit/gkachievementdescription) object will also contain the [identifier](https://developer.apple.com/documentation/gamekit/gkachievementdescription/identifier).
With some Art!
Let’s make our UI a little better, with some images.
func loadImageForAchievement(identifier: String) async -> Image? {
guard
let achievement = self.achievementDescriptions.first(where: {
$0.identifier == identifier
})
else {
return nil
}
guard let uiImage = try? await achievement.loadImage() else {
return nil
}
return Image(uiImage: uiImage)
}
Report Achievement Progress
Enough UI! Back to some logics!
To Report the player’s progress, or mark an achievement as complete, two steps.
- Set
[percentComplete](https://developer.apple.com/documentation/gamekit/gkachievement/percentcomplete) to the progress [report(_:)](https://developer.apple.com/documentation/gamekit/gkachievement/report(_:withcompletionhandler:)) the progress to Game Center
And when we set the [percentComplete](https://developer.apple.com/documentation/gamekit/gkachievement/percentcomplete) to 100%, that is basically telling the game center that the achievement is completed.
// showsCompletionBanner
// - A Boolean value that indicates whether GameKit displays a banner when the player completes the achievement.
// Set to false to disable system default banner and display our own UI
func completeAchievement(identifier: String, showCompletionBanner: Bool) {
self.reportAchievementProgress(
identifier: identifier,
progress: 100,
showCompletionBanner: showCompletionBanner
)
}
// When reporting a percentage greater than 0 and less than 100, the dashboard shows the achievement as in-progress.
// When you report that the player completes the achievement 100%, the dashboard shows the image for the achievement, and Game Center adds it to the player’s completed achievements.
func reportAchievementProgress(
identifier: String,
progress: Double,
showCompletionBanner: Bool
) {
guard (0...100).contains(progress) else {
return
}
let achievement =
self.achievements.first(where: {
$0.identifier == identifier
}) ?? .init(identifier: identifier)
achievement.percentComplete = progress
achievement.showsCompletionBanner = showCompletionBanner
Task {
do {
try await GKAchievement.report([achievement])
print("finish reporting")
if let index = self.achievements.firstIndex(where: {
$0.identifier == identifier
}) {
self.achievements[index] = achievement
} else {
self.achievements.append(achievement)
}
} catch (let error) {
self.error = error
}
}
}
The [showsCompletionBanner](https://developer.apple.com/documentation/gamekit/gkachievement/showscompletionbanner) suppose to control whether GameKit will display a banner notifying the player when they complete an achievement or not, but I can never get the banner to be displayed… Not on simulator, not on real device.
I think that it might have something to do with all those UI controllers such as [GKAchievementViewController](https://developer.apple.com/documentation/gamekit/gkachievementviewcontroller) being deprecated (I am running on 26.5), but not 100% sure…
Reset Achievements
Honestly speaking, I don’t know when will we ever want to expose a “reset Achievements” feature to our user, but it is really useful for debugging/testing purpose to clear all progress the local player makes toward all the achievements.
func resetAllAchievements() {
Task {
do {
try await GKAchievement.resetAchievements()
self.loadAchievements()
} catch (let error) {
self.error = error
}
}
}
By the way, you might realize that I am keep updating the **self**.achievements property manually in a lot of the functions even though the changes seem to be just a property change, for example, the percentComplete. Yap! GameKit is so old that of course, those classes/objects are not observable!
High Score (LeaderBoard)
If achievements are like my little sweet spots for myself, leaderboard is where I expose myself (I mean, my score) to the crowd!
With that being said, the functions/operations are similar, but different (of course…).
Achievement is per user, whereas a leaderboard is shared between friends, or even globally.
Load Leaderboards
Again, starting with a little enum containing the boards IDs we have.
enum LeaderboardID {
static let highScore = "itsuki.enjoy.GameKitDemo.highScore"
static let allLeaderboards: [String] = [
highScore
]
}
We can then fetch the leaderboards with the [loadLeaderboards(IDs:)](https://developer.apple.com/documentation/gamekit/gkleaderboard/loadleaderboards(ids:completionhandler:)) function.
func loadLeaderboards(
_ leaderboardIDs: [String] = LeaderboardID.allLeaderboards
) {
Task {
do {
// Loads leaderboards for the specified leaderboard IDs that Game Center uses.
// If leaderboardIDs is nil, this loads all classic and recurring leaderboards for this game.
let leaderboards = try await GKLeaderboard.loadLeaderboards(
IDs: leaderboardIDs
)
for board in leaderboards {
if let index = self.leaderboards.firstIndex(where: {
$0.baseLeaderboardID == board.baseLeaderboardID
}) {
self.leaderboards[index] = board
} else {
self.leaderboards.append(board)
}
self.loadScoresForLeaderboard(board.baseLeaderboardID)
}
} catch (let error) {
self.error = error
}
}
}
NOTE!
In the code doc, it mentions that if we pass in nil to the leaderboardIDs, we should be able to load all leaderboards.

Unfortunately, not true for me!
When I passed in nil, I got NOTHING!
Get And Set Scores
An empty leaderboard doesn’t look too good (Unfortunately, all leaderboards start at this state…).
So!
Time to submit some scores to it!
There are couple functions we can use here.
Either the class function [submitScore(_:context:player:leaderboardIDs:)](https://developer.apple.com/documentation/gamekit/gkleaderboard/submitscore(_:context:player:leaderboardids:completionhandler:)) on [GKLeaderboard](https://developer.apple.com/documentation/gamekit/gkleaderboard) to submit a score to one or more leaderboards, or the [submitScore(_:context:player:)](https://developer.apple.com/documentation/gamekit/gkleaderboard/submitscore(_:context:player:completionhandler:)) instance method to submit the score to a specific leaderboard.
func submitLeaderboardScore(_ identifier: String, score: Int) {
Task {
do {
// Loads leaderboards for the specified leaderboard IDs that Game Center uses.
// If leaderboardIDs is nil, this loads all classic and recurring leaderboards for this game.
try await GKLeaderboard.submitScore(
score,
context: 0,
player: self.localPlayer,
leaderboardIDs: [identifier]
)
// to refresh the score
self.loadScoresForLeaderboard(identifier)
} catch (let error) {
self.error = error
}
}
}
Yap, I leaked my function. We are writing that loadScoresForLeaderboard next to get the scores from leaderboards!
func loadScoresForLeaderboard(_ identifier: String) {
guard
let leaderboard = self.leaderboards.first(where: {
$0.baseLeaderboardID == identifier
})
else { return }
Task {
do {
// Loads leaderboards for the specified leaderboard IDs that Game Center uses.
// If leaderboardIDs is nil, this loads all classic and recurring leaderboards for this game.
let scores = try await leaderboard.loadEntries(
for: GKLeaderboard.PlayerScope.global,
timeScope: GKLeaderboard.TimeScope.allTime,
range: NSMakeRange(1, 100)
)
self.leaderboardScores[identifier] = .init(
localPlayerScore: scores.0,
allScores: scores.1,
totalPlayerCount: scores.2
)
} catch (let error) {
self.error = error
}
}
}
Code For Today
Above is all I have! Of course, there are a lot more we can do with GameKit.
- Adding Recurring Leaderboards,
- Creating challenges from leaderboards
- Creating real-time games
- Exchanging data between players in real-time games
- Adding voice chat to multiplayer games
But! Out of the scope for now!
Just to finish off our day, here is the code snippet in case you are too lazy downloading from my GitHub!
Main Logic
enum AchievementID {
static let firstPlay = "itsuki.enjoy.GameKitDemo.firstPlay"
static let tenPlays = "itsuki.enjoy.GameKitDemo.tenPlay"
static let allAchievements: [String] = [
firstPlay, tenPlays,
]
}
enum LeaderboardID {
static let highScore = "itsuki.enjoy.GameKitDemo.highScore"
static let allLeaderboards: [String] = [
highScore
]
}
struct LeaderboardScore {
var localPlayerScore: GKLeaderboard.Entry?
// The scores this method loads that match the playerScope, timeScope, and range parameters, including the local player’s score if it exists.
var allScores: [GKLeaderboard.Entry]
var totalPlayerCount: Int
var allScoresSorted: [GKLeaderboard.Entry] {
return self.allScores.sorted(by: { first, second in
first.rank < second.rank
})
}
}
@Observable
final class GameCenterManager {
private(set) var achievements: [GKAchievement] = []
private(set) var achievementDescriptions: [GKAchievementDescription] = []
private(set) var leaderboards: [GKLeaderboard] = []
private(set) var leaderboardScores: [String: LeaderboardScore] = [:]
var showController: Bool = false {
didSet {
if !showController {
self.gameCenterController = nil
}
}
}
var gameCenterController: UIViewController? {
didSet {
if self.gameCenterController != nil {
self.showController = true
}
}
}
var error: Error? {
didSet {
if let error {
print(error)
}
}
}
var isAuthenticated: Bool = false
var isUnderage: Bool = false
var isMultiplayerGamingRestricted: Bool = false
var isPersonalizedCommunicationRestricted: Bool = false
private var localPlayer: GKLocalPlayer {
// make sure to always return one that reflect the latest status
return GKLocalPlayer.local
}
init() {
self.refreshLocalPlayerStatus()
self.initializeLocalPlayer()
if self.isAuthenticated, !self.isUnderage {
self.loadAchievements()
self.loadAchievementDetails()
self.loadLeaderboards()
}
}
func initializeLocalPlayer() {
// to listen for changes: GKPlayerAuthenticationDidChangeNotificationName
localPlayer.authenticateHandler = { viewController, error in
self.refreshLocalPlayerStatus()
if let viewController = viewController {
self.gameCenterController = viewController
// Present the view controller so the player can sign in.
return
}
if error != nil {
// Player is not available
// Disable Game Center in the game.
self.error = error
return
}
if self.isAuthenticated, !self.isUnderage {
self.loadAchievements()
self.loadAchievementDetails()
self.loadLeaderboards()
}
}
}
private func refreshLocalPlayerStatus() {
self.isAuthenticated = localPlayer.isAuthenticated
self.isUnderage = localPlayer.isUnderage
self.isMultiplayerGamingRestricted =
localPlayer.isMultiplayerGamingRestricted
self.isPersonalizedCommunicationRestricted =
localPlayer.isPersonalizedCommunicationRestricted
}
}
// MARK: - Achievements
extension GameCenterManager {
func loadAchievements() {
Task {
do {
// Loads the achievements that you previously reported the player making progress toward.
var achievements = try await GKAchievement.loadAchievements()
let nonExisting = AchievementID.allAchievements.filter({
!achievements.map(\.identifier).contains($0)
})
achievements.append(
contentsOf: nonExisting.map({
GKAchievement(identifier: $0)
})
)
self.achievements = achievements
} catch (let error) {
self.error = error
}
}
}
func loadAchievementDetails() {
Task {
do {
self.achievementDescriptions =
try await GKAchievementDescription
.loadAchievementDescriptions()
} catch (let error) {
self.error = error
}
}
}
func loadImageForAchievement(identifier: String) async -> Image? {
guard
let achievement = self.achievementDescriptions.first(where: {
$0.identifier == identifier
})
else {
return nil
}
guard let uiImage = try? await achievement.loadImage() else {
return nil
}
return Image(uiImage: uiImage)
}
// showsCompletionBanner
// - A Boolean value that indicates whether GameKit displays a banner when the player completes the achievement.
// Set to false to disable system default banner and display our own UI
func completeAchievement(identifier: String, showCompletionBanner: Bool) {
self.reportAchievementProgress(
identifier: identifier,
progress: 100,
showCompletionBanner: showCompletionBanner
)
}
// When reporting a percentage greater than 0 and less than 100, the dashboard shows the achievement as in-progress.
// When you report that the player completes the achievement 100%, the dashboard shows the image for the achievement, and Game Center adds it to the player’s completed achievements.
func reportAchievementProgress(
identifier: String,
progress: Double,
showCompletionBanner: Bool
) {
guard (0...100).contains(progress) else {
return
}
let achievement =
self.achievements.first(where: {
$0.identifier == identifier
}) ?? .init(identifier: identifier)
achievement.percentComplete = progress
achievement.showsCompletionBanner = showCompletionBanner
Task {
do {
try await GKAchievement.report([achievement])
print("finish reporting")
if let index = self.achievements.firstIndex(where: {
$0.identifier == identifier
}) {
self.achievements[index] = achievement
} else {
self.achievements.append(achievement)
}
} catch (let error) {
self.error = error
}
}
}
func resetAllAchievements() {
Task {
do {
try await GKAchievement.resetAchievements()
self.loadAchievements()
} catch (let error) {
self.error = error
}
}
}
}
// MARK: - Leaderboards
extension GameCenterManager {
func loadLeaderboards(
_ leaderboardIDs: [String] = LeaderboardID.allLeaderboards
) {
Task {
do {
// Loads leaderboards for the specified leaderboard IDs that Game Center uses.
// If leaderboardIDs is nil, this loads all classic and recurring leaderboards for this game.
let leaderboards = try await GKLeaderboard.loadLeaderboards(
IDs: leaderboardIDs
)
for board in leaderboards {
if let index = self.leaderboards.firstIndex(where: {
$0.baseLeaderboardID == board.baseLeaderboardID
}) {
self.leaderboards[index] = board
} else {
self.leaderboards.append(board)
}
self.loadScoresForLeaderboard(board.baseLeaderboardID)
}
} catch (let error) {
self.error = error
}
}
}
func submitLeaderboardScore(_ identifier: String, score: Int) {
Task {
do {
// Loads leaderboards for the specified leaderboard IDs that Game Center uses.
// If leaderboardIDs is nil, this loads all classic and recurring leaderboards for this game.
try await GKLeaderboard.submitScore(
score,
context: 0,
player: self.localPlayer,
leaderboardIDs: [identifier]
)
// to refresh the score
self.loadScoresForLeaderboard(identifier)
} catch (let error) {
self.error = error
}
}
}
func loadScoresForLeaderboard(_ identifier: String) {
guard
let leaderboard = self.leaderboards.first(where: {
$0.baseLeaderboardID == identifier
})
else { return }
Task {
do {
// Loads leaderboards for the specified leaderboard IDs that Game Center uses.
// If leaderboardIDs is nil, this loads all classic and recurring leaderboards for this game.
let scores = try await leaderboard.loadEntries(
for: GKLeaderboard.PlayerScope.global,
timeScope: GKLeaderboard.TimeScope.allTime,
range: NSMakeRange(1, 100)
)
self.leaderboardScores[identifier] = .init(
localPlayerScore: scores.0,
allScores: scores.1,
totalPlayerCount: scores.2
)
} catch (let error) {
self.error = error
}
}
}
}
A Little UI
Just for testing purpose!
struct ContentView: View {
@State private var gameCenterManager = GameCenterManager()
var body: some View {
NavigationStack {
Group {
if self.gameCenterManager.isAuthenticated {
List {
Section("Achievements") {
ForEach(
gameCenterManager.achievementDescriptions,
id: \.identifier
) { description in
let achievement: GKAchievement? = self
.gameCenterManager.achievements.first(
where: {
$0.identifier
== description.identifier
})
HStack {
Text(description.title)
.frame(
maxWidth: .infinity,
alignment: .leading
)
if let achievement {
CircularProgressView(
progress: achievement
.percentComplete
)
}
}
.contextMenu {
if description
.identifier == AchievementID.firstPlay,
achievement?
.isCompleted == false
{
Button(
action: {
self.gameCenterManager
.completeAchievement(
identifier: description
.identifier,
showCompletionBanner:
true
)
},
label: {
Text("Complete!")
}
)
} else {
Button(
action: {
self.gameCenterManager
.reportAchievementProgress(
identifier: description
.identifier,
progress: (achievement?
.percentComplete
?? 0) + 10,
showCompletionBanner:
true
)
},
label: {
Text("Make Progress!")
}
)
}
}
}
}
Section {
Button(
action: {
self.gameCenterManager
.resetAllAchievements()
},
label: {
Text("Reset Achievements")
.padding(.vertical, 8)
.font(.headline)
}
)
.buttonSizing(.flexible)
.buttonStyle(.glassProminent)
.listRowBackground(Color.clear)
.listRowInsets(.horizontal, 0)
}
Section("Leaderboards") {
ForEach(
self.gameCenterManager.leaderboards,
id: \.baseLeaderboardID
) { leaderboard in
let score = self.gameCenterManager
.leaderboardScores[
leaderboard.baseLeaderboardID
]
NavigationLink(
destination: {
LeaderboardView(
leaderboard: leaderboard,
score: score
)
},
label: {
VStack(alignment: .leading) {
Text(
leaderboard.title
?? "Unknown Board"
)
if let score {
Text(
"\(score.allScores.count) scores by \(score.totalPlayerCount) players."
)
.foregroundStyle(.secondary)
}
}
}
)
.contextMenu {
Button(
action: {
self.gameCenterManager
.submitLeaderboardScore(
leaderboard
.baseLeaderboardID,
score: (1...100)
.randomElement() ?? 1
)
},
label: {
Text("Submit Random Score!")
}
)
}
}
}
}
} else {
Button(
action: {
gameCenterManager.initializeLocalPlayer()
},
label: {
Text("Sign In to Game Center")
.padding(.vertical, 8)
.font(.headline)
}
)
.buttonSizing(.flexible)
.buttonStyle(.glassProminent)
.padding()
}
}
.navigationTitle("Game Center")
.sheet(
isPresented: $gameCenterManager.showController,
content: {
if let controller = gameCenterManager.gameCenterController {
GameCenterAuthView(controller: controller)
}
}
)
}
}
}
private struct LeaderboardView: View {
var leaderboard: GKLeaderboard
var score: LeaderboardScore?
var body: some View {
List {
Section("Top Scores") {
if let score, score.allScores.count > 0 {
ForEach(score.allScoresSorted.enumerated(), id: \.offset) {
_,
entry in
Text(
"\(entry.formattedScore) by \(entry.player.displayName)"
)
}
} else {
Text("No scores yet")
.foregroundStyle(.secondary)
}
}
}
.navigationTitle(leaderboard.title ?? "Unknown Board")
.navigationBarTitleDisplayMode(.large)
}
}
private struct CircularProgressView: View {
let progress: Double
var body: some View {
ZStack {
Circle()
.stroke(.secondary.opacity(0.8), style: .init(lineWidth: 4))
.fill(.clear)
Circle()
.trim(from: 0.0, to: progress / 100)
.stroke(.link, style: .init(lineWidth: 4))
.fill(.clear)
}
.frame(width: 36)
.overlay(content: {
Text(
"\(progress.formatted(.number.precision(.fractionLength(0))))%"
)
.fixedSize()
.font(.caption.bold())
})
}
}
struct GameCenterAuthView: UIViewControllerRepresentable {
var controller: UIViewController
func makeUIViewController(context: Context) -> UIViewController {
return controller
}
func updateUIViewController(
_ uiViewController: UIViewController,
context: Context
) {}
}
By the way, you might think that why don’t we just display the Game Center dashboard directly? Unfortunately, all the related APIs such as [GKGameCenterViewController](https://developer.apple.com/documentation/gamekit/gkgamecenterviewcontroller) are deprecated!
So!
We will either have to create all UIs ourselves , or use an access point instead.
Thank you for reading!
That’s it for this article!
Honestly speaking, I think that all apps should have GameCenter (or something similar) integrated, regardless of whether it is a game or not, because I love achievements! (I don’t know if Apple allows non-game app to have Game Center related implementations though…)
Anyway!
Happy gaming!
메타데이터
- post_id
- bee9a7b5db97
- slug
- swiftui-gamecenter-gamekit-integration-bee9a7b5db97
- url
- https://medium.com/@itsuki.enjoy/swiftui-gamecenter-gamekit-integration-bee9a7b5db97
- canonical_url
- https://medium.com/@itsuki.enjoy/swiftui-gamecenter-gamekit-integration-bee9a7b5db97
- author_url
- https://medium.com/@itsuki.enjoy
- status
- ok
- fetched_at
- 2026-06-09 15:37:30