π 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 λΉλ°λ²νΈ λ±) μμ νκ² μ²λ¦¬
- μ€λ₯ μ²λ¦¬: μ°κ²° μ€ν¨ μ λͺ νν ν΄κ²° λ°©λ² μ μ
- κΆν: Bluetooth λ° λ€νΈμν¬ κΆν μ€λͺ μΆκ°
π― μ€λ¬΄ νμ©
- μ€λ§νΈ ν: HomeKit μ‘μΈμ리 κ°νΈ μ€μ
- μ¨μ΄λ¬λΈ: νΌνΈλμ€ νΈλ컀, μ€λ§νΈμμΉ νμ΄λ§
- IoT λλ°μ΄μ€: Matter κΈ°λ° μ€λ§νΈ λλ°μ΄μ€
- μ€λμ€: Bluetooth ν€λν°/μ€νΌμ»€ μ°κ²°
- ν¬μ€μΌμ΄: μλ£ κΈ°κΈ° μμ ν νμ΄λ§
π λ μμ보기
- AccessorySetupKit 곡μ λ¬Έμ
- WWDC 2024: AccessorySetupKit
- HomeKit κ°λ°μ κ°μ΄λ
- Matter νμ€
β‘οΈ μ±λ₯ ν: Wi-Fi νλ‘λΉμ λ μ
autoProvisionCurrentNetworkλ₯Ό νμ±ννλ©΄ μ¬μ©μκ° μ§μ λΉλ°λ²νΈλ₯Ό μ
λ ₯ν νμ μμ΄ νμ¬ μ°κ²°λ λ€νΈμν¬ μ 보λ₯Ό μλμΌλ‘ μ λ¬ν μ μμ΅λλ€.