멀티피어 커넥티비티는 일반적인 데이터 교환 형식에 대한 대안입니다. 일반적으로 백엔드 서버인 중간 브로커를 통해 Wi-Fi 또는 셀룰러 네트워크를 통해 데이터를 교환하는 대신, 멀티피어 커넥티비티는 중개자 없이 여러 근처 장치 간에 정보를 교환할 수 있는 기능을 제공합니다.
iPhone 과 iPad는 Wi-Fi와 Bluetooth 기술을 사용하는 반면, MacBook과 Apple TV는 Wi-Fi와 이더넷을 사용합니다.
여기에서 이 기술의 장단점을 바로 따라갑니다. 장점에는 분산화와 그에 따라 중개자 없이 정보를 교환할 수 있는 능력이 포함됩니다.
단점 - 공유는 iPhone 및 iPad의 경우 Wi-Fi 및 Bluetooth 범위로 제한되고, MacBook 및 Apple TV의 경우 Wi-Fi 및 이더넷으로 제한됩니다. 즉, 정보 교환은 장치 바로 근처에서 수행할 수 있습니다.
Multipeer Connectivity 통합은 복잡하지 않으며 다음 단계로 구성됩니다.
프로젝트 사전 설정
다른 장치에 대한 가시성 설정
범위 내에서 보이는 장치를 검색합니다.
데이터 교환을 위한 장치 쌍 생성
데이터 교환
위의 각 단계를 자세히 살펴보겠습니다.
이 단계에서는 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 소문자, 숫자, 하이픈(최소 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
대리자 메서드는 피어가 발견되거나 손실되면 MCPeerID
추가하거나 제거합니다.
startBrowsing
및 finishBrowsing
메서드는 화면이 나타나면 표시된 장치 검색을 시작하거나 화면이 사라진 후 검색을 중지하는 데 사용됩니다.
다음 View
UI로 사용됩니다.
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
피어 간에 데이터를 전송하는 데 도움이 됩니다.
메시지를 수신한 장치에서 delegate 메서드 func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID)
가 트리거됩니다.
또한, 중간 게시자 messagePublisher
MCSession
대리자 메서드가 DispatchQueue global()
에서 실행되기 때문에 메시지를 수신하는 데 사용됩니다.
Multipeer Connectivity 통합 프로토타입에 대한 자세한 내용은 여기에서 확인할 수 있습니다.
언제든지 저에게 연락주세요.