paint-brush
掌握 iOS 多点连接并在无需互联网访问的情况下在多个设备之间共享数据经过@bugorbn
251 讀數

掌握 iOS 多点连接并在无需互联网访问的情况下在多个设备之间共享数据

经过 Boris Bugor12m2024/08/19
Read on Terminal Reader

太長; 讀書

多点连接允许使用 Wi-Fi、蓝牙和以太网在 Apple 设备之间直接交换数据,绕过传统服务器。本文概述了将此技术集成到您的项目中的优势、局限性和步骤。
featured image - 掌握 iOS 多点连接并在无需互联网访问的情况下在多个设备之间共享数据
Boris Bugor HackerNoon profile picture
0-item


多点连接是通用数据交换格式的替代方案。多点连接无需通过中间代理(通常是后端服务器)通过 Wi-Fi 或蜂窝网络交换数据,而是能够在没有中间人的情况下在多个附近设备之间交换信息。


iPhone和 iPad 使用 Wi-Fi 和蓝牙技术,而 MacBook 和 Apple TV 依赖 Wi-Fi 和以太网。


从这里开始,这项技术的利弊就显而易见了。其优点包括去中心化,以及无需中介即可交换信息的能力。


缺点——共享仅限于 iPhone 和 iPad 的 Wi-Fi 和蓝牙覆盖范围,或 MacBook 和 Apple TV 的 Wi-Fi 和以太网覆盖范围。换句话说,信息交换可以在设备的附近进行。


多点连接的集成并不复杂,包括以下步骤:

  1. 项目预设

  2. 设置其他设备的可见性

  3. 扫描范围内的可见设备

  4. 创建一对用于数据交换的设备

  5. 数据交换


让我们仔细看看上述每个步骤。


1.项目预设

在此阶段,项目必须为实现多点连接做好准备。为此,您需要从用户那里获得额外的权限才能扫描:

  • Info.plist文件中添加隐私-本地网络使用说明,并说明使用目的;
  • 此外,为了实现信息交换的可能性, Info.plist还需要补充如下几行:


 <key>NSBonjourServices</key> <array> <string>_nearby-devices._tcp</string> <string>_nearby-devices._upd</string> </array>


需要注意的是,此处以nearby-devices子字符串为例。在您的项目中,此键必须满足以下要求:

长度为 1-15 个字符,有效字符包括 ASCII 小写字母、数字和连字符,至少包含一个字母且不包含相邻的连字符。

您可以在此处阅读有关要求的更多信息。

至于通信协议,示例中使用了tcpupd (可靠性较高和可靠性较低)。如果您不知道需要哪种协议,则应同时输入两者。

2.设置其他设备的可见性

多对等连接的设备可见性组织由MCNearbyServiceAdvertiser实现。让我们创建一个负责检测、显示和在设备之间共享信息的类。


 import MultipeerConnectivity import SwiftUI class DeviceFinderViewModel: ObservableObject { private let advertiser: MCNearbyServiceAdvertiser private let session: MCSession private let serviceType = "nearby-devices" @Published var isAdvertised: Bool = false { didSet { isAdvertised ? advertiser.startAdvertisingPeer() : advertiser.stopAdvertisingPeer() } } init() { let peer = MCPeerID(displayName: UIDevice.current.name) session = MCSession(peer: peer) advertiser = MCNearbyServiceAdvertiser( peer: peer, discoveryInfo: nil, serviceType: serviceType ) } }


多点连接的核心是MCSession ,它允许您在设备之间连接和交换数据。

serviceType就是上面提到的键,它与交换协议一起添加到Info.plist文件中。

isAdvertised属性允许您使用Toggle切换设备的可见性。

3.扫描范围内可见设备

多对等连接的设备可见性扫描由MCNearbyServiceBrowser执行:


 class DeviceFinderViewModel: NSObject, ObservableObject { ... private let browser: MCNearbyServiceBrowser ... @Published var peers: [PeerDevice] = [] ... override init() { ... browser = MCNearbyServiceBrowser(peer: peer, serviceType: serviceType) super.init() browser.delegate = self } func startBrowsing() { browser.startBrowsingForPeers() } func finishBrowsing() { browser.stopBrowsingForPeers() } } extension DeviceFinderViewModel: MCNearbyServiceBrowserDelegate { func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) { peers.append(PeerDevice(peerId: peerID)) } func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) { peers.removeAll(where: { $0.peerId == peerID }) } } struct PeerDevice: Identifiable, Hashable { let id = UUID() let peerId: MCPeerID }


所有可见设备的列表将存储在peers中。当找到或丢失对等设备时, MCNearbyServiceBrowser委托方法将添加或删除MCPeerID


startBrowsingfinishBrowsing方法将用于在屏幕出现时开始发现可见设备,或在屏幕消失后停止搜索。


