bevy_ios_toolkit 0.4.0

Native iOS integrations for Bevy as ECS resources and messages: StoreKit IAP, Google AdMob ads, App Tracking Transparency, Game Center, review prompts, haptics and safe-area — each behind a feature and a polled C-ABI Swift bridge, with desktop fakes only where useful.
Documentation
// Game Center (GameKit) bridge for bevy_ios_toolkit (feature "gamekit").
//
// Same contract as the other bridges: @_cdecl C-ABI called FROM Rust; the one
// piece of polled state is the authentication result (it gates submission).
// Score/achievement submission is fire-and-forget; failures are logged here.
// Add this file to the app's Xcode target when the `gamekit` feature is enabled,
// enable the Game Center capability, and create the leaderboards/achievements in
// App Store Connect. Deployment floor iOS 26 — the modern GameKit API needs no
// availability guards.

import Foundation
import os

#if canImport(GameKit)
import GameKit
#if canImport(UIKit)
import UIKit
#endif

final class GameKitBridge: NSObject, @unchecked Sendable {
    static let shared = GameKitBridge()

    // 0 unknown, 1 authenticating, 2 authenticated, 3 unavailable.
    private let authState = OSAllocatedUnfairLock(initialState: Int32(0))

    func authenticate() {
        authState.withLock { $0 = 1 }
        GKLocalPlayer.local.authenticateHandler = { [weak self] viewController, error in
            guard let self else { return }
            #if canImport(UIKit)
            if let viewController {
                self.present(viewController)
                return
            }
            #endif
            if GKLocalPlayer.local.isAuthenticated {
                self.authState.withLock { $0 = 2 }
            } else {
                if let error {
                    NSLog("[gamekit] authentication failed: %@", String(describing: error))
                }
                self.authState.withLock { $0 = 3 }
            }
        }
    }

    func authStateValue() -> Int32 { authState.withLock { $0 } }

    func submitScore(_ score: Int, leaderboardID: String) {
        guard GKLocalPlayer.local.isAuthenticated else { return }
        GKLeaderboard.submitScore(
            score, context: 0, player: GKLocalPlayer.local, leaderboardIDs: [leaderboardID]
        ) { error in
            if let error {
                NSLog("[gamekit] score submit failed: %@", String(describing: error))
            }
        }
    }

    func reportAchievement(_ id: String, percent: Double) {
        guard GKLocalPlayer.local.isAuthenticated else { return }
        let achievement = GKAchievement(identifier: id)
        achievement.percentComplete = percent
        achievement.showsCompletionBanner = true
        GKAchievement.report([achievement]) { error in
            if let error {
                NSLog("[gamekit] achievement report failed: %@", String(describing: error))
            }
        }
    }

    func showDashboard() {
        #if canImport(UIKit)
        DispatchQueue.main.async {
            let vc = GKGameCenterViewController(state: .dashboard)
            vc.gameCenterDelegate = self
            self.present(vc)
        }
        #endif
    }

    #if canImport(UIKit)
    private func present(_ viewController: UIViewController) {
        let root = UIApplication.shared.connectedScenes
            .compactMap { $0 as? UIWindowScene }
            .flatMap { $0.windows }
            .first { $0.isKeyWindow }?
            .rootViewController
        root?.present(viewController, animated: true)
    }
    #endif
}

#if canImport(UIKit)
extension GameKitBridge: GKGameCenterControllerDelegate {
    func gameCenterViewControllerDidFinish(_ gameCenterViewController: GKGameCenterViewController) {
        gameCenterViewController.dismiss(animated: true)
    }
}
#endif

@_cdecl("gamekit_authenticate")
public func gamekit_authenticate() { GameKitBridge.shared.authenticate() }

@_cdecl("gamekit_auth_state")
public func gamekit_auth_state() -> Int32 { GameKitBridge.shared.authStateValue() }

@_cdecl("gamekit_submit_score")
public func gamekit_submit_score(_ leaderboardID: UnsafePointer<CChar>, _ score: Int64) {
    GameKitBridge.shared.submitScore(Int(score), leaderboardID: String(cString: leaderboardID))
}

@_cdecl("gamekit_report_achievement")
public func gamekit_report_achievement(_ achievementID: UnsafePointer<CChar>, _ percent: Double) {
    GameKitBridge.shared.reportAchievement(String(cString: achievementID), percent: percent)
}

@_cdecl("gamekit_show_dashboard")
public func gamekit_show_dashboard() { GameKitBridge.shared.showDashboard() }

#else
// GameKit unavailable: linking stubs. Reports unavailable, drops submissions.

@_cdecl("gamekit_authenticate") public func gamekit_authenticate() {}
@_cdecl("gamekit_auth_state") public func gamekit_auth_state() -> Int32 { 3 }
@_cdecl("gamekit_submit_score") public func gamekit_submit_score(_ leaderboardID: UnsafePointer<CChar>, _ score: Int64) {}
@_cdecl("gamekit_report_achievement") public func gamekit_report_achievement(_ achievementID: UnsafePointer<CChar>, _ percent: Double) {}
@_cdecl("gamekit_show_dashboard") public func gamekit_show_dashboard() {}

#endif