SwiftUIGamepad

SwiftUIGamepad 可以轻松地将 Gamepad 支持添加到任何 SwiftUI View

支持

如果您觉得 SwiftUIGamepad 有用,并希望帮助支持其持续开发和维护,请考虑进行小额捐赠,尤其是在您将其用于商业产品时。

Buy Me A Coffee

通过像您这样的贡献者的支持,我可以继续免费构建、发布和维护高质量、文档完善的 Swift 包,例如 SwiftUIGamepad

安装

Swift Package Manager (Xcode 11 及以上)

  1. 在 Xcode 中,选择File > Add Package Dependency… 菜单项。
  2. 在对话框中粘贴 https://github.com/Appracatappra/SwiftUIGamepad.git
  3. 按照 Xcode 的说明完成安装。

为什么不选择 CocoaPods、Carthage 或其他?

支持多个依赖管理器会使库的维护呈指数级复杂和耗时。

由于 Swift Package Manager 与 Xcode 11 (及更高版本) 集成,因此它是进一步支持的最简单选择。

概述

使用 SwiftUIGamepad,您可以轻松地将 Gamepad 支持添加到任何基于 SwiftUI 的应用程序。 SwiftUIGamepad 提供了一组内置的 Gamepad 图像,并支持基于 Gamepad 在任何 View 中使用方式显示帮助叠加层。 SwiftUIGamepad 还支持 Gamepad 连接/断开与设备的连接,并在应用程序需要 Gamepad 才能工作时显示叠加层。

启用 Gamepad 支持

在 Swift 应用程序中使用 Gamepad 之前,您需要启用支持。 在 Xcode 中,选择您的应用程序的Project > Signing & Capabilities > + Capability 并添加 Game Controllers

启用后,从复选框列表中选择您要支持的 Gamepad 类型。

如果您启用了 Micro Gamepad,它可能会阻止 Apple TV 识别 Extended Gamepad 已连接。 如果您在 tvOS 应用程序中使用此包,我建议禁用它。

响应 Gamepad 事件

通过将 SwiftUIGamepad 导入 SwiftUI View 定义中,它将获得几个新的事件,您可以响应这些事件,因为用户与游戏手柄交互并将其连接到或断开与运行该应用程序的设备的连接。

为了使 SwiftUIGamepad 正常工作,您必须GamepadManager 事件添加到应用程序的 .onChange 事件中。 有关完整详细信息,请参见下面的 在哪里设置样式更改和连接 Gamepad 支持 文档。

例如,让我们看一下 About View 的 body

@State var showGamepadHelp:Bool = false
@State var isGamepadConnected:Bool = false
    
 var body: some View {
    mainContents()
    .onAppear {
        connectGamepad(viewID: "About", handler: { controller, gamepadInfo in
            isGamepadConnected = true
            buttonMenuUsage(viewID: "About", "Return to the **Cover Page Menu**.")
            buttonAUsage(viewID: "About", "Show or hide **Gamepad Help**.")
        })
    }
    .onDisappear {
        disconnectGamepad(viewID: "About")
    }
    .onRotate { newOrientation in
        Execute.onMain {
            orientation = newOrientation
        }
    }
    .onGampadAppBecomingActive(viewID: "About") {
        reconnectGamepad()
    }
    .onGamepadDisconnected(viewID: "About") { controller, gamepadInfo in
        isGamepadConnected = false
    }
    .onGamepadButtonMenu(viewID: "About") { isPressed in
        if isPressed {
            // Return to main menu ...
        }
    }
    .onGamepadButtonA(viewID: "About") { isPressed in
        if isPressed {
            showGamepadHelp = !showGamepadHelp
        }
    }
    .onGamepadLeftShoulder(viewID: "About") { isPressed, value in
        if isPressed {
            // Return to main menu ...
        }
    }
}

接下来的章节将详细介绍此代码。

请求 View Gamepad 访问

首先要注意的是,当 View 出现时,我们要求它请求访问连接到运行该应用程序的设备的任何游戏手柄

.onAppear {
    connectGamepad(viewID: "About", handler: { controller, gamepadInfo in
        isGamepadConnected = true
        buttonMenuUsage(viewID: "About", "Return to the **Cover Page Menu**.")
        buttonAUsage(viewID: "About", "Show or hide **Gamepad Help**.")
    })
}

