├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── PrivacyInfo.xcprivacy
├── Sources
└── GameCenterKit
│ ├── PlayerEntry.swift
│ ├── GKPlayer+Extensions.swift
│ ├── GameCenterView.swift
│ ├── .gitignore
│ └── GameCenterKit.swift
├── Package.swift
├── .github
└── workflows
│ └── CI.yml
├── LICENSE
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm/config/registries.json
8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
9 | .netrc
10 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/PrivacyInfo.xcprivacy:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSPrivacyTracking
6 |
7 | NSPrivacyCollectedDataTypes
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Sources/GameCenterKit/PlayerEntry.swift:
--------------------------------------------------------------------------------
1 | // Created on 02.09.22
2 | // Copyright © 2022 Flavio Serrazes. All rights reserved.
3 |
4 | import SwiftUI
5 |
6 | public struct PlayerEntry: Identifiable {
7 | public let id: String
8 | public let displayName: String
9 | public let photo: Image?
10 | public let leaderboard: Leaderboard
11 |
12 | public struct Leaderboard {
13 | public let rank: Int
14 | public let score: Int
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/GameCenterKit/GKPlayer+Extensions.swift:
--------------------------------------------------------------------------------
1 | // Created on 27/08/2025
2 | // Copyright © 2025 Flavio Serrazes. All rights reserved.
3 |
4 | import GameKit
5 |
6 | extension GKPlayer {
7 | func loadPhoto(for size: GKPlayer.PhotoSize) async -> UIImage? {
8 | await withCheckedContinuation { continuation in
9 | self.loadPhoto(for: size) { photo, _ in
10 | continuation.resume(returning: photo)
11 | }
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.5
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "GameCenterKit",
8 | platforms: [
9 | .iOS(.v14)
10 | ],
11 | products: [
12 | .library(name: "GameCenterKit", targets: ["GameCenterKit"]),
13 | ],
14 | targets: [
15 | .target(name: "GameCenterKit", dependencies: [], path: "Sources")
16 | ]
17 | )
18 |
--------------------------------------------------------------------------------
/.github/workflows/CI.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | lint:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v3
14 | - name: SwiftLint
15 | uses: norio-nomura/action-swiftlint@3.2.0
16 |
17 | build:
18 | runs-on: macos-latest
19 | steps:
20 | - uses: actions/checkout@v3
21 | - name: Build
22 | run: xcodebuild clean build -scheme "GameCenterKit" -destination "platform=iOS Simulator,name=iPhone 14,OS=16.0" -sdk iphonesimulator -derivedDataPath .build-ci
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright © 2022 Flavio Serrazes.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/Sources/GameCenterKit/GameCenterView.swift:
--------------------------------------------------------------------------------
1 | // Created on 02.09.22
2 | // Copyright © 2022 Flavio Serrazes. All rights reserved.
3 |
4 | import SwiftUI
5 | import GameKit
6 |
7 | /// Presents the game center view provided by GameKit.
8 | public struct GameCenterView: UIViewControllerRepresentable {
9 | let viewController: GKGameCenterViewController
10 |
11 | public init(viewState: GKGameCenterViewControllerState = .default) {
12 | self.viewController = GKGameCenterViewController(state: viewState)
13 | }
14 |
15 | public func makeCoordinator() -> GameCenterCoordinator {
16 | return GameCenterCoordinator(self)
17 | }
18 |
19 | public func makeUIViewController(context: Context) -> GKGameCenterViewController {
20 | let gameCenterViewController = viewController
21 | gameCenterViewController.gameCenterDelegate = context.coordinator
22 | return gameCenterViewController
23 | }
24 |
25 | public func updateUIViewController(_ uiViewController: GKGameCenterViewController, context: Context) {
26 | return
27 | }
28 | }
29 |
30 | public class GameCenterCoordinator: NSObject, GKGameCenterControllerDelegate {
31 | let view: GameCenterView
32 |
33 | init(_ view: GameCenterView) {
34 | self.view = view
35 | }
36 |
37 | public func gameCenterViewControllerDidFinish(_ gameCenterViewController: GKGameCenterViewController) {
38 | gameCenterViewController.dismiss(animated: true)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Sources/GameCenterKit/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/swift,macos
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=swift,macos
3 |
4 | ### macOS ###
5 | # General
6 | .DS_Store
7 | .AppleDouble
8 | .LSOverride
9 |
10 | # Icon must end with two \r
11 | Icon
12 |
13 | # Thumbnails
14 | ._*
15 |
16 | # Files that might appear in the root of a volume
17 | .DocumentRevisions-V100
18 | .fseventsd
19 | .Spotlight-V100
20 | .TemporaryItems
21 | .Trashes
22 | .VolumeIcon.icns
23 | .com.apple.timemachine.donotpresent
24 |
25 | # Directories potentially created on remote AFP share
26 | .AppleDB
27 | .AppleDesktop
28 | Network Trash Folder
29 | Temporary Items
30 | .apdisk
31 |
32 | ### macOS Patch ###
33 | # iCloud generated files
34 | *.icloud
35 |
36 | ### Swift ###
37 | # Xcode
38 | #
39 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
40 |
41 | ## User settings
42 | xcuserdata/
43 |
44 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
45 | *.xcscmblueprint
46 | *.xccheckout
47 |
48 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
49 | build/
50 | DerivedData/
51 | *.moved-aside
52 | *.pbxuser
53 | !default.pbxuser
54 | *.mode1v3
55 | !default.mode1v3
56 | *.mode2v3
57 | !default.mode2v3
58 | *.perspectivev3
59 | !default.perspectivev3
60 |
61 | ## Obj-C/Swift specific
62 | *.hmap
63 |
64 | ## App packaging
65 | *.ipa
66 | *.dSYM.zip
67 | *.dSYM
68 |
69 | ## Playgrounds
70 | timeline.xctimeline
71 | playground.xcworkspace
72 |
73 | # Swift Package Manager
74 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
75 | # Packages/
76 | # Package.pins
77 | # Package.resolved
78 | # *.xcodeproj
79 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
80 | # hence it is not needed unless you have added a package configuration file to your project
81 | # .swiftpm
82 |
83 | .build/
84 |
85 | # CocoaPods
86 | # We recommend against adding the Pods directory to your .gitignore. However
87 | # you should judge for yourself, the pros and cons are mentioned at:
88 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
89 | # Pods/
90 | # Add this line if you want to avoid checking in source code from the Xcode workspace
91 | # *.xcworkspace
92 |
93 | # Carthage
94 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
95 | # Carthage/Checkouts
96 |
97 | Carthage/Build/
98 |
99 | # Accio dependency management
100 | Dependencies/
101 | .accio/
102 |
103 | # fastlane
104 | # It is recommended to not store the screenshots in the git repo.
105 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
106 | # For more information about the recommended setup visit:
107 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
108 |
109 | fastlane/report.xml
110 | fastlane/Preview.html
111 | fastlane/screenshots/**/*.png
112 | fastlane/test_output
113 |
114 | # Code Injection
115 | # After new code Injection tools there's a generated folder /iOSInjectionProject
116 | # https://github.com/johnno1962/injectionforxcode
117 |
118 | iOSInjectionProject/
119 |
120 | # End of https://www.toptal.com/developers/gitignore/api/swift,macos
121 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GameCenterKit
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | A Swift package for iOS that wraps GameKit with support for UIKit and SwiftUI.
13 |
14 | Enable players to interact with friends, compare leaderboard ranks and earn achievements.
15 |
16 | # Requirements
17 |
18 | The latest version of GameKitUI requires:
19 |
20 | - Swift 5+
21 | - iOS 14+
22 | - Xcode 13+
23 |
24 | # Installation
25 |
26 | ## Swift Package Manager
27 |
28 | Add the following to your package dependencies:
29 |
30 | ```swift
31 | .package(url: "https://github.com/fserrazes/GameCenterKit.git", branch: "main")
32 |
33 | ```
34 |
35 | # Usage
36 |
37 | ## Requirements
38 |
39 | 1. The local player must be authenticated on Game Center
40 | 2. Your app must have a leaderboard identifier defined in **App Store Connect**.
41 |
42 | ## Authenticate Player
43 |
44 | Authenticates the local player with Game Center (must be done before other actions).
45 |
46 | ### Completion-based API:
47 |
48 | ```swift
49 | GameCenterKit.shared.authenticate { isAuthenticated in
50 | if isAuthenticated {
51 | // Local player is authenticated
52 | } else {
53 | // Local player is not authenticated
54 | }
55 | ```
56 |
57 | ### Async/await API:
58 |
59 | ```swift
60 | let state = await GameCenterKit.shared.authenticate()
61 | ```
62 |
63 | ## Present Game Center UI
64 |
65 | There are 3 options:
66 |
67 | 1. Toggle AccessPointView
68 | 2. Present from a ViewController (UIKit)
69 | 3. Present from a View (SwiftUI)
70 |
71 | ### Toggle AccessPointView
72 |
73 | ```swift
74 | GameCenterKit.shared.toggleGameAccessPointView()
75 | ```
76 |
77 | ### UIKit Example
78 |
79 | ```swift
80 | do {
81 | try GameCenterKit.shared.showGameCenter(viewController: self)
82 | } catch {
83 | print(error)
84 | }
85 | ```
86 |
87 | ### SwiftUI Example
88 |
89 | ```swift
90 | import SwiftUI
91 | import GameCenterKit
92 |
93 | struct ContentView: View {
94 | @State var isGameCenterOpen: Bool = false
95 |
96 | var body: some View {
97 | VStack {
98 | Button("Open GameCenter View") {
99 | isGameCenterOpen = true
100 | }
101 | .buttonStyle(.borderedProminent)
102 | .padding()
103 | }
104 | .sheet(isPresented: $isGameCenterOpen) {
105 | GameCenterView()
106 | }
107 | }
108 | }
109 | ```
110 |
111 | ## Leaderboards
112 |
113 | Define your leaderboard identifier:
114 |
115 | ```swift
116 | let identifierId: String = "your-app-leaderboard-id"
117 | ```
118 |
119 | ### Retrieve Score
120 |
121 | The score earned by the local player (time scope defined is all time).
122 |
123 | ```swift
124 | if let score = try await GameCenterKit.shared.retrieveScore(identifier: identifierId) {
125 | print("best score: \(String(describing: score))")
126 | }
127 | ```
128 |
129 | ### Retrieve Rank
130 |
131 | The rank earned by the local player (time scope defined is all time).
132 |
133 | ```swift
134 | do {
135 | let (current, previous) = try await GameCenterKit.shared.retrieveRank(identifier: identifierId)
136 | print("current rank: \(String(describing: current))")
137 | print("previous rank: \(String(describing: previous))")
138 | } catch {
139 | print(error)
140 | }
141 | ```
142 |
143 | ### Retrieve Best Players
144 |
145 | The best players list and the number of total players (time scope defined is all time).
146 |
147 | ```swift
148 | let topPlayers = 10 // Number of top players (1–50)
149 |
150 | do {
151 | let (players, totalPlayers) = try await GameCenterKit.shared.retrieveBestPlayers(
152 | identifier: identifierId,
153 | topPlayers: topPlayers
154 | )
155 | print("total players: \(String(describing: totalPlayers))")
156 |
157 | for player in players {
158 | print("player: \(player.displayName)\t score: \(player.leaderboard.score)")
159 | }
160 | } catch {
161 | print(error)
162 | }
163 | ```
164 |
165 | ### Submit Score
166 |
167 | ```swift
168 | let score: Int = 10
169 |
170 | do {
171 | try await GameCenterKit.shared.submitScore(score: score, identifier: identifierId)
172 | } catch {
173 | print(error)
174 | }
175 | ```
176 | ## Achievements
177 |
178 | Define your achievement identifier:
179 |
180 | ```swift
181 | let achievementId: String = "your-app-achievement-id"
182 | ```
183 |
184 | ### Submit Achievement
185 |
186 | ```swift
187 | let percent = 10.0 // Progress value (0–100)
188 |
189 | do {
190 | try await GameCenterKit.shared.submitAchievement(identifier: achievementId, percent: percent)
191 | } catch {
192 | print(error)
193 | }
194 | ```
195 |
196 | ### Reset Achievements
197 |
198 | Resets the percentage completed for all of the player’s achievements.
199 |
200 | ```swift
201 | do {
202 | try await GameCenterKit.shared.resetAchievements()
203 | } catch {
204 | print(error)
205 | }
206 | ```
207 |
208 | ## Documentation
209 | + [Apple Documentation GameKit](https://developer.apple.com/documentation/gamekit/)
210 |
--------------------------------------------------------------------------------
/Sources/GameCenterKit/GameCenterKit.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import GameKit
3 | import OSLog
4 |
5 | public enum GameCenterError: Error {
6 | case notAuthenticated
7 | case emptyLeaderboad(String)
8 | case failure(Error)
9 | }
10 |
11 | public class GameCenterKit: NSObject, GKLocalPlayerListener {
12 | private let logger = Logger(subsystem: "com.serrazes.gamecenterkit", category: "GameCenter")
13 | private(set) var localPlayer = GKLocalPlayer.local
14 |
15 | public var isAuthenticated: Bool {
16 | return localPlayer.isAuthenticated
17 | }
18 |
19 | // Create as a Singleton to avoid more than one instance.
20 | public static var shared: GameCenterKit = GameCenterKit()
21 |
22 | private override init() {
23 | super.init()
24 | }
25 |
26 | /// Authenticates the local player with in Game Center if it's possible.
27 | ///
28 | /// If viewController is nil, Game Center authenticates the player and the player can start your game.
29 | /// Otherwise, present the view controller so the player can perform any additional actions to complete authentication.
30 | /// - Returns: Player autentication status.
31 | public func authenticate(completion: @escaping (Bool) -> Void) {
32 | guard !isAuthenticated else { completion(true); return }
33 |
34 | localPlayer.authenticateHandler = { [self] (viewController, error) in
35 | guard error == nil else {
36 | logger.error("Error on user authentication: \(error)")
37 | completion(false)
38 | return
39 | }
40 |
41 | GKAccessPoint.shared.isActive = false
42 |
43 | if self.localPlayer.isAuthenticated {
44 | localPlayer.register(self)
45 | }
46 | completion(isAuthenticated)
47 | }
48 | }
49 |
50 | /// Authenticates the local player with in Game Center if it's possible.
51 | ///
52 | /// If viewController is nil, Game Center authenticates the player and the player can start your game.
53 | /// Otherwise, present the view controller so the player can perform any additional actions to complete authentication.
54 | /// - Returns: Player autentication status.
55 | public func authenticate() async -> Bool {
56 | await withCheckedContinuation { continuation in
57 | var hasResumed = false
58 |
59 | self.authenticate { state in
60 | guard !hasResumed else { return }
61 | hasResumed = true
62 | continuation.resume(returning: state)
63 | }
64 | }
65 | }
66 |
67 | // MARK: - Leaderboad methods
68 |
69 | /// The score earned by the local player (time scope defined is all time).
70 | /// - Requires: The local user must be authenticated on Game Center.
71 | /// - Throws: GameCenterError.notAuthenticated: if local player is not authenticated.
72 | /// GameCenterError.failure: If an error occurred, It holds an error that describes the problem.
73 | /// - Parameter identifier: leaderboard id defined in App Store Connect.
74 | /// - Returns: Player score's, if a previous score was submitted.
75 | public func retrieveScore(identifier: String) async throws -> Int? {
76 | guard isAuthenticated else { throw GameCenterError.notAuthenticated }
77 |
78 | do {
79 | let leaderboards: [GKLeaderboard] = try await GKLeaderboard.loadLeaderboards(IDs: [identifier])
80 | if leaderboards.isEmpty {
81 | logger.warning("Leaderboad with \(identifier) is empty")
82 | throw GameCenterError.emptyLeaderboad(identifier)
83 | }
84 | let (localPlayerEntry, _) = try await leaderboards[0].loadEntries(for: [localPlayer], timeScope: .allTime)
85 | return localPlayerEntry?.score
86 | } catch {
87 | logger.error("Error to retrieve leaderboad \(identifier) score: \(error)")
88 | throw GameCenterError.failure(error)
89 | }
90 | }
91 |
92 | /// The rank earned by the local player (time scope defined is all time).
93 | ///
94 | /// The current and previous rankings are returned to measure the evolution of the player.
95 | /// - Requires: The local user must be authenticated on Game Center.
96 | /// - Throws: GameCenterError.notAuthenticated: if local player is not authenticated.
97 | /// GameCenterError.failure: If an error occurred, It holds an error that describes the problem.
98 | /// - Parameter identifier: leaderboard id defined in App Store Connect.
99 | /// - Returns: Current and previous player rank's, if any score was submitted.
100 | public func retrieveRank(identifier: String) async throws -> (current: Int?, previous: Int?) {
101 | guard isAuthenticated else { throw GameCenterError.notAuthenticated }
102 |
103 | var currentRank: Int? = nil
104 | var previousRank: Int? = nil
105 |
106 | do {
107 | let leaderboards: [GKLeaderboard] = try await GKLeaderboard.loadLeaderboards(IDs: [identifier])
108 | if leaderboards.isEmpty {
109 | logger.warning("Leaderboad with \(identifier) is empty")
110 | throw GameCenterError.emptyLeaderboad(identifier)
111 | }
112 | if let (currentEntry, _) = try? await leaderboards[0].loadEntries(for: [localPlayer], timeScope: .allTime) {
113 | currentRank = currentEntry?.rank
114 | }
115 | if let (previousEntry, _) = try? await leaderboards[0].loadPreviousOccurrence()?.loadEntries(for: [localPlayer], timeScope: .allTime) {
116 | previousRank = previousEntry?.rank
117 | }
118 | } catch {
119 | logger.error("Error to retrieve leadeboard \(identifier) rank: \(error)")
120 | throw GameCenterError.failure(error)
121 | }
122 | return (currentRank, previousRank)
123 | }
124 |
125 | /// The best players list and the number of total players (time scope defined is all time).
126 | /// - Requires: The local user must be authenticated on Game Center.
127 | /// - Throws: GameCenterError.notAuthenticated: if local player is not authenticated.
128 | /// GameCenterError.failure: If an error occurred, It holds an error that describes the problem.
129 | /// - Parameters:
130 | /// - identifier: leaderboard id defined in App Store Connect.
131 | /// - topPlayers: Specifies the number of top players (1 - 50) to use for getting the scores.
132 | /// - Returns: Ordered top player list and the number of total players.
133 | public func retrieveBestPlayers(identifier: String, topPlayers: Int = 5) async throws -> (player: [PlayerEntry], totalPlayers: Int) {
134 | guard isAuthenticated else { throw GameCenterError.notAuthenticated }
135 |
136 | let maxTopPlayers = min(topPlayers, 50)
137 | let range = NSRange(1...maxTopPlayers)
138 |
139 | do {
140 | let leaderboards = try await GKLeaderboard.loadLeaderboards(IDs: [identifier])
141 | guard let leaderboard = leaderboards.first else {
142 | logger.warning("Leaderboard with \(identifier) is empty")
143 | throw GameCenterError.emptyLeaderboad(identifier)
144 | }
145 |
146 | let (_, entries, totalPlayerCount) = try await leaderboard.loadEntries(
147 | for: .global,
148 | timeScope: .allTime,
149 | range: range
150 | )
151 |
152 | let players: [PlayerEntry] = try await withThrowingTaskGroup(of: PlayerEntry.self) { group in
153 | for entry in entries {
154 | group.addTask {
155 | let uiImage = await entry.player.loadPhoto(for: .small) ?? UIImage()
156 | let image = Image(uiImage: uiImage)
157 | return PlayerEntry(
158 | id: entry.player.gamePlayerID,
159 | displayName: entry.player.displayName,
160 | photo: image,
161 | leaderboard: PlayerEntry.Leaderboard(
162 | rank: entry.rank,
163 | score: entry.score
164 | )
165 | )
166 | }
167 | }
168 |
169 | var result = [PlayerEntry]()
170 | for try await player in group {
171 | result.append(player)
172 | }
173 | return result
174 | }
175 |
176 | return (players.sorted(by: { $0.leaderboard.score < $1.leaderboard.score }), totalPlayerCount)
177 |
178 | } catch {
179 | logger.error("Error to retrieve leaderboard \(identifier) best players: \(error.localizedDescription)")
180 | throw GameCenterError.failure(error)
181 | }
182 | }
183 |
184 | /// Reports a high score eligible for placement on a leaderboard.
185 | /// - Requires: The local user must be authenticated on Game Center.
186 | /// - Throws: GameCenterError.notAuthenticated: if local player is not authenticated.
187 | /// GameCenterError.failure: If an error occurred, It holds an error that describes the problem.
188 | /// - Parameters:
189 | /// - score: The score earned by the local player
190 | /// - identifier: leaderboard id defined in App Store Connect.
191 | public func submitScore(score: Int, identifier: String) async throws {
192 | guard isAuthenticated else { throw GameCenterError.notAuthenticated }
193 |
194 | do {
195 | try await GKLeaderboard.submitScore(score, context: 0, player: localPlayer, leaderboardIDs: [identifier])
196 | } catch {
197 | logger.error("Error to submit leaderboard \(identifier) scores: \(error)")
198 | throw GameCenterError.failure(error)
199 | }
200 | }
201 |
202 | // MARK: - Achievement methods
203 |
204 | /// Reports progress on an achievement, if it has not been completed already.
205 | /// - Requires: The local user must be authenticated on Game Center.
206 | /// - Throws: GameCenterError.notAuthenticated: if local player is not authenticated.
207 | /// GameCenterError.failure: If an error occurred, It holds an error that describes the problem.
208 | /// - Parameters:
209 | /// - identifier: achievement id defined in App Store Connect.
210 | /// - percent: A percentage value (0 - 100) stating how far the user has progressed on the achievement
211 | /// - banner: Define if achievement banner is shown with the local player progress.
212 | public func submitAchievement(identifier: String, percent: Double, showsCompletionBanner banner: Bool = true) async throws {
213 | guard isAuthenticated else { throw GameCenterError.notAuthenticated }
214 |
215 | do {
216 | let achievements = try await GKAchievement.loadAchievements()
217 |
218 | // Find an existing achievement, otherwise, create a new achievement.
219 | // If the achievement isn’t in the array, your game hasn’t reported any progress for this player yet, and the dashboard shows it in the locked state.
220 | var achievement = achievements.first(where: {$0.identifier == identifier})
221 | if achievement == nil {
222 | achievement = GKAchievement(identifier: identifier)
223 | }
224 |
225 | if let achievement = achievement, !achievement.isCompleted && achievement.percentComplete < percent {
226 | achievement.percentComplete = min(percent, 100)
227 | achievement.showsCompletionBanner = banner
228 |
229 | // Report the Player’s Progress
230 | try await GKAchievement.report([achievement])
231 | }
232 | } catch {
233 | logger.error("Error to submit achievement \(identifier) progress: \(error)")
234 | throw GameCenterError.failure(error)
235 | }
236 | }
237 |
238 | /// Resets the percentage completed for all of the player’s achievements.
239 | /// - Requires: The local user must be authenticated on Game Center.
240 | /// - Throws: GameCenterError.notAuthenticated: if local player is not authenticated.
241 | /// GameCenterError.failure: If an error occurred, It holds an error that describes the problem.
242 | public func resetAchievements() async throws {
243 | guard isAuthenticated else { throw GameCenterError.notAuthenticated }
244 |
245 | do {
246 | try await GKAchievement.resetAchievements()
247 | } catch {
248 | logger.error("Error to reset achievements: \(error)")
249 | throw GameCenterError.failure(error)
250 | }
251 | }
252 | }
253 |
254 | extension GameCenterKit: GKGameCenterControllerDelegate {
255 | public func toggleGameAccessPointView() {
256 | if isAuthenticated {
257 | GKAccessPoint.shared.isActive.toggle()
258 | }
259 | }
260 |
261 | /// Presents the game center view controller provided by GameKit.
262 | ///
263 | /// For SwiftUI projects consider using GameCenterView instead.
264 | /// - Requires: The local user must be authenticated on Game Center.
265 | /// - Throws: GameCenterError.notAuthenticated: if local player is not authenticated.
266 | /// - Parameters:
267 | /// - viewController: The view controller to present GameKit's view controller from.
268 | /// - viewState: The state in which to present the new view controller
269 | public func showGameCenter(viewController: UIViewController, viewState: GKGameCenterViewControllerState = .default) throws {
270 | guard isAuthenticated else { throw GameCenterError.notAuthenticated }
271 |
272 | let gameCenterViewController = GKGameCenterViewController(state: viewState)
273 | gameCenterViewController.gameCenterDelegate = self
274 | viewController.present(gameCenterViewController, animated: true)
275 | }
276 |
277 | public func gameCenterViewControllerDidFinish(_ gameCenterViewController: GKGameCenterViewController) {
278 | gameCenterViewController.dismiss(animated: true)
279 | }
280 | }
281 |
--------------------------------------------------------------------------------