πŸ‡ΊπŸ‡Έ EN

πŸ”— AccessorySetupKit 2

⭐ λ‚œμ΄λ„: ⭐⭐⭐ ⏱️ μ˜ˆμƒ μ‹œκ°„: 2h πŸ“‚ iOS 26

Bluetooth & Wi-Fi μ•‘μ„Έμ„œλ¦¬ κ°„νŽΈ νŽ˜μ–΄λ§

iOS 18+πŸ†• 2024

✨ AccessorySetupKit 2λž€?

AccessorySetupKit 2λŠ” iOS 18μ—μ„œ λŒ€ν­ κ°œμ„ λœ μ•‘μ„Έμ„œλ¦¬ μ„€μ • ν”„λ ˆμž„μ›Œν¬λ‘œ, Bluetooth 및 Wi-Fi κΈ°κΈ°λ₯Ό λ³„λ„μ˜ λ³΅μž‘ν•œ μ„€μ • 없이 κ°„νŽΈν•˜κ²Œ μ—°κ²°ν•  수 있게 ν•©λ‹ˆλ‹€. QR μ½”λ“œ μŠ€μΊ”, Matter ν”„λ‘œν† μ½œ 지원, HomeKit 톡합이 κ°•ν™”λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

πŸ’‘ 핡심 κΈ°λŠ₯: κ°„νŽΈ νŽ˜μ–΄λ§ Β· QR μ½”λ“œ 인증 Β· Matter 지원 Β· Wi-Fi ν”„λ‘œλΉ„μ €λ‹ Β· HomeKit 톡합 Β· λ³΄μ•ˆ μ—°κ²° Β· μžλ™ μž¬μ—°κ²°

🎯 1. κΈ°λ³Έ μ„€μ •

AccessorySetupKit μ„Έμ…˜μ„ μƒμ„±ν•˜κ³  μ•‘μ„Έμ„œλ¦¬λ₯Ό κ²€μƒ‰ν•©λ‹ˆλ‹€.

AccessoryManager.swift β€” κΈ°λ³Έ μ„€μ •
import AccessorySetupKit
import SwiftUI

@Observable
class AccessorySetupManager {
    var discoveredAccessories: [ASKAccessory] = []
    var connectedAccessory: ASKAccessory?
    private var session: ASKSession?

    // μ„Έμ…˜ μ‹œμž‘
    func startSession() async {
        // πŸ†• iOS 18+ μƒˆλ‘œμš΄ μ„Έμ…˜ 생성 방식
        let configuration = ASKSessionConfiguration()
        configuration.supportedProtocols = [.bluetooth, .wifi]
        configuration.enableBackgroundRefresh = true

        session = ASKSession(configuration: configuration)

        // μ•‘μ„Έμ„œλ¦¬ 검색 μ‹œμž‘
        do {
            for try await accessory in session!.discoverAccessories() {
                discoveredAccessories.append(accessory)
            }
        } catch {
            print("검색 μ‹€νŒ¨: \(error)")
        }
    }

    // μ•‘μ„Έμ„œλ¦¬ νŽ˜μ–΄λ§
    func pair(accessory: ASKAccessory) async throws {
        guard let session else { return }

        // μ‹œμŠ€ν…œ νŽ˜μ–΄λ§ UI ν‘œμ‹œ
        try await session.pair(accessory)
        connectedAccessory = accessory
    }

    // μ—°κ²° ν•΄μ œ
    func disconnect() async {
        guard let accessory = connectedAccessory,
              let session else { return }

        await session.disconnect(accessory)
        connectedAccessory = nil
    }

    // μ„Έμ…˜ μ’…λ£Œ
    func endSession() {
        session?.invalidate()
        session = nil
        discoveredAccessories.removeAll()
    }
}

πŸ“± 2. QR μ½”λ“œ νŽ˜μ–΄λ§

QR μ½”λ“œλ₯Ό μŠ€μΊ”ν•˜μ—¬ λΉ λ₯΄κ²Œ μ•‘μ„Έμ„œλ¦¬λ₯Ό μ—°κ²°ν•©λ‹ˆλ‹€.

QRPairing.swift β€” QR μ½”λ“œ νŽ˜μ–΄λ§
import AccessorySetupKit
import AVFoundation

@Observable
class QRPairingManager: NSObject {
    var scannedAccessory: ASKAccessory?
    private var session: ASKSession?

