我最近对 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 中讨论过。