在所有 SwiftUIGamepad 控件和事件中使用的 viewID 属性非常重要,并且对于应用程序中的每个视图必须是唯一的。 未能提供唯一的 viewID 将导致应用程序中出现不可预测的行为,并且可能导致错误的 View 响应错误的事件或根本无法响应游戏手柄事件。

此外,同一 View 上的所有 SwiftUIGamepad 控件必须使用相同的 viewID 属性。

如果建立了游戏手柄访问权限,我们将设置一个状态变量,并为 View 响应的游戏手柄控件提供用户帮助描述。

您可以使用 GamepadHelpOverlay 视图在您的应用程序中显示标准化帮助,如下所示

if showGamepadHelp {
    GamepadHelpOverlay()
}

您使用 buttonMenuUsagebuttonAUsage 等函数添加的条目将在此处显示。

此外,如果您的应用程序需要游戏手柄才能工作,则可以使用标准化的 GamepadRequiredOverlay 来请求用户在继续之前连接游戏手柄

if !isGamepadConnected {
    GamepadRequiredOverlay()
}

Gamepad 连接事件

您的 View 应随时响应游戏手柄连接或断开与运行该应用程序的设备的连接。 使用 onGampadAppBecomingActive 事件

.onGampadAppBecomingActive(viewID: "About") {
    reconnectGamepad()
}

调用 reconnectGamepad 函数可确保在应用程序从后台唤醒时再次调用 connectGamepad 事件。

要处理游戏手柄与应用程序断开连接,请使用

.onGamepadDisconnected(viewID: "About") { controller, gamepadInfo in
    isGamepadConnected = false
}

通过 Gamepad 响应用户输入

当用户与游戏手柄的控件(例如按下 A 按钮或拉动右侧扳机)交互时,View 可以响应几种不同类型的事件。 例如

.onGamepadButtonMenu(viewID: "About") { isPressed in
    if isPressed {
        // Return to main menu ...
    }
}
.onGamepadButtonA(viewID: "About") { isPressed in
    if isPressed {
        showGamepadHelp = !showGamepadHelp
    }
}
.onGamepadLeftShoulder(viewID: "About") { isPressed, value in
    if isPressed {
        // Return to main menu ...
    }
}

在我们的示例 About View 中,按下游戏手柄的左肩按钮菜单按钮会将用户返回到应用程序的主菜单。 按下 A 按钮 将显示或隐藏 Gamepad Help Overlay

有关游戏手柄 View Events 及其使用的完整列表,请参阅 Extended Modules > SwiftUI > Extended Protocols > View in the DocC Documentation。

显示 Gamepad 控制提示

GamepadControlTip 控件可用于向应用程序用户显示快速帮助。 例如

GamepadControlTip(iconName: GamepadManager.gamepadOne.gampadInfo.buttonAImage, title: "Help", scale: ScreenMetrics.controlButtonScale, enabledColor: Color("HUDForeground"))

在此示例中,GamepadManager.gamepadOne.gampadInfo.buttonAImage 将根据用户连接到设备的 Gamepad 类型(PS4、PS5、Xbox 等)获取 A 按钮 的正确图像。

有关您可以获得的有关连接的游戏手柄的完整信息,请参阅 GamePadInfo 类。

释放 Gamepad 访问

View 完成访问游戏手柄时,它需要使用以下代码释放其访问权限

.onDisappear {
    disconnectGamepad(viewID: "About")
}

嵌入式声音

SwiftUIGamepad 中嵌入了两个默认声音,可在 GamepadMenuView 控件中使用

两种声音均来自 Freeound.org,采用 Creative Commons 0 许可。

嵌入式图像

SwiftUIGamepad 中嵌入了几个 Gamepad 图像,可用于显示用户的帮助。 为以下控制器提供图像

所有图像均在 Creative Commons 0 许可 下发布。

SwiftUIGamepad 类

SwiftUIGamepad 提供了一些辅助实用程序,可让您轻松访问存储在 Swift 包中的资源(例如上面的图像)。

例如,以下代码将返回 xxx.png 文件的路径

let path = SwiftUIGamepad.pathTo(resource:"xxx.png")

