マルチピア接続は、一般的なデータ交換形式の代替手段です。中間ブローカー (通常はバックエンド サーバー) を介して Wi-Fi またはセルラー ネットワーク経由でデータを交換する代わりに、マルチピア接続では、仲介者なしで複数の近くのデバイス間で情報を交換できます。
iPhoneと iPad は Wi-Fi と Bluetooth テクノロジーを使用しますが、MacBook と Apple TV は Wi-Fi とイーサネットに依存します。
ここから、このテクノロジーの長所と短所がすぐにわかります。利点としては、分散化と、それに伴う仲介者なしで情報を交換できることが挙げられます。
デメリット - 共有は、iPhone と iPad の場合は Wi-Fi と Bluetooth の範囲内、MacBook と Apple TV の場合は Wi-Fi と Ethernet の範囲内に制限されます。つまり、情報の交換はデバイスのすぐ近くで行うことができます。
マルチピア接続の統合は複雑ではなく、次の手順で構成されます。
プロジェクトプリセット
他のデバイスの可視性を設定する
範囲内の可視デバイスをスキャンする
データ交換用のデバイスのペアを作成する
データ交換
上記の各ステップを詳しく見てみましょう。
この段階では、プロジェクトはマルチピア接続の実装に向けて準備する必要があります。これを行うには、スキャンできるようにユーザーから追加の権限を取得する必要があります。
Info.plist
ファイルに「プライバシー - ローカル ネットワークの使用状況の説明」を追加します。Info.plist
に次の行も追加する必要があります。
<key>NSBonjourServices</key> <array> <string>_nearby-devices._tcp</string> <string>_nearby-devices._upd</string> </array>
このコンテキストでは、 nearby-devices
サブ文字列が例として使用されていることに注意することが重要です。プロジェクトでは、このキーは次の要件を満たしている必要があります。
長さは 1 ~ 15 文字で、有効な文字には ASCII 小文字、数字、ハイフンが含まれ、少なくとも 1 つの文字が含まれ、隣接するハイフンは含まれません。
要件の詳細については、 ここ__でご覧いただけます__。
通信プロトコルに関しては、例では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 ) } }
マルチピアの中核は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 デリゲート メソッドはMCNearbyServiceBrowser
ピアが見つかった場合や失われた場合にMCPeerID
追加または削除します。
startBrowsing
とfinishBrowsing
メソッドは、画面が表示されたときに表示されているデバイスの検出を開始したり、画面が消えた後に検索を停止したりするために使用されます。
UI として次の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
配列に追加されます。将来、このプロパティは UI によって処理されます。
セッションにこのピアが存在しない場合は、 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)
トリガーされます。
また、 MCSession
デリゲート メソッドはDispatchQueue global()
で実行されるため、中間パブリッシャーmessagePublisher
使用してメッセージが受信されます。
マルチピア接続統合プロトタイプの詳細については、こちらをご覧ください。