[iOS] Multipeer Connectivity 소개 및 예제 프로젝트

Lawn·2022년 7월 15일
1

🧑🏻‍💻 Study

목록 보기
2/6
post-thumbnail

안녕하세요 🌱 Lawn입니다 프로젝트를 통해 공부한 Multipeer Connectivity를 정리해보겠습니다.

🌱 Multipeer Connectivity

Multipeer Connectivity는 P2P 연결 및 주변 장치 검색을 지원합니다.

iOS 7.0+ iPadOS 7.0+ macOS 10.10+ Mac Catalyst 13.0+ tvOS 10.0+


🍀 Overview

Multipeer Connectivity 프레임워크는 주변 장치에서 제공하는 서비스 검색을 지원하고 메시지 기반 데이터, 스트리밍 데이터 및 리소스(예: 파일)를 통해 해당 서비스와의 통신을 지원합니다. iOS에서 프레임워크 전송을 위해 Wi-Fi 네트워크, P2P Wi-Fi 및 Bluetooth를 사용합니다. macOS 및 tvOS에서는 Wi-Fi, P2P Wi-Fi 및 이더넷을 사용합니다.

❗️Tip : 로컬 네트워크를 사용하는 앱은 Info.plist에 NSLocalNetworkUsageDescription 키와 함께 사용 문자열을 제공해야 합니다. Bonjour를 사용하는 앱은 NSBonjourServices 키를 사용하여 탐색하는 서비스도 선언해야 합니다.


🍀 Architecture

Multipeer Connectivity 프레임워크로 작업할 때 앱은 여러 유형의 개체와 상호 작용해야 합니다.

  • MCSession : 연결된 피어 장치 간의 통신을 지원합니다. 앱은 세션을 생성하고 피어가 연결 초대를 수락하면 여기에 피어를 추가하고, 다른 피어가 연결하도록 초대하면 세션을 생성합니다. 세션 개체는 세션에 연결된 피어를 나타내는 피어 ID 개체 집합을 유지 및 관리합니다.

  • MCNearbyServiceAdvertiser : 앱이 지정된 유형의 세션에 참여할 의향이 있음을 주변 피어에게 알립니다. advertiser object는 단일 로컬 피어를 사용하여 장치와 해당 사용자를 식별하는 정보를 근처의 다른 장치에 제공합니다.

  • MCAdvertiserAssistant : advertiser object와 동일한 기능을 제공하지만 사용자가 초대를 수락할 수 있는 표준 사용자 인터페이스도 제공합니다. 고유한 사용자 인터페이스를 제공하거나 표시되는 초대를 프로그래밍 방식으로 추가 제어하려는 경우 advertiser object를 직접 사용합니다.

  • MCNearbyServiceBrowser : 앱이 특정 유형의 세션을 지원하는 앱이 있는 주변 장치를 프로그래밍 방식으로 검색할 수 있습니다.

  • MCBrowserViewController : 사용자가 세션에 추가할 주변 피어를 선택할 수 있는 표준 사용자 인터페이스를 제공합니다.

  • MCPeerID : 장치에서 실행 중인 앱을 주변 피어에게 고유하게 식별합니다.


🍀 Example Project

Multipeer Connectivity 프레임워크를 사용해 iOS 장치 간에 통신하는 방법을 알아보겠습니다. Multipeer Connectivity 프레임워크는 Bonjour 프로토콜 위에 레이어를 제공합니다. 프레임워크는 두 장치가 동일한 Wi-Fi 네트워크에 있는 경우, P2P Wi-Fi 또는 Blutooth 를 선택해서 연결합니다.

이 튜토리얼은 멀티피어 연결 프레임워크를 사용하여 iOS 장치 간에 통신하는 방법을 보여줍니다. 이 예제에서는 여러 장치 간에 색상 값을 동기화하는 ConnectedColors앱을 구현하는 방법을 보여줍니다


