在 Swift 中将 iPhone 用作具有多点连接的 3D 鼠标

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

我最近对 ​​CoreMotion 的实验 CoreMotion Controlled 3D Sketching on an iPhone with Swift 让我想知道是否可以将 iPhone 用作 3D 鼠标来控制单独设备上的另一个应用程序。事实证明,使用 Apple 的 Multipeer Connectivity 框架,这不仅是可能的,而且非常棒!

Multipeer Connectivity 框架通过 Wi-Fi 和蓝牙在 iOS 设备之间提供对等通信。除了允许设备发送离散的信息包外,它还支持流式传输,这是我需要让我的 iPhone 传输连续的数据流,描述其在 3D 空间中的姿态(滚动、俯仰和偏航)。

我不会详细介绍框架的细节,这些在我用来帮助我快速上手的三篇主要文章中有很好的解释:

我的单一代码库完成了显示漂浮在空间中的立方体的 iPad“旋转立方体”应用程序和控制立方体 3D 旋转的 iPhone“3D 鼠标”应用程序的工作。由于这更像是一个概念验证项目而不是一段生产代码,所有内容都在 一个视图控制器 中,这不是一个好的架构,但是当在两种“模式”之间快速移动时,它非常快工作。

iPad“旋转立方体应用程序”

使用 Multipeer Connectivity 的应用程序可以发布服务或浏览服务。在我的项目中, Rotating Cube App 扮演了广告商的角色,因此我的视图控制器实现了 MCNearbyServiceAdvertiserDelegate 协议。我开始做广告后:


 func initialiseAdvertising()
    {
        serviceAdvertiser = MCNearbyServiceAdvertiser(peer: peerID, discoveryInfo: nil, serviceType: serviceType)
    serviceAdvertiser.delegate = self
    serviceAdvertiser.startAdvertisingPeer()

}

...协议的 advertiser() 方法在收到来自对等方的邀请时被调用。我想自动接受它:


 func initialiseAdvertising()
    {
        serviceAdvertiser = MCNearbyServiceAdvertiser(peer: peerID, discoveryInfo: nil, serviceType: serviceType)
    serviceAdvertiser.delegate = self
    serviceAdvertiser.startAdvertisingPeer()

}

iPhone“3D 鼠标应用程序”

由于 旋转立方体应用程序 是广告商,我的 3D 鼠标应用程序 是浏览器。所以我的整体视图控制器也实现了 MCNearbyServiceBrowserDelegate 并且,就像广告商一样,它开始浏览:


 func initialiseAdvertising()
    {
        serviceAdvertiser = MCNearbyServiceAdvertiser(peer: peerID, discoveryInfo: nil, serviceType: serviceType)
    serviceAdvertiser.delegate = self
    serviceAdvertiser.startAdvertisingPeer()

}

...一旦找到对等点,它就会发送我们在上面看到的加入会话的邀请:


 func initialiseAdvertising()
    {
        serviceAdvertiser = MCNearbyServiceAdvertiser(peer: peerID, discoveryInfo: nil, serviceType: serviceType)
    serviceAdvertiser.delegate = self
    serviceAdvertiser.startAdvertisingPeer()

}

在这里,我还实例化了 CADisplayLink 以对每个帧调用 step() 方法。 step() 做了两件事:它使用我在上面定义的 streamTargetPeer 来尝试启动流式会话...


 func initialiseAdvertising()
    {
        serviceAdvertiser = MCNearbyServiceAdvertiser(peer: peerID, discoveryInfo: nil, serviceType: serviceType)
    serviceAdvertiser.delegate = self
    serviceAdvertiser.startAdvertisingPeer()

}

...并且,如果该流会话可用,则通过流发送 iPhone 在 3D 空间中的姿态(使用 CoreMotion 获取):


 func initialiseAdvertising()
    {
        serviceAdvertiser = MCNearbyServiceAdvertiser(peer: peerID, discoveryInfo: nil, serviceType: serviceType)
    serviceAdvertiser.delegate = self
    serviceAdvertiser.startAdvertisingPeer()

}

序列化和反序列化浮点值

姿态(MotionControllerAttitude 类型)结构包含三个浮点值,用于滚动、俯仰和偏航,但流仅支持 UInt8 字节。为了序列化和反序列化该数据,我在 StackOverflow 上找到了 Rintaro 的这两个函数,它们接受任何类型并与 UInt8 数组相互转换:


 func initialiseAdvertising()
    {
        serviceAdvertiser = MCNearbyServiceAdvertiser(peer: peerID, discoveryInfo: nil, serviceType: serviceType)
    serviceAdvertiser.delegate = self
    serviceAdvertiser.startAdvertisingPeer()

}

我的 MotionControllerAttitude 结构有一个 toBytes() 方法,该方法使用 toByteArray() 和 flatMap() 创建 outputStream.write 可以使用的 UInt8 数组:


 func initialiseAdvertising()
    {
        serviceAdvertiser = MCNearbyServiceAdvertiser(peer: peerID, discoveryInfo: nil, serviceType: serviceType)
    serviceAdvertiser.delegate = self
    serviceAdvertiser.startAdvertisingPeer()

}

...并且,相反地,还有一个 init() 来使用 fromByteArray() 从 UInt8 数组中实例化它自己的一个实例:


 func initialiseAdvertising()
    {
        serviceAdvertiser = MCNearbyServiceAdvertiser(peer: peerID, discoveryInfo: nil, serviceType: serviceType)
    serviceAdvertiser.delegate = self
    serviceAdvertiser.startAdvertisingPeer()

}

这是非常脆弱的代码——同样,这只是一个概念证明!

旋转立方体

回到 Rotating Cube App 中,因为视图控制器也充当了 steam 的 NSStreamDelegate(你现在可以看到事情正在渴望重构!),当 iPad 接收到数据包时调用 stream() 方法。

我需要检查传入流实际上是一个 NSInputStream 并且它有可用字节。如果确实如此,我使用上面的代码从传入数据创建一个 MotionControllerAttitude 实例,并简单地在我的立方体上设置欧拉角:


 func initialiseAdvertising()
    {
        serviceAdvertiser = MCNearbyServiceAdvertiser(peer: peerID, discoveryInfo: nil, serviceType: serviceType)
    serviceAdvertiser.delegate = self
    serviceAdvertiser.startAdvertisingPeer()

}

综上所述

这个项目展示了 Multipeer Connectivity 的强大功能:无论您是在创建游戏还是内容创建应用程序,多个 iOS 设备都可以协同工作并快速可靠地传输任何类型的数据。可以想象,一屋子的 iPad 都可以像对等设备一样连接起来,充当 渲染农场 或一个巨大的单一多设备显示器。

与往常一样,该项目的源代码可在 我的 GitHub 存储库中 获取。

我没有介绍 CoreMotion 代码,这都在 CoreMotion Controlled 3D sketching on an iPhone with Swift 中讨论过。