Co-founder of VideoEditor: Reels & Stories | Founder of Flow: Diary, Calendar, Gallery | #Swift | #UIKit | #SwiftUI
Walkthroughs, tutorials, guides, and tips. This story will teach you how to do something new or how to do something better.
多点连接是通用数据交换格式的替代方案。多点连接无需通过中间代理(通常是后端服务器)通过 Wi-Fi 或蜂窝网络交换数据,而是能够在没有中间人的情况下在多个附近设备之间交换信息。
iPhone和 iPad 使用 Wi-Fi 和蓝牙技术,而 MacBook 和 Apple TV 依赖 Wi-Fi 和以太网。
从这里开始,这项技术的利弊就显而易见了。其优点包括去中心化,以及无需中介即可交换信息的能力。
缺点——共享仅限于 iPhone 和 iPad 的 Wi-Fi 和蓝牙覆盖范围,或 MacBook 和 Apple TV 的 Wi-Fi 和以太网覆盖范围。换句话说,信息交换可以在设备的附近进行。
多点连接的集成并不复杂,包括以下步骤:
项目预设
设置其他设备的可见性
扫描范围内的可见设备
创建一对用于数据交换的设备
数据交换
让我们仔细看看上述每个步骤。
在此阶段,项目必须为实现多点连接做好准备。为此,您需要从用户那里获得额外的权限才能扫描:
Info.plist
文件中添加隐私-本地网络使用说明,并说明使用目的;Info.plist
还需要补充如下几行:
<key>NSBonjourServices</key> <array> <string>_nearby-devices._tcp</string> <string>_nearby-devices._upd</string> </array>
需要注意的是,此处以nearby-devices
子字符串为例。在您的项目中,此键必须满足以下要求:
长度为 1-15 个字符,有效字符包括 ASCII 小写字母、数字和连字符,至少包含一个字母且不包含相邻的连字符。
您可以在此处阅读有关要求的更多信息。
至于通信协议,示例中使用了tcp
和upd
(可靠性较高和可靠性较低)。如果您不知道需要哪种协议,则应同时输入两者。
多对等连接的设备可见性组织由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
切换设备的可见性。
多对等连接的设备可见性扫描由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
。
startBrowsing
和finishBrowsing
方法将用于在屏幕出现时开始发现可见设备,或在屏幕消失后停止搜索。
下列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
启用/禁用。
因此,现阶段设备的检测和显示应该可以正常工作。
委托方法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
将返回发送邀请、许可和会话的设备。
因此,成功接受邀请后,将创建一对:
创建完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
来接收消息。
有关多点连接集成原型的更多详细信息,请参阅此处