paint-brush
Освойте многоточечное подключение iOS и обменивайтесь данными между несколькими устройствами без доступа в Интернетк@bugorbn
251 чтения

Освойте многоточечное подключение iOS и обменивайтесь данными между несколькими устройствами без доступа в Интернет

к Boris Bugor12m2024/08/19
Read on Terminal Reader

Слишком долго; Читать

Multipeer Connectivity позволяет осуществлять прямой обмен данными между устройствами Apple с использованием Wi-Fi, Bluetooth и Ethernet, минуя традиционные серверы. В статье описываются преимущества, ограничения и шаги по интеграции этой технологии в ваши проекты.
featured image - Освойте многоточечное подключение iOS и обменивайтесь данными между несколькими устройствами без доступа в Интернет
Boris Bugor HackerNoon profile picture
0-item


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. Другими словами, обмен информацией может осуществляться в непосредственной близости от устройств.


Интеграция многоточечного соединения несложна и состоит из следующих шагов:

  1. Предварительная настройка проекта

  2. Настройте видимость для других устройств

  3. Сканирование на наличие видимых устройств в радиусе действия

  4. Создание пары устройств для обмена данными

  5. Обмен данными


Давайте подробнее рассмотрим каждый из вышеперечисленных шагов.


1. Предварительная настройка проекта

На данном этапе проект должен быть подготовлен к внедрению Multipeer Connectivity. Для этого необходимо получить дополнительные разрешения от пользователя для сканирования:

  • добавить Privacy — Local Network Usage Description в файл 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 (более надежный и менее надежный). Если вы не знаете, какой протокол вам нужен, то следует ввести оба.

2. Настройте видимость для других устройств

Организация видимости устройств для многопирового соединения реализуется 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 .

3. Сканирование на наличие видимых устройств в радиусе действия

Сканирование видимости устройства для многорангового соединения выполняется 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 .
В результате на данном этапе обнаружение и отображение устройств должно работать корректно.


4. Создание пары устройств для обмена данными

Метод делегата 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 вернет устройство, отправившее приглашение, разрешение и сеанс, если разрешение было получено успешно.


В результате, после успешного принятия приглашения, будет создана пара:


5. Обмен данными

После создания пары 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 можно найти здесь. репозиторий . Например, эта технология предоставила возможность обмена сообщениями между устройствами.



Не стесняйтесь обращаться ко мне по адресу Твиттер если у вас есть какие-либо вопросы. Также вы всегда можете купи мне кофе .