🛠 Project setup

  1. 최신버전의 Xcode
  2. SwiftUI 기반의 iOS App ConnectedColors 생성
  3. 새로운 Class인 ColorMultipeerSession 생성

  • MCPeerID는 장치를 고유하게 식별합니다. displayName은 다른 장치에서 볼 수 있습니다.

  • MCNearbyServiceAdvertiser가 서비스를 알립니다.

  • MCNearbyServiceBrowser는 네트워크에서 서비스를 찾습니다.

  • MCSession은 연결된 모든 장치(피어)를 관리하고 메시지 송수신을 허용합니다.

❗️ Tip 다음 코드를 Starting point로 작성하세요. 이코드는 단순히 delegate handling이 필요한 boilerplate code입니다. 이벤트를 기록하고 서비스를 알리는것 말고는 아무 동작을 하지 않습니다.

import MultipeerConnectivity
import os

class ColorMultipeerSession: NSObject, ObservableObject {

//serviceType은 서비스를 식별합니다
//(최대 15자 길이의 고유한 문자열이어야 하며 ASCII 소문자, 숫자 및 하이픈만 포함할 수 있습니다).

    private let serviceType = "example-color"
private let myPeerId = MCPeerID(displayName: UIDevice.current.name)
private let serviceAdvertiser: MCNearbyServiceAdvertiser
private let serviceBrowser: MCNearbyServiceBrowser
private let session: MCSession
    private let log = Logger()

    override init() {
        session = MCSession(peer: myPeerId, securityIdentity: nil, encryptionPreference: .none)
        serviceAdvertiser = MCNearbyServiceAdvertiser(peer: myPeerId, discoveryInfo: nil, serviceType: serviceType)
        serviceBrowser = MCNearbyServiceBrowser(peer: myPeerId, serviceType: serviceType)

        super.init()

        session.delegate = self
        serviceAdvertiser.delegate = self
        serviceBrowser.delegate = self

        serviceAdvertiser.startAdvertisingPeer()
        serviceBrowser.startBrowsingForPeers()
    }

    deinit {
        serviceAdvertiser.stopAdvertisingPeer()
        serviceBrowser.stopBrowsingForPeers()
    }
}

extension ColorMultipeerSession: MCNearbyServiceAdvertiserDelegate {
    func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didNotStartAdvertisingPeer error: Error) {
        log.error("ServiceAdvertiser didNotStartAdvertisingPeer: \(String(describing: error))")
    }

    func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
        log.info("didReceiveInvitationFromPeer \(peerID)")
    }
}

extension ColorMultipeerSession: MCNearbyServiceBrowserDelegate {
    func browser(_ browser: MCNearbyServiceBrowser, didNotStartBrowsingForPeers error: Error) {
        log.error("ServiceBrowser didNotStartBrowsingForPeers: \(String(describing: error))")
    }

    func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String: String]?) {
        log.info("ServiceBrowser found peer: \(peerID)")
    }

    func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {
        log.info("ServiceBrowser lost peer: \(peerID)")
    }
}

extension ColorMultipeerSession: MCSessionDelegate {
    func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
        log.info("peer \(peerID) didChangeState: \(state.rawValue)")
    }

    func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
        log.info("didReceive bytes \(data.count) bytes")
    }

    public func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {
        log.error("Receiving streams is not supported")
    }

    public func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {
        log.error("Receiving resources is not supported")
    }

    public func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) {
        log.error("Receiving resources is not supported")
    }
}

1. ContentView에 ColorMultipeerSession의 인스턴스를 생성하세요

class ContentView: View {
    @StateObject var colorSession = ColorMultipeerSession()

    // ...
}

2. 다음과같이 두가지 Bonjour services key를 Info.plist안에 추가하세요(service type의 이름은 꼭 같아야합니다.)

3. app을 실행하세요 (simulator or device).

❗️ Tip : (Optionally) 로컬 네트워크에서 service가 올바르게 advertiseing되는지 확인합니다.

Listing all Bonjour services on the local network

4. 두 피어를 초대하고 수락해 연결합니다.

피어를 찾으면 연결합니다. 만약 연결을 위한 초대를 받았다면 수락을 통해 연결합니다. ObservableObject 프로토콜을 구현하고 연결된 피어 목록과 함께 @Published 속성을 유지합니다. 피어 상태가 변경되면 업데이트합니다.

