π MultipeerConnectivity
β Difficulty: βββ
β±οΈ Est. Time: 2-3h
π System & Network
근거리 P2P ν΅μ λ° λ°μ΄ν° 곡μ νλ μμν¬
iOS 7+Wi-Fi/BLE μλ μ ν
β¨ MultipeerConnectivity?
MultipeerConnectivity is a framework for exchanging data between nearby devices using Wi-Fi, Bluetooth, and P2P Wi-Fi. Up to 8 devices can form an ad-hoc network without internet, reliably transferring text, files, and stream data. You can implement AirDrop-like functionality directly in your app.
π‘ Key Features: Peer-to-Peer Communication Β· Auto Device Discovery Β· Wi-Fi/BLE Auto Switch Β· File Transfer Β· Stream Data Β· Up to 8 Connections Β· Encrypted Communication
π‘ 1. MCSession μ΄κΈ°ν
Peer-to-Peer μΈμ μ μμ±νκ³ κ΄λ¦¬.
MultipeerManager.swift β Session Initialization
import MultipeerConnectivity import SwiftUI @Observable class MultipeerManager: NSObject { private let serviceType = "hig-lab-chat" // 15μ μ΄ν, μλ¬Έμ/μ«μ/νμ΄νλ§ private var myPeerID: MCPeerID private var session: MCSession private var advertiser: MCNearbyServiceAdvertiser? private var browser: MCNearbyServiceBrowser? var connectedPeers: [MCPeerID] = [] var availablePeers: [MCPeerID] = [] var receivedMessages: [String] = [] var isAdvertising = false var isBrowsing = false override init() { // Peer ID μμ± (κΈ°κΈ° μ΄λ¦ μ¬μ©) myPeerID = MCPeerID(displayName: UIDevice.current.name) // μΈμ μμ± session = MCSession( peer: myPeerID, securityIdentity: nil, encryptionPreference: .required ) super.init() session.delegate = self print("π± Peer ID: \(myPeerID.displayName)") } // Advertiser μμ (λ€λ₯Έ κΈ°κΈ°κ° λλ₯Ό λ°κ²¬ν μ μκ²) func startAdvertising() { advertiser = MCNearbyServiceAdvertiser( peer: myPeerID, discoveryInfo: ["status": "available"], serviceType: serviceType ) advertiser?.delegate = self advertiser?.startAdvertisingPeer() isAdvertising = true print("π’ Advertising μμ") } // Advertiser μ€μ§ func stopAdvertising() { advertiser?.stopAdvertisingPeer() advertiser = nil isAdvertising = false print("βΉοΈ Advertising μ€μ§") } // Browser μμ (λ€λ₯Έ κΈ°κΈ° κ²μ) func startBrowsing() { browser = MCNearbyServiceBrowser(peer: myPeerID, serviceType: serviceType) browser?.delegate = self browser?.startBrowsingForPeers() isBrowsing = true print("π Browsing μμ") } // Browser μ€μ§ func stopBrowsing() { browser?.stopBrowsingForPeers() browser = nil isBrowsing = false print("βΉοΈ Browsing μ€μ§") } // Peerμκ² μ΄λ 보λ΄κΈ° func invitePeer(_ peerID: MCPeerID) { guard let browser = browser else { return } browser.invitePeer( peerID, to: session, withContext: nil, timeout: 30 ) print("βοΈ μ΄λ μ μ‘: \(peerID.displayName)") } // μ°κ²° ν΄μ func disconnect() { session.disconnect() connectedPeers.removeAll() print("β μ°κ²° ν΄μ ") } }
π¬ 2. λ©μμ§ μ μ‘
μ°κ²°λ Peerλ€μκ² λ°μ΄ν°λ₯Ό μ μ‘.
MultipeerManager+Send.swift β Data Transfer
import MultipeerConnectivity extension MultipeerManager { // ν μ€νΈ λ©μμ§ μ μ‘ func sendMessage(_ message: String) { guard !connectedPeers.isEmpty else { print("β οΈ μ°κ²°λ Peerκ° μμ΅λλ€") return } guard let data = message.data(using: .utf8) else { return } do { // λͺ¨λ μ°κ²°λ Peerμκ² μ μ‘ try session.send( data, toPeers: connectedPeers, with: .reliable ) print("π€ λ©μμ§ μ μ‘: \(message)") } catch { print("β μ μ‘ μ€ν¨: \(error.localizedDescription)") } } // νΉμ Peerμκ²λ§ μ μ‘ func sendMessage(_ message: String, to peer: MCPeerID) { guard let data = message.data(using: .utf8) else { return } do { try session.send(data, toPeers: [peer], with: .reliable) print("π€ λ©μμ§ μ μ‘ (\(peer.displayName)): \(message)") } catch { print("β μ μ‘ μ€ν¨: \(error.localizedDescription)") } } // νμΌ μ μ‘ func sendFile(at url: URL, to peer: MCPeerID, withName name: String) { session.sendResource( at: url, withName: name, toPeer: peer, withCompletionHandler: { error in if let error = error { print("β νμΌ μ μ‘ μ€ν¨: \(error.localizedDescription)") } else { print("β νμΌ μ μ‘ μλ£: \(name)") } } ) print("π νμΌ μ μ‘ μμ: \(name)") } // μ΄λ―Έμ§ μ μ‘ func sendImage(_ image: UIImage) { guard !connectedPeers.isEmpty else { return } guard let imageData = image.jpegData(compressionQuality: 0.8) else { return } do { try session.send(imageData, toPeers: connectedPeers, with: .reliable) print("πΌοΈ μ΄λ―Έμ§ μ μ‘ μλ£") } catch { print("β μ΄λ―Έμ§ μ μ‘ μ€ν¨: \(error.localizedDescription)") } } // JSON λ°μ΄ν° μ μ‘ func sendJSON<T: Encodable>(_ object: T) { guard !connectedPeers.isEmpty else { return } do { let encoder = JSONEncoder() let data = try encoder.encode(object) try session.send(data, toPeers: connectedPeers, with: .reliable) print("π¦ JSON μ μ‘ μλ£") } catch { print("β JSON μ μ‘ μ€ν¨: \(error.localizedDescription)") } } }
π₯ 3. Session λΈλ¦¬κ²μ΄νΈ
μ°κ²° μν λ³νμ λ°μ΄ν° μμ handling.
MultipeerManager+SessionDelegate.swift β Session Delegate
import MultipeerConnectivity extension MultipeerManager: MCSessionDelegate { // Peer μν λ³κ²½ func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) { switch state { case .connected: print("β μ°κ²°λ¨: \(peerID.displayName)") if !connectedPeers.contains(peerID) { connectedPeers.append(peerID) } case .connecting: print("π μ°κ²° μ€: \(peerID.displayName)") case .notConnected: print("β μ°κ²° ν΄μ : \(peerID.displayName)") connectedPeers.removeAll { $0 == peerID } @unknown default: break } } // λ°μ΄ν° μμ func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) { print("π₯ λ°μ΄ν° μμ from \(peerID.displayName): \(data.count) bytes") // ν μ€νΈ λ©μμ§ μλ if let message = String(data: data, encoding: .utf8) { let formattedMessage = "\(peerID.displayName): \(message)" receivedMessages.append(formattedMessage) print(" π¬ λ©μμ§: \(message)") } // μ΄λ―Έμ§ μλ else if let image = UIImage(data: data) { print(" πΌοΈ μ΄λ―Έμ§ μμ : \(image.size)") receivedMessages.append("\(peerID.displayName): [μ΄λ―Έμ§]") } // κΈ°ν λ°μ΄λ리 λ°μ΄ν° else { print(" π¦ λ°μ΄λ리 λ°μ΄ν°") receivedMessages.append("\(peerID.displayName): [λ°μ΄ν°]") } } // νμΌ μμ μμ func session( _ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress ) { print("π₯ νμΌ μμ μμ: \(resourceName) from \(peerID.displayName)") print(" μ§νλ₯ : \(progress.fractionCompleted * 100)%") } // νμΌ μμ μλ£ func session( _ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error? ) { if let error = error { print("β νμΌ μμ μ€ν¨: \(error.localizedDescription)") return } guard let localURL = localURL else { return } print("β νμΌ μμ μλ£: \(resourceName)") print(" κ²½λ‘: \(localURL.path)") receivedMessages.append("\(peerID.displayName): [νμΌ] \(resourceName)") // νμΌμ Documents ν΄λλ‘ λ³΅μ¬ do { let documentsURL = FileManager.default .urls(for: .documentDirectory, in: .userDomainMask)[0] let destinationURL = documentsURL.appendingPathComponent(resourceName) // κΈ°μ‘΄ νμΌ μμ if FileManager.default.fileExists(atPath: destinationURL.path) { try FileManager.default.removeItem(at: destinationURL) } try FileManager.default.copyItem(at: localURL, to: destinationURL) print(" π νμΌ μ μ₯: \(destinationURL.path)") } catch { print("β νμΌ μ μ₯ μ€ν¨: \(error.localizedDescription)") } } // μ€νΈλ¦Ό μμ func session( _ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID ) { print("π‘ μ€νΈλ¦Ό μμ : \(streamName) from \(peerID.displayName)") // μ€νΈλ¦Ό μ²λ¦¬ λ‘μ§ κ΅¬ν } }
π 4. Browser & Advertiser Delegates
Peer κ²μκ³Ό μ΄λ μλ½ handling.
MultipeerManager+ServiceDelegate.swift β Service Delegate
import MultipeerConnectivity // Browser Delegate extension MultipeerManager: MCNearbyServiceBrowserDelegate { // Peer λ°κ²¬ func browser( _ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String: String]? ) { print("π Peer λ°κ²¬: \(peerID.displayName)") if let info = info { print(" Discovery Info: \(info)") } if !availablePeers.contains(peerID) { availablePeers.append(peerID) } } // Peer μ¬λΌμ§ func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) { print("β Peer μ¬λΌμ§: \(peerID.displayName)") availablePeers.removeAll { $0 == peerID } } // Browser μλ¬ func browser(_ browser: MCNearbyServiceBrowser, didNotStartBrowsingForPeers error: Error) { print("β Browsing μμ μ€ν¨: \(error.localizedDescription)") } } // Advertiser Delegate extension MultipeerManager: MCNearbyServiceAdvertiserDelegate { // μ΄λ μμ func advertiser( _ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void ) { print("βοΈ μ΄λ μμ : \(peerID.displayName)") // μλ μλ½ invitationHandler(true, session) // λλ μ¬μ©μμκ² νμΈ (μ€μ μ±μμλ UIλ‘ μ²λ¦¬) // invitationHandler(false, nil) // κ±°μ } // Advertiser μλ¬ func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didNotStartAdvertisingPeer error: Error) { print("β Advertising μμ μ€ν¨: \(error.localizedDescription)") } }
π± 5. SwiftUI Integration
P2P μ±ν μ± UI implementation.
MultipeerChatView.swift β P2P Chat UI
import SwiftUI struct MultipeerChatView: View { @State private var multipeerManager = MultipeerManager() @State private var messageText = "" @State private var showingPeerList = false var body: some View { NavigationStack { VStack(spacing: 0) { // μ°κ²° μν connectionStatus // λ©μμ§ λͺ©λ‘ messageList // μ λ ₯ μ°½ messageInput } .navigationTitle("P2P μ±ν ") .toolbar { ToolbarItem(placement: .topBarTrailing) { Button { showingPeerList = true } label: { Image(systemName: "person.2") } } } .sheet(isPresented: $showingPeerList) { peerListSheet } .onAppear { multipeerManager.startAdvertising() multipeerManager.startBrowsing() } .onDisappear { multipeerManager.stopAdvertising() multipeerManager.stopBrowsing() } } } // μ°κ²° μν ν€λ private var connectionStatus: some View { HStack { Circle() .fill(multipeerManager.connectedPeers.isEmpty ? Color.red : Color.green) .frame(width: 12, height: 12) if multipeerManager.connectedPeers.isEmpty { Text("μ°κ²°λ κΈ°κΈ° μμ") .font(.subheadline) .foregroundStyle(.secondary) } else { Text("\(multipeerManager.connectedPeers.count)κ° κΈ°κΈ° μ°κ²°λ¨") .font(.subheadline) .foregroundStyle(.secondary) } Spacer() } .padding() .background(Color(.systemGroupedBackground)) } // λ©μμ§ λͺ©λ‘ private var messageList: some View { ScrollView { LazyVStack(alignment: .leading, spacing: 12) { ForEach(multipeerManager.receivedMessages, id: \.self) { message in Text(message) .padding(12) .background(Color(.systemGray6)) .cornerRadius(12) } } .padding() } } // λ©μμ§ μ λ ₯ private var messageInput: some View { HStack { TextField("λ©μμ§ μ λ ₯...", text: $messageText) .textFieldStyle(.roundedBorder) .onSubmit { sendMessage() } Button { sendMessage() } label: { Image(systemName: "paperplane.fill") .font(.title3) } .disabled(messageText.isEmpty || multipeerManager.connectedPeers.isEmpty) } .padding() .background(Color(.systemBackground)) } // Peer λͺ©λ‘ μνΈ private var peerListSheet: some View { NavigationStack { List { Section("μ°κ²°λ κΈ°κΈ°") { if multipeerManager.connectedPeers.isEmpty { Text("μ°κ²°λ κΈ°κΈ° μμ") .foregroundStyle(.secondary) } else { ForEach(multipeerManager.connectedPeers, id: \.self) { peer in HStack { Image(systemName: "checkmark.circle.fill") .foregroundStyle(.green) Text(peer.displayName) } } } } Section("κ·Όμ² κΈ°κΈ°") { if multipeerManager.availablePeers.isEmpty { Text("κ²μλ κΈ°κΈ° μμ") .foregroundStyle(.secondary) } else { ForEach(multipeerManager.availablePeers, id: \.self) { peer in Button { multipeerManager.invitePeer(peer) } label: { HStack { Image(systemName: "iphone") Text(peer.displayName) Spacer() Image(systemName: "plus.circle") } } } } } } .navigationTitle("κΈ°κΈ° λͺ©λ‘") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarTrailing) { Button("λ«κΈ°") { showingPeerList = false } } } } } private func sendMessage() { multipeerManager.sendMessage(messageText) multipeerManager.receivedMessages.append("λ: \(messageText)") messageText = "" } }
π‘ HIG Guidelines
- Permission Settings: Add NSLocalNetworkUsageDescription, NSBonjourServices to Info.plist
- μλΉμ€ νμ : 15μ μ΄ν, μλ¬Έμ/μ«μ/νμ΄νλ§ μ¬μ©
- μνΈν: Set encryptionPreference to .required
- Battery Efficiency: Stop advertiser/browser when not in use
- μ°κ²° μ ν: μ΅λ 8λκΉμ§λ§ μ°κ²° κ°λ₯
π― Practical Usage
- Multiplayer Games: λ‘컬 λμ κ²μ
- File Sharing: AirDrop μ€νμΌ νμΌ μ μ‘
- νμ μ±: νμ΄νΈλ³΄λ, 곡λ νΈμ§
- 근거리 μ±ν : μΈν°λ· μμ΄ μ±ν
- IoT Control: κ·Όμ² Smart home device control
π Learn More
β‘οΈ Performance Tips:
MCSessionSendDataMode.reliable uses packet retransmission for reliability but is slower. For real-time games, .unreliable. Wi-Fi and BLE are automatically selected for optimal transfer.