Multipeer Connectivity — это альтернатива общепринятому формату обмена данными. Вместо обмена данными по Wi-Fi или сотовой сети через промежуточного брокера, которым обычно является внутренний сервер, Multipeer Connectivity предоставляет возможность обмениваться информацией между несколькими близлежащими устройствами без посредников.
iPhone и iPad используют технологии Wi-Fi и Bluetooth, тогда как MacBook и Apple TV полагаются на Wi-Fi и Ethernet.
Отсюда сразу вытекают плюсы и минусы этой технологии. К плюсам можно отнести децентрализацию и, соответственно, возможность обмена информацией без посредников.
Недостатки — обмен данными ограничен зоной покрытия Wi-Fi и Bluetooth для iPhone и iPad или Wi-Fi и Ethernet для MacBook и Apple TV. Другими словами, обмен информацией может осуществляться в непосредственной близости от устройств.
Интеграция многоточечного соединения несложна и состоит из следующих шагов:
Предварительная настройка проекта
Настройте видимость для других устройств
Сканирование на наличие видимых устройств в радиусе действия
Создание пары устройств для обмена данными
Обмен данными
Давайте подробнее рассмотрим каждый из вышеперечисленных шагов.
На данном этапе проект должен быть подготовлен к внедрению Multipeer Connectivity. Для этого необходимо получить дополнительные разрешения от пользователя для сканирования:
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 ) } }
Ядром multipeer является 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
:
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
. В дальнейшем это свойство будет обрабатываться пользовательским интерфейсом.
При отсутствии данного пира в сеансе 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
вернет устройство, отправившее приглашение, разрешение и сеанс, если разрешение было получено успешно.
В результате, после успешного принятия приглашения, будет создана пара:
После создания пары 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)
запускается на устройстве, получившем сообщение.
Кроме того, для получения сообщений используется промежуточный издатель messagePublisher
, поскольку методы делегата MCSession
срабатывают в DispatchQueue global()
.
Более подробную информацию о прототипе интеграции Multipeer Connectivity можно найти здесь.
Не стесняйтесь обращаться ко мне по адресу