class ColorMultipeerSession: NSObject, ObservableObject {
    // ...

    @Published var connectedPeers: [MCPeerID] = []
}

extension ColorMultipeerSession: MCNearbyServiceBrowserDelegate {

    func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String: String]?) {
        log.info("ServiceBrowser found peer: \(peerID)")
        browser.invitePeer(peerID, to: session, withContext: nil, timeout: 10)
    }

    // ...

}

extension ColorMultipeerSession: MCNearbyServiceAdvertiserDelegate {

    func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
        log.info("didReceiveInvitationFromPeer \(peerID)")
        invitationHandler(true, session)
    }

    // ...

}

extension ColorMultipeerSession: MCSessionDelegate {

    func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
        log.info("peer \(peerID) didChangeState: \(state.rawValue)")
        DispatchQueue.main.async {
    connectedPeers = session.connectedPeers
}
    }

    // ...
}

❗️ Tip: 이 코드는 모든 피어를 자동으로 초대합니다. MCBrowserViewController 클래스를 사용하여 피어를 검색하고 수동으로 초대할 수 있습니다.

또한 이 코드는 들어오는 모든 연결을 자동으로 수락합니다. 따라서 피어를 신뢰할 수 없으므로 네트워크를 통해 수신하는 모든 데이터를 확인하고 삭제하는데 매우 주의해야합니다. 세션을 비공개로 유지하려면 사용자에게 이를 알리고 들어오는 연결을 확인하도록 요청해야 합니다. 이것은 MCAdvertiserAssistant 클래스를 사용하여 구현할 수 있습니다.

5. Color values를 보내거나 받습니다.

currentColor 속성을 추가하고 메시지가 수신되면 업데이트합니다. 새로운 색상 값을 보내는 방법을 제공합니다. MCSessionDelegate에서 값을 받으면 디코딩하고 currentColor 속성을 업데이트합니다. 다음 예제에서는 열거형을 사용하여 값을 인코딩 및 디코딩합니다.

enum NamedColor: String, CaseIterable {
    case red, green, yellow
}

class ColorMultipeerSession: NSObject, ObservableObject {

    // ...

    @Published var currentColor: NamedColor? = nil

    /// ...

    func send(color: NamedColor) {
    log.info("sendColor: \(String(describing: color)) to \(self.session.connectedPeers.count) peers")
    self.currentColor = color

    if !session.connectedPeers.isEmpty {
        do {
            try session.send(color.rawValue.data(using: .utf8)!, toPeers: session.connectedPeers, with: .reliable)
        } catch {
            log.error("Error for sending: \(String(describing: error))")
        }
    }
}

}

extension ColorMultipeerSession: MCSessionDelegate {

    // ...

    func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
    if let string = String(data: data, encoding: .utf8), let color = NamedColor(rawValue: string) {
        log.info("didReceive color \(string)")
        DispatchQueue.main.async {
            self.currentColor = color
        }
    } else {
        log.info("didReceive invalid value \(data.count) bytes")
    }
}
}

6. UI를 업데이트 합니다.

현재 연결된 장치의 이름을 표시하고 현재 색상을 배경으로 사용하고 값을 보낼 수 있는 View를 제공합니다.

struct ContentView: View {
    @StateObject var colorSession = ColorMultipeerSession()

    var body: some View {
        VStack(alignment: .leading) {
            Text("Connected Devices:")
            Text(String(describing: colorSession.connectedPeers.map(\.displayName)))

            Divider()

            HStack {
                ForEach(NamedColor.allCases, id: \.self) { color in
                    Button(color.rawValue) {
                        colorSession.send(color: color)
                    }
                    .padding()
                }
            }
            Spacer()
        }
        .padding()
        .background((colorSession.currentColor.map(\.color) ?? .clear).ignoresSafeArea())
    }
}

extension NamedColor {
    var color: Color {
        switch self {
        case .red:
            return .red
        case .green:
            return .green
        case .yellow:
            return .yellow
        }
    }
}

7. 업데이트된 앱을 실행하고 두 기기를 가지고 테스트를 합니다.

profile
안녕하세요 글쓰는 🌱풀떼기 입니다.

0개의 댓글