La conectividad multipeer es una alternativa al formato común de intercambio de datos. En lugar de intercambiar datos a través de Wi-Fi o red celular mediante un intermediario, que suele ser un servidor backend, la conectividad multipeer brinda la capacidad de intercambiar información entre múltiples dispositivos cercanos sin intermediarios.
El iPhone y el iPad utilizan tecnología Wi-Fi y Bluetooth, mientras que MacBook y Apple TV dependen de Wi-Fi y Ethernet.
De aquí se desprenden inmediatamente los pros y los contras de esta tecnología. Entre las ventajas se encuentra la descentralización y, en consecuencia, la posibilidad de intercambiar información sin intermediarios.
Desventajas: el intercambio de información se limita a la cobertura Wi-Fi y Bluetooth para iPhone y iPad, o Wi-Fi y Ethernet para MacBook y Apple TV. En otras palabras, el intercambio de información puede realizarse en las inmediaciones de los dispositivos.
La integración de la conectividad multipeer no es complicada y consta de los siguientes pasos:
Proyecto preestablecido
Configurar la visibilidad para otros dispositivos
Escanear en busca de dispositivos visibles dentro del alcance
Creación de un par de dispositivos para el intercambio de datos
Intercambio de datos
Veamos más de cerca cada uno de los pasos anteriores.
En esta etapa, el proyecto debe estar preparado para la implementación de la conectividad multipeer. Para ello, es necesario obtener permisos adicionales del usuario para poder escanear:
Info.plist
con una descripción del propósito de uso;Info.plist
también debe complementarse con las siguientes líneas:
<key>NSBonjourServices</key> <array> <string>_nearby-devices._tcp</string> <string>_nearby-devices._upd</string> </array>
Es importante tener en cuenta que la subcadena nearby-devices
se utiliza como ejemplo en este contexto. En su proyecto, esta clave debe cumplir los siguientes requisitos:
Tiene una longitud de 1 a 15 caracteres y los caracteres válidos incluyen letras minúsculas ASCII, números y el guion, que contengan al menos una letra y ningún guion adyacente.
Puede leer más sobre los requisitos__ aquí__ .
En cuanto a los protocolos de comunicación, en el ejemplo se utilizan tcp
y upd
(uno más fiable y otro menos fiable). Si no sabes qué protocolo necesitas, debes introducir ambos.
La organización de la visibilidad de los dispositivos para la conexión entre múltiples pares se implementa mediante MCNearbyServiceAdvertiser
. Creemos una clase que será responsable de detectar, mostrar y compartir información entre dispositivos.
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 ) } }
El núcleo del multipeer es una MCSession
, que le permitirá conectarse e intercambiar datos entre dispositivos.
serviceType
es la clave mencionada anteriormente, que se agregó al archivo Info.plist
junto con los protocolos de intercambio.
La propiedad isAdvertised
le permitirá cambiar la visibilidad del dispositivo usando Toggle
.
El escaneo de visibilidad del dispositivo para una conexión multi-peer lo realiza 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 }
Se almacenará una lista de todos los dispositivos visibles en peers
. Los métodos delegados MCNearbyServiceBrowser
agregarán o eliminarán un MCPeerID
cuando se encuentre o se pierda un peer.
Los métodos startBrowsing
y finishBrowsing
se utilizarán para comenzar a descubrir dispositivos visibles cuando aparezca la pantalla o detener la búsqueda después de que la pantalla desaparezca.
La siguiente View
se utilizará como interfaz de usuario:
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) } } } } }
La visibilidad del dispositivo se habilitará o deshabilitará mediante el Toggle
.
Como resultado, en esta etapa, la detección y visualización de dispositivos deberían funcionar correctamente.
El método delegado MCNearbyServiceAdvertiserdidReceiveInvitationFromPeer
es responsable de enviar una invitación entre un par de dispositivos. Ambos deben ser capaces de gestionar esta solicitud.
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 }
Cuando se configura selectedPeer
, se activa el método connect. Si este peer
está en la lista de peers
existentes, se agregará a la matriz joinedPeer
. En el futuro, la UI procesará esta propiedad.
En ausencia de este par en la sesión, el browser
invitará a este dispositivo a crear un par.
Después de eso, se procesará el método didReceiveInvitationFromPeer
para el dispositivo invitado. En nuestro caso, después del inicio de didReceiveInvitationFromPeer
, se crea un permissionRequest
con una devolución de llamada retrasada, que se mostrará como una alerta en el dispositivo invitado:
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) }) ) }) ... } } }
En el caso de una aprobación, didReceiveInvitationFromPeer
devolverá el dispositivo que envió la invitación, el permiso y la sesión si el permiso fue exitoso.
Como resultado, después de aceptar con éxito la invitación, se creará un par:
Después de crear un par, MCSession
es responsable del intercambio de datos:
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) } }
El método func send(_ data: Data, toPeers peerIDs: [MCPeerID], with mode: MCSessionSendDataMode) throws
ayuda a enviar datos entre pares.
El método delegado func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID)
se activa en el dispositivo que recibió el mensaje.
Además, se utiliza un mensaje de publicador intermedio messagePublisher
para recibir mensajes, ya que los métodos delegados MCSession
se activan en el DispatchQueue global()
.
Se pueden encontrar más detalles sobre el prototipo de integración de conectividad multipeer en este
No dudes en contactarme en