    // QR μ½”λ“œ μŠ€μΊ” 및 νŽ˜μ–΄λ§
    func scanAndPair() async throws {
        let configuration = ASKSessionConfiguration()
        configuration.pairingMethod = .qrCode // πŸ†• QR μ½”λ“œ λͺ¨λ“œ

        session = ASKSession(configuration: configuration)

        // μ‹œμŠ€ν…œ QR μŠ€μΊλ„ˆ ν‘œμ‹œ
        if let accessory = try await session?.scanQRCode() {
            scannedAccessory = accessory
            try await session?.pair(accessory)
        }
    }

    // Matter λ””λ°”μ΄μŠ€ QR μ½”λ“œ νŽ˜μ–΄λ§
    func pairMatterDevice(qrCode: String) async throws {
        let configuration = ASKSessionConfiguration()
        configuration.supportedProtocols = [.matter] // πŸ†• Matter 지원

        session = ASKSession(configuration: configuration)

        // QR μ½”λ“œμ—μ„œ λ””λ°”μ΄μŠ€ 정보 νŒŒμ‹±
        let accessory = try await session?.parseQRCode(qrCode)
        try await session?.pair(accessory!)
    }
}

πŸ“‘ 3. Wi-Fi ν”„λ‘œλΉ„μ €λ‹

μ•‘μ„Έμ„œλ¦¬μ— Wi-Fi 정보λ₯Ό μ „λ‹¬ν•˜μ—¬ λ„€νŠΈμ›Œν¬μ— μ—°κ²°ν•©λ‹ˆλ‹€.

WiFiProvisioning.swift β€” Wi-Fi μ„€μ •
import AccessorySetupKit
import NetworkExtension

@Observable
class WiFiProvisioningManager {
    private var session: ASKSession?
    var provisioningStatus: String = ""

    // Wi-Fi ν”„λ‘œλΉ„μ €λ‹ μ‹œμž‘
    func provisionWiFi(
        accessory: ASKAccessory,
        ssid: String,
        password: String
    ) async throws {
        let configuration = ASKSessionConfiguration()
        configuration.supportedProtocols = [.wifi]

        session = ASKSession(configuration: configuration)

        // πŸ†• Wi-Fi 자격 증λͺ… 전달
        let credentials = ASKWiFiCredentials(
            ssid: ssid,
            password: password,
            securityType: .wpa2
        )

        provisioningStatus = "μ—°κ²° 쀑..."

        try await session?.provisionWiFi(
            for: accessory,
            credentials: credentials
        )

        provisioningStatus = "μ—°κ²° μ™„λ£Œ"
    }

    // ν˜„μž¬ λ„€νŠΈμ›Œν¬ 정보 μžλ™ 전달
    func provisionCurrentNetwork(accessory: ASKAccessory) async throws {
        let configuration = ASKSessionConfiguration()
        configuration.supportedProtocols = [.wifi]
        configuration.autoProvisionCurrentNetwork = true // πŸ†• μžλ™ μ„€μ •

        session = ASKSession(configuration: configuration)

        // μ‹œμŠ€ν…œμ΄ μžλ™μœΌλ‘œ ν˜„μž¬ Wi-Fi 정보 전달
        try await session?.pair(accessory)
    }

    // Wi-Fi μ„€μ • μƒνƒœ λͺ¨λ‹ˆν„°λ§
    func monitorProvisioningStatus(
        accessory: ASKAccessory
    ) async {
        guard let session else { return }

        for await status in session.provisioningStatusUpdates(for: accessory) {
            switch status {
            case .connecting:
                provisioningStatus = "λ„€νŠΈμ›Œν¬ μ—°κ²° 쀑"
            case .connected:
                provisioningStatus = "μ—°κ²° μ™„λ£Œ"
            case .failed(let error):
                provisioningStatus = "μ—°κ²° μ‹€νŒ¨: \(error.localizedDescription)"
            }
        }
    }
}

🏠 4. HomeKit 톡합

HomeKitκ³Ό μ—°λ™ν•˜μ—¬ 슀마트 ν™ˆ μ•‘μ„Έμ„œλ¦¬λ₯Ό μ„€μ •ν•©λ‹ˆλ‹€.

HomeKitIntegration.swift β€” HomeKit 톡합
import AccessorySetupKit
import HomeKit

@Observable
class HomeKitAccessoryManager: NSObject, HMHomeManagerDelegate {
    private let homeManager = HMHomeManager()
    private var session: ASKSession?
    var homes: [HMHome] = []

