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 integração da conectividade multipeer não é complicada e consiste nas seguintes etapas:
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__ __.
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