├── .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 | --------------------------------------------------------------------------------