    override init() {
        super.init()
        homeManager.delegate = self
    }

    // HomeKit μ•‘μ„Έμ„œλ¦¬ μΆ”κ°€
    func addHomeKitAccessory(
        to home: HMHome,
        accessory: ASKAccessory
    ) async throws {
        let configuration = ASKSessionConfiguration()
        configuration.supportedProtocols = [.homeKit, .matter] // πŸ†• HomeKit + Matter
        configuration.homeKitHome = home

        session = ASKSession(configuration: configuration)

        // νŽ˜μ–΄λ§ 및 HomeKit μΆ”κ°€
        try await session?.pair(accessory)

        // HomeKit μ„€μ • μžλ™ μ™„λ£Œ
        try await session?.configureHomeKit(for: accessory, in: home)
    }

    // Matter λ””λ°”μ΄μŠ€λ₯Ό HomeKit에 μΆ”κ°€
    func addMatterDevice(
        qrCode: String,
        to home: HMHome
    ) async throws {
        let configuration = ASKSessionConfiguration()
        configuration.supportedProtocols = [.matter]
        configuration.homeKitHome = home
        configuration.pairingMethod = .qrCode

        session = ASKSession(configuration: configuration)

        // QR μ½”λ“œλ‘œ Matter λ””λ°”μ΄μŠ€ νŽ˜μ–΄λ§
        if let accessory = try await session?.parseQRCode(qrCode) {
            try await session?.pair(accessory)
            try await session?.configureHomeKit(for: accessory, in: home)
        }
    }

    // HomeManager 델리게이트
    func homeManagerDidUpdateHomes(_ manager: HMHomeManager) {
        homes = manager.homes
    }
}

πŸ” 5. λ³΄μ•ˆ νŽ˜μ–΄λ§

μ•ˆμ „ν•œ νŽ˜μ–΄λ§κ³Ό κΆŒν•œ 관리λ₯Ό κ΅¬ν˜„ν•©λ‹ˆλ‹€.

SecurePairing.swift β€” λ³΄μ•ˆ νŽ˜μ–΄λ§
import AccessorySetupKit
import CryptoKit

@Observable
class SecurePairingManager {
    private var session: ASKSession?
    var authenticationStatus: String = ""

    // 인증 μ½”λ“œ 기반 νŽ˜μ–΄λ§
    func pairWithAuthCode(
        accessory: ASKAccessory,
        code: String
    ) async throws {
        let configuration = ASKSessionConfiguration()
        configuration.requiresAuthentication = true // πŸ†• 인증 ν•„μˆ˜

        session = ASKSession(configuration: configuration)

        // 인증 μ½”λ“œ 검증
        let isValid = try await session?.validateAuthCode(code, for: accessory)

        if isValid == true {
            authenticationStatus = "인증 성곡"
            try await session?.pair(accessory)
        } else {
            authenticationStatus = "인증 μ‹€νŒ¨"
            throw PairingError.authenticationFailed
        }
    }

    // PIN 기반 νŽ˜μ–΄λ§
    func pairWithPIN(
        accessory: ASKAccessory,
        pin: String
    ) async throws {
        let configuration = ASKSessionConfiguration()
        configuration.pairingMethod = .pin // πŸ†• PIN λͺ¨λ“œ

        session = ASKSession(configuration: configuration)

        // PIN 제곡 및 νŽ˜μ–΄λ§
        try await session?.pair(accessory, pin: pin)
    }

    // μ•”ν˜Έν™”λœ μ—°κ²° 확인
    func verifyEncryptedConnection(accessory: ASKAccessory) async -> Bool {
        guard let session else { return false }

        // μ—°κ²° λ³΄μ•ˆ μƒνƒœ 확인
        let securityLevel = await session.securityLevel(for: accessory)
        return securityLevel == .encrypted
    }
}

enum PairingError: Error {
    case authenticationFailed
    case invalidPIN
    case connectionLost
}

πŸ“± SwiftUI 톡합

AccessorySetupView.swift β€” μ’…ν•© 예제
import SwiftUI
import AccessorySetupKit

struct AccessorySetupView: View {
    @State private var manager = AccessorySetupManager()
    @State private var qrManager = QRPairingManager()
    @State private var wifiManager = WiFiProvisioningManager()
    @State private var showQRScanner = false

