🌐 KO

πŸ”— 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

🎯 Practical Usage

πŸ“š 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.

πŸ“Ž Apple Official Resources

πŸ“˜ Documentation 🎬 WWDC Sessions