默认控制设置

SwiftUIGamepad 中定义的几个控件具有一组静态属性来控制在不指定这些属性的情况下创建的任何控件的所有实例。

例如,GamepadMenu 定义了

/// Defines the default card font for the `GamepadMenu` view.
public static var gameMenuCardFontName:String = "Arial"
    
/// Defines the default card font size for the `GamepadMenu` view.
public static var gameMenuCardFontSize:Float = 24
    
/// Defines the default card font color for the `GamepadMenu` view.
public static var gameMenuCardFontColor:Color = .white
    
/// Defines the default card background color for the `GamepadMenu` view.
public static var gameMenuCardBackground:Color = .blue

如果您想统一设置应用程序中使用的所有 GamepadMenuCardView 实例的样式,只需调整 SwiftUIGamepadgameMenuCardBackground

SwiftUIGamepad.gameMenuCardBackground = .red

现在,所有新创建的 GamepadMenuCardView 项都将具有 red 背景。

在哪里设置样式更改和连接 Gamepad 支持

为了使样式更改生效,您需要在绘制任何 Views 之前进行更改。 您可以在您的主应用程序中使用以下代码

import SwiftUI
import SwiftletUtilities
import LogManager
import SwiftUIKit
import SwiftUIGamepad

@main
struct PackageTesterApp: App {
    @UIApplicationDelegateAdaptor private var appDelegate: AppDelegate
    @Environment(\.scenePhase) private var scenePhase
    @Environment(\.colorScheme) var colorScheme
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .onChange(of: scenePhase) { oldScenePhase, newScenePhase in
            switch newScenePhase {
            case .active:
                Debug.info(subsystem: "PackageTesterApp", category: "Scene Phase", "App is active")
                
                // Start watching for Gamepads being connected
                GamepadManager.startWatchingForGamepads()
                
                // Inform any gamepad views that the app is becoming active
                GamepadManager.gamepadOne.appIsBecomingActive()
            case .inactive:
                Debug.info(subsystem: "PackageTesterApp", category: "Scene Phase", "App is inactive")
                
                // Inform any gamepad views that the app is becoming inactive
                GamepadManager.gamepadOne.appIsBecomingInactive()
            case .background:
                Debug.info(subsystem: "PackageTesterApp", category: "Scene Phase", "App is in background")
                
                // Stop watching for gamepads
                GamepadManager.stopWatchingForGamepads()
                
                // Inform any gamepad views that the app is entering the background
                GamepadManager.gamepadOne.appIsEnteringBackground()
            @unknown default:
                Debug.notice(subsystem: "PackageTesterApp", category: "Scene Phase", "App has entered an unexpected scene: \(oldScenePhase), \(newScenePhase)")
            }
        }
    }
}

/// Class the handle the event that would typically be handled by the Application Delegate so they can be handled in SwiftUI.
class AppDelegate: NSObject, UIApplicationDelegate {
    
    /// Handles the app finishing launching
    /// - Parameter application: The app that has started.
    func applicationDidFinishLaunching(_ application: UIApplication) {
        // Register to receive remote notifications
        UIApplication.shared.registerForRemoteNotifications()
    }
    
    /// Handle the application getting ready to launch
    /// - Parameters:
    ///   - application: The application that is going to launch.
    ///   - launchOptions: Any options being passed to the application at launch time.
    /// - Returns: Returns `True` if the application can launch.
    func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        // Set any `SwiftUIGamepad` global style defaults here before any `Views` are drawn.
        // Set style defaults
        SwiftUIGamepad.gameMenuCardBackground = .red
        return true
    }
    
    /// Handles the app receiving a remote notification
    /// - Parameters:
    ///   - application: The app receiving the notifications.
    ///   - userInfo: The info that has been sent to the App.
    func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]) {
        
    }
}

有了这段代码,在 func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool 中进行任何样式更改,它们将应用于之后构建的所有视图。

为了使 SwiftUIGamepad 正常工作,您必须GamepadManager 事件添加到应用程序的 .onChange 事件中(如上所列)。 目前,SwiftUIGamepad 仅支持一次连接一个游戏手柄,它将在 GamepadManager.gamepadOne 属性中可用。

文档

Package 包含其所有功能的完整 DocC Documentation