    var body: some View {
        NavigationStack {
            List {
                Section("κ²€μƒ‰λœ μ•‘μ„Έμ„œλ¦¬") {
                    if manager.discoveredAccessories.isEmpty {
                        ContentUnavailableView(
                            "μ•‘μ„Έμ„œλ¦¬ μ—†μŒ",
                            systemImage: "antenna.radiowaves.left.and.right.slash",
                            description: Text("검색을 μ‹œμž‘ν•˜μ„Έμš”")
                        )
                    } else {
                        ForEach(manager.discoveredAccessories, id: \.self.identifier) { accessory in
                            HStack {
                                VStack(alignment: .leading) {
                                    Text(accessory.name)
                                        .font(.headline)
                                    Text(accessory.category.rawValue)
                                        .font(.caption)
                                        .foregroundStyle(.secondary)
                                }

                                Spacer()

                                Button("μ—°κ²°") {
                                    Task {
                                        try? await manager.pair(accessory: accessory)
                                    }
                                }
                                .buttonStyle(.bordered)
                            }
                        }
                    }
                }

                if let connected = manager.connectedAccessory {
                    Section("μ—°κ²°λœ μ•‘μ„Έμ„œλ¦¬") {
                        HStack {
                            Image(systemName: "checkmark.circle.fill")
                                .foregroundStyle(.green)
                            Text(connected.name)
                            Spacer()
                            Button("μ—°κ²° ν•΄μ œ") {
                                Task {
                                    await manager.disconnect()
                                }
                            }
                        }
                    }
                }

                Section("μ„€μ • 방법") {
                    Button {
                        Task {
                            await manager.startSession()
                        }
                    } label: {
                        Label("Bluetooth 검색", systemImage: "dot.radiowaves.left.and.right")
                    }

                    Button {
                        showQRScanner = true
                    } label: {
                        Label("QR μ½”λ“œ μŠ€μΊ”", systemImage: "qrcode.viewfinder")
                    }
                }

                if !wifiManager.provisioningStatus.isEmpty {
                    Section("Wi-Fi μƒνƒœ") {
                        Text(wifiManager.provisioningStatus)
                            .foregroundStyle(.secondary)
                    }
                }
            }
            .navigationTitle("μ•‘μ„Έμ„œλ¦¬ μ„€μ •")
            .sheet(isPresented: $showQRScanner) {
                QRScannerView { qrCode in
                    Task {
                        try? await qrManager.pairMatterDevice(qrCode: qrCode)
                        showQRScanner = false
                    }
                }
            }
        }
    }
}

struct QRScannerView: View {
    let onScan: (String) -> Void
    @Environment(\.dismiss) var dismiss

    var body: some View {
        VStack {
            Text("μ•‘μ„Έμ„œλ¦¬μ˜ QR μ½”λ“œλ₯Ό μŠ€μΊ”ν•˜μ„Έμš”")
                .font(.headline)
                .padding()

            // QR μŠ€μΊλ„ˆ κ΅¬ν˜„
            Rectangle()
                .fill(Color.gray.opacity(0.3))
                .frame(width: 300, height: 300)
                .overlay {
                    Image(systemName: "qrcode.viewfinder")
                        .font(.system(size: 100))
                        .foregroundStyle(.white)
                }

            Button("μ·¨μ†Œ") {
                dismiss()
            }
            .padding()
        }
    }
}

πŸ’‘ HIG κ°€μ΄λ“œλΌμΈ

🎯 싀무 ν™œμš©

πŸ“š 더 μ•Œμ•„λ³΄κΈ°

⚑️ μ„±λŠ₯ 팁: Wi-Fi ν”„λ‘œλΉ„μ €λ‹ μ‹œ autoProvisionCurrentNetworkλ₯Ό ν™œμ„±ν™”ν•˜λ©΄ μ‚¬μš©μžκ°€ 직접 λΉ„λ°€λ²ˆν˜Έλ₯Ό μž…λ ₯ν•  ν•„μš” 없이 ν˜„μž¬ μ—°κ²°λœ λ„€νŠΈμ›Œν¬ 정보λ₯Ό μžλ™μœΌλ‘œ 전달할 수 μžˆμŠ΅λ‹ˆλ‹€.

πŸ“Ž Apple 곡식 자료

πŸ“˜ 곡식 λ¬Έμ„œ πŸ’» μƒ˜ν”Œ μ½”λ“œ 🎬 WWDC μ„Έμ…˜