下列View将用作 UI:


 struct ContentView: View { @StateObject var model = DeviceFinderViewModel() var body: some View { NavigationStack { List(model.peers) { peer in HStack { Image(systemName: "iphone.gen1") .imageScale(.large) .foregroundColor(.accentColor) Text(peer.peerId.displayName) .frame(maxWidth: .infinity, alignment: .leading) } .padding(.vertical, 5) } .onAppear { model.startBrowsing() } .onDisappear { model.finishBrowsing() } .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Toggle("Press to be discoverable", isOn: $model.isAdvertised) .toggleStyle(.switch) } } } } }


设备可见性可以通过Toggle启用/禁用。
因此,现阶段设备的检测和显示应该可以正常工作。


4. 创建一对用于数据交换的设备

委托方法MCNearbyServiceAdvertiserdidReceiveInvitationFromPeer负责在两个设备之间发送邀请。两个设备都必须能够处理此请求。


 class DeviceFinderViewModel: NSObject, ObservableObject { ... @Published var permissionRequest: PermitionRequest? @Published var selectedPeer: PeerDevice? { didSet { connect() } } ... @Published var joinedPeer: [PeerDevice] = [] override init() { ... advertiser.delegate = self } func startBrowsing() { browser.startBrowsingForPeers() } func finishBrowsing() { browser.stopBrowsingForPeers() } func show(peerId: MCPeerID) { guard let first = peers.first(where: { $0.peerId == peerId }) else { return } joinedPeer.append(first) } private func connect() { guard let selectedPeer else { return } if session.connectedPeers.contains(selectedPeer.peerId) { joinedPeer.append(selectedPeer) } else { browser.invitePeer(selectedPeer.peerId, to: session, withContext: nil, timeout: 60) } } } extension DeviceFinderViewModel: MCNearbyServiceAdvertiserDelegate { func advertiser( _ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void ) { permissionRequest = PermitionRequest( peerId: peerID, onRequest: { [weak self] permission in invitationHandler(permission, permission ? self?.session : nil) } ) } } struct PermitionRequest: Identifiable { let id = UUID() let peerId: MCPeerID let onRequest: (Bool) -> Void }


设置selectedPeer后,将触发 connect 方法。如果此peer位于现有peers列表中,它将被添加到joinedPeer数组中。将来,此属性将由 UI 处理。


当会话中没有该对等体时, browser将邀请该设备创建一对。


之后,将为受邀设备处理didReceiveInvitationFromPeer方法。在我们的例子中,在didReceiveInvitationFromPeer启动后,将创建一个具有延迟回调的permissionRequest ,该回调将作为警报显示在受邀设备上:


 struct ContentView: View { @StateObject var model = DeviceFinderViewModel() var body: some View { NavigationStack { ... .alert(item: $model.permissionRequest, content: { request in Alert( title: Text("Do you want to join \(request.peerId.displayName)"), primaryButton: .default(Text("Yes"), action: { request.onRequest(true) model.show(peerId: request.peerId) }), secondaryButton: .cancel(Text("No"), action: { request.onRequest(false) }) ) }) ... } } }


在批准的情况下,如果许可成功, didReceiveInvitationFromPeer将返回发送邀请、许可和会话的设备。


因此,成功接受邀请后,将创建一对:


5.数据交换

创建完pair之后, MCSession负责数据的交换:


 import MultipeerConnectivity import Combine class DeviceFinderViewModel: NSObject, ObservableObject { ... @Published var messages: [String] = [] let messagePublisher = PassthroughSubject<String, Never>() var subscriptions = Set<AnyCancellable>() func send(string: String) { guard let data = string.data(using: .utf8) else { return } try? session.send(data, toPeers: [joinedPeer.last!.peerId], with: .reliable) messagePublisher.send(string) } override init() { ... session.delegate = self messagePublisher .receive(on: DispatchQueue.main) .sink { [weak self] in self?.messages.append($0) } .store(in: &subscriptions) } } extension DeviceFinderViewModel: MCSessionDelegate { func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) { guard let last = joinedPeer.last, last.peerId == peerID, let message = String(data: data, encoding: .utf8) else { return } messagePublisher.send(message) } }


方法func send(_ data: Data, toPeers peerIDs: [MCPeerID], with mode: MCSessionSendDataMode) throws有助于在对等体之间发送数据。


在收到消息的设备上触发委托方法func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID)


另外,由于MCSession委托方法在DispatchQueue global()中触发,因此使用中间发布者messagePublisher来接收消息。


有关多点连接集成原型的更多详细信息,请参阅此处存储库举个例子,这项技术提供了设备之间交换消息的能力。



请随时联系我叽叽喳喳如果您有任何疑问。此外,您还可以随时给我买杯咖啡