bevy_ios_toolkit 0.2.1

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, over a polled C-ABI Swift bridge with a testable desktop fake.
Documentation
// Platform shim for bevy_ios_toolkit: haptics, safe-area inset, outbound links.
//
// Same contract as StoreKitBridge.swift: @_cdecl C-ABI called FROM Rust, polled
// getters (no callbacks into Rust). Everything that needs UIKit sits behind
// #if canImport(UIKit) with linking stubs otherwise, so the staticlib links on
// any target. Symbol prefix `platform_`.
//
// Shipped as this package's `Platform` product; link it from your app target.

import Foundation

// Tiny lock-wrapped int — Foundation has no public atomic Int; uncontended
// (one writer per value) — the same atomic discipline the other shims use.
final class AtomicInt {
    private let lock = NSLock()
    private var raw: Int
    init(_ v: Int) { raw = v }
    var value: Int {
        get { lock.lock(); defer { lock.unlock() }; return raw }
        set { lock.lock(); raw = newValue; lock.unlock() }
    }
}

#if canImport(UIKit)
import UIKit

// MARK: - Haptics

/// Prepared impact generators; index = haptic kind from Rust.
/// 0 light, 1 medium, 2 heavy, 3 rigid (sharp tick).
@MainActor private var hapticGenerators: [UIImpactFeedbackGenerator] = []

@_cdecl("platform_haptic")
public func platform_haptic(_ kind: Int32) {
    DispatchQueue.main.async {
        if hapticGenerators.isEmpty {
            let styles: [UIImpactFeedbackGenerator.FeedbackStyle] = [.light, .medium, .heavy, .rigid]
            hapticGenerators = styles.map { UIImpactFeedbackGenerator(style: $0) }
            hapticGenerators.forEach { $0.prepare() }
        }
        let i = Int(kind)
        guard hapticGenerators.indices.contains(i) else { return }
        hapticGenerators[i].impactOccurred()
        hapticGenerators[i].prepare()
    }
}

// MARK: - Safe area

/// Top safe-area inset in points, cached so Rust can poll it synchronously from
/// any thread. The first call may return 0; callers re-ask after the window
/// exists.
private let safeTopCache = AtomicInt(0)

@_cdecl("platform_safe_top")
public func platform_safe_top() -> Float {
    DispatchQueue.main.async {
        let inset = UIApplication.shared.connectedScenes
            .compactMap { $0 as? UIWindowScene }
            .flatMap { $0.windows }
            .first { $0.isKeyWindow }?.safeAreaInsets.top ?? 0
        safeTopCache.value = Int(inset.rounded())
    }
    return Float(safeTopCache.value)
}

// MARK: - Outbound links

@_cdecl("platform_open_url")
public func platform_open_url(_ url: UnsafePointer<CChar>) {
    let s = String(cString: url)
    DispatchQueue.main.async {
        guard let u = URL(string: s) else { return }
        UIApplication.shared.open(u)
    }
}

#else
// UIKit unavailable: linking stubs.

@_cdecl("platform_haptic") public func platform_haptic(_ kind: Int32) {}
@_cdecl("platform_safe_top") public func platform_safe_top() -> Float { 0 }
@_cdecl("platform_open_url") public func platform_open_url(_ url: UnsafePointer<CChar>) {}

#endif