Multipeer Connectivity é uma alternativa ao formato comum de troca de dados. Em vez de trocar dados via Wi-Fi ou Rede Celular por meio de um broker intermediário, que geralmente é um servidor backend, Multipeer Connectivity fornece a capacidade de trocar informações entre vários dispositivos próximos sem intermediários.
O iPhone e o iPad usam tecnologia Wi-Fi e Bluetooth, enquanto o MacBook e a Apple TV dependem de Wi-Fi e Ethernet.
A partir daqui, os prós e contras dessa tecnologia seguem imediatamente. As vantagens incluem descentralização e, consequentemente, a capacidade de trocar informações sem intermediários.
Desvantagens — o compartilhamento é limitado à cobertura Wi-Fi e Bluetooth para iPhone e iPad, ou Wi-Fi e Ethernet para MacBook e Apple TV. Em outras palavras, a troca de informações pode ser realizada nas imediações dos dispositivos.
A integração da conectividade multipeer não é complicada e consiste nas seguintes etapas:
Predefinição do projeto
Configurar visibilidade para outros dispositivos
Procure por dispositivos visíveis dentro do alcance
Criando um par de dispositivos para troca de dados
Troca de dados
Vamos analisar mais detalhadamente cada uma das etapas acima.
Nesta fase, o projeto deve ser preparado para a implementação do Multipeer Connectivity. Para isso, você precisa obter permissões adicionais do usuário para poder escanear:
Info.plist
com uma descrição da finalidade do uso;Info.plist
também precisa ser complementado com as seguintes linhas:
<key>NSBonjourServices</key> <array> <string>_nearby-devices._tcp</string> <string>_nearby-devices._upd</string> </array>
É importante notar que a substring nearby-devices
é usada como um exemplo neste contexto. No seu projeto, esta chave deve atender aos seguintes requisitos:
1–15 caracteres e caracteres válidos incluem letras minúsculas ASCII, números e o hífen, contendo pelo menos uma letra e nenhum hífen adjacente.
Você pode ler mais sobre os requisitos__ aqui __.
Quanto aos protocolos de comunicação, o exemplo usa tcp
e upd
(o mais confiável e o menos confiável). Se você não sabe qual protocolo precisa, você deve digitar ambos.
A organização da visibilidade do dispositivo para conexão multi-peer é implementada por MCNearbyServiceAdvertiser
. Vamos criar uma classe que será responsável por detectar, exibir e compartilhar informações 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 ) } }
O núcleo do multipeer é uma MCSession
, que permitirá que você se conecte e troque dados entre dispositivos.
O serviceType
é a chave mencionada acima, que foi adicionada ao arquivo Info.plist
junto com os protocolos de troca.
A propriedade isAdvertised
permitirá que você alterne a visibilidade do dispositivo usando Toggle
.
A varredura de visibilidade do dispositivo para uma conexão multi-peer é realizada pelo 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 }
Uma lista de todos os dispositivos visíveis será armazenada em peers
. Os métodos delegados MCNearbyServiceBrowser
adicionarão ou removerão um MCPeerID
quando um peer for encontrado ou perdido.
Os métodos startBrowsing
e finishBrowsing
serão usados para começar a descobrir dispositivos visíveis quando a tela aparecer ou parar a pesquisa depois que a tela desaparecer.
A seguinte View
será usada como interface do usuário:
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) } } } } }
A visibilidade do dispositivo será ativada/desativada pelo Toggle
.
Como resultado, nesta fase, a detecção e a exibição dos dispositivos devem funcionar corretamente.
O método delegado MCNearbyServiceAdvertiserdidReceiveInvitationFromPeer
é responsável por enviar um convite entre um par de dispositivos. Ambos devem ser capazes de manipular essa solicitação.
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 }
Quando selectedPeer
é definido, o método connect dispara. Se esse peer
estiver na lista de peers
existentes, ele será adicionado ao array joinedPeer
. No futuro, essa propriedade será processada pela UI.
Na ausência deste par na sessão, o browser
convidará este dispositivo a criar um par.
Depois disso, o método didReceiveInvitationFromPeer
será processado para o dispositivo convidado. No nosso caso, após o início de didReceiveInvitationFromPeer
, um permissionRequest
é criado com um callback atrasado, que será mostrado como um alerta no dispositivo convidado:
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) }) ) }) ... } } }
No caso de uma aprovação, didReceiveInvitationFromPeer
retornará o dispositivo que enviou o convite, a permissão e a sessão, se a permissão foi bem-sucedida.
Como resultado, após aceitar o convite com sucesso, um par será criado:
Após criar um par, MCSession
é responsável pela troca de dados:
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) } }
O método func send(_ data: Data, toPeers peerIDs: [MCPeerID], with mode: MCSessionSendDataMode) throws
ajuda a enviar dados entre pares.
O método delegado func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID)
é acionado no dispositivo que recebeu a mensagem.
Além disso, um publicador intermediário messagePublisher
é usado para receber mensagens, já que os métodos delegados MCSession
são disparados no DispatchQueue global()
.
Mais detalhes sobre o protótipo de integração Multipeer Connectivity podem ser encontrados neste
Não hesite em contactar-me em