๐ถ Wi-Fi Aware
Bluetooth ์์ด Wi-Fi๋ก ๊ทผ๊ฑฐ๋ฆฌ ๊ธฐ๊ธฐ ๋ฐ๊ฒฌ ๋ฐ P2P ํต์
โจ Wi-Fi Aware๋?
Wi-Fi Aware๋ iOS 18์์ ๋์ ๋ ์๋ก์ด ๊ทผ๊ฑฐ๋ฆฌ ํต์ ๊ธฐ์ ์ ๋๋ค. Bluetooth๋ฅผ ์ฌ์ฉํ์ง ์๊ณ Wi-Fi๋ง์ผ๋ก ์ฃผ๋ณ ๊ธฐ๊ธฐ๋ฅผ ๋ฐ๊ฒฌํ๊ณ ๋ฐ์ดํฐ๋ฅผ ๊ตํํ ์ ์์ต๋๋ค. ์ธํฐ๋ท ์ฐ๊ฒฐ์ด๋ ๊ธฐ์กด Wi-Fi ๋คํธ์ํฌ ์์ด๋ ๋์ํ๋ฉฐ, ์ ์ ๋ ฅ์ผ๋ก ํจ์จ์ ์ธ P2P(Peer-to-Peer) ํต์ ์ ์ ๊ณตํฉ๋๋ค.
๐ง 1. ํ๋ก์ ํธ ์ค์
Wi-Fi Aware๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด ํ๋ก์ ํธ์ ํ์ ๊ถํ๊ณผ Capability๋ฅผ ์ถ๊ฐํด์ผ ํฉ๋๋ค.
// Info.plist์ ์ถ๊ฐ NSLocalNetworkUsageDescription "๊ทผ์ฒ ๊ธฐ๊ธฐ์ Wi-Fi๋ฅผ ํตํด ์ฐ๊ฒฐํ๊ณ ๋ฐ์ดํฐ๋ฅผ ๊ณต์ ํฉ๋๋ค" // Bonjour ์๋น์ค ์ถ๊ฐ (์ต์ ) NSBonjourServices _wifiaware._tcp
๐ฏ 2. Wi-Fi Aware ์ธ์ ์์
NIWiFiAwareSession์ ์ฌ์ฉํ์ฌ Wi-Fi Aware ์ธ์ ์ ์์ํ๊ณ ์ฃผ๋ณ ๊ธฐ๊ธฐ๋ฅผ ๋ฐ๊ฒฌํฉ๋๋ค.
import NearbyInteraction import Network @Observable class WiFiAwareManager: NSObject { var session: NIWiFiAwareSession? var discoveryToken: NIWiFiAwareDiscoveryToken? var discoveredPeers: [NIWiFiAwarePeerToken] = [] var isRunning = false // ์ธ์ ์์ func startSession() { // Wi-Fi Aware ์ธ์ ์์ฑ session = NIWiFiAwareSession() session?.delegate = self // ์ธ์ ์์ ๋ฐ Discovery Token ์์ฑ do { discoveryToken = try session?.createDiscoveryToken() isRunning = true print("Wi-Fi Aware ์ธ์ ์์๋จ") } catch { print("์ธ์ ์์ ์คํจ: \(error)") } } // ์ธ์ ์ค์ง func stopSession() { session?.invalidate() session = nil discoveryToken = nil discoveredPeers.removeAll() isRunning = false print("Wi-Fi Aware ์ธ์ ์ข ๋ฃ๋จ") } // Discovery Token์ Data๋ก ๋ณํ (๊ณต์ ์ฉ) func getDiscoveryTokenData() -> Data? { guard let token = discoveryToken else { return nil } return try? NSKeyedArchiver.archivedData( withRootObject: token, requiringSecureCoding: true ) } // ์๋๋ฐฉ์ Discovery Token์ผ๋ก ํผ์ด ์ถ๊ฐ func addPeer(tokenData: Data) { do { let peerToken = try NSKeyedUnarchiver.unarchivedObject( ofClass: NIWiFiAwarePeerToken.self, from: tokenData ) if let peerToken = peerToken { // ํผ์ด ์ถ๊ฐ session?.add(peerToken) print("ํผ์ด ์ถ๊ฐ๋จ") } } catch { print("ํผ์ด ์ถ๊ฐ ์คํจ: \(error)") } } // ํผ์ด ์ ๊ฑฐ func removePeer(_ peerToken: NIWiFiAwarePeerToken) { session?.remove(peerToken) discoveredPeers.removeAll { $0 == peerToken } print("ํผ์ด ์ ๊ฑฐ๋จ") } } // Delegate ๊ตฌํ extension WiFiAwareManager: NIWiFiAwareSessionDelegate { // ์ธ์ ์ด ์์๋์์ ๋ func wifiAwareSession(_ session: NIWiFiAwareSession, didStartWith configuration: NIWiFiAwareConfiguration) { print("Wi-Fi Aware ์ธ์ ํ์ฑํ๋จ") } // ์ธ์ ์ด ์ค์ง๋์์ ๋ func wifiAwareSession(_ session: NIWiFiAwareSession, didInvalidateWith error: Error) { print("์ธ์ ๋ฌดํจํ: \(error)") isRunning = false } // ํผ์ด๊ฐ ๋ฐ๊ฒฌ๋์์ ๋ func wifiAwareSession(_ session: NIWiFiAwareSession, didDiscover peerToken: NIWiFiAwarePeerToken) { if !discoveredPeers.contains(peerToken) { discoveredPeers.append(peerToken) print("์ ํผ์ด ๋ฐ๊ฒฌ: \(peerToken)") } } // ํผ์ด ์ฐ๊ฒฐ์ด ๋์ด์ก์ ๋ func wifiAwareSession(_ session: NIWiFiAwareSession, didLose peerToken: NIWiFiAwarePeerToken) { discoveredPeers.removeAll { $0 == peerToken } print("ํผ์ด ์ฐ๊ฒฐ ๋๊น: \(peerToken)") } }
๐ก 3. ๋ฐ์ดํฐ ์ ์ก
Network ํ๋ ์์ํฌ๋ฅผ ์ฌ์ฉํ์ฌ Wi-Fi Aware๋ก ๋ฐ๊ฒฌํ ํผ์ด์ ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ณ ๋ฐ์ต๋๋ค.
import Network import NearbyInteraction @Observable class WiFiAwareDataChannel { var connection: NWConnection? var listener: NWListener? var receivedMessages: [String] = [] var isConnected = false // ๋ฐ์ดํฐ ๋ฆฌ์ค๋ ์์ (์๋ฒ ์ญํ ) func startListener(peerToken: NIWiFiAwarePeerToken) { let parameters = NWParameters.tcp parameters.requiredInterfaceType = .wifi do { listener = try NWListener(using: parameters) listener?.newConnectionHandler = { [weak self] newConnection in self?.handleNewConnection(newConnection) } listener?.stateUpdateHandler = { state in switch state { case .ready: print("๋ฆฌ์ค๋ ์ค๋น๋จ") case .failed(let error): print("๋ฆฌ์ค๋ ์คํจ: \(error)") default: break } } listener?.start(queue: .main) } catch { print("๋ฆฌ์ค๋ ์์ ์คํจ: \(error)") } } // ํผ์ด์ ์ฐ๊ฒฐ (ํด๋ผ์ด์ธํธ ์ญํ ) func connectToPeer(endpoint: NWEndpoint) { let parameters = NWParameters.tcp parameters.requiredInterfaceType = .wifi connection = NWConnection(to: endpoint, using: parameters) connection?.stateUpdateHandler = { [weak self] state in switch state { case .ready: self?.isConnected = true print("ํผ์ด ์ฐ๊ฒฐ ์๋ฃ") self?.receiveData() case .failed(let error): self?.isConnected = false print("์ฐ๊ฒฐ ์คํจ: \(error)") case .cancelled: self?.isConnected = false print("์ฐ๊ฒฐ ์ทจ์๋จ") default: break } } connection?.start(queue: .main) } // ์ ์ฐ๊ฒฐ ์ฒ๋ฆฌ private func handleNewConnection(_ newConnection: NWConnection) { connection = newConnection isConnected = true connection?.stateUpdateHandler = { [weak self] state in if state == .ready { self?.receiveData() } } connection?.start(queue: .main) } // ๋ฐ์ดํฐ ์ ์ก func sendData(_ message: String) { guard let data = message.data(using: .utf8) else { return } connection?.send( content: data, completion: .contentProcessed { error in if let error = error { print("์ ์ก ์คํจ: \(error)") } else { print("๋ฉ์์ง ์ ์ก ์๋ฃ: \(message)") } } ) } // ๋ฐ์ดํฐ ์์ private func receiveData() { connection?.receive(minimumIncompleteLength: 1, maximumLength: 65536) { [weak self] data, context, isComplete, error in if let data = data, !data.isEmpty { if let message = String(data: data, encoding: .utf8) { self?.receivedMessages.append(message) print("๋ฉ์์ง ์์ : \(message)") } } if error == nil { self?.receiveData() // ๊ณ์ ์์ ๋๊ธฐ } } } // ์ฐ๊ฒฐ ์ข ๋ฃ func disconnect() { connection?.cancel() listener?.cancel() connection = nil listener = nil isConnected = false } }
๐ 4. Discovery Token ๊ตํ
QR ์ฝ๋๋ ๋ค๋ฅธ ๋ฐฉ๋ฒ์ผ๋ก Discovery Token์ ๊ตํํ์ฌ ํผ์ด๋ฅผ ๋ฐ๊ฒฌํฉ๋๋ค.
import SwiftUI import CoreImage.filters class TokenExchangeHelper { // Token์ QR ์ฝ๋๋ก ๋ณํ func generateQRCode(from tokenData: Data) -> UIImage? { let filter = CIFilter.qrCodeGenerator() filter.message = tokenData if let outputImage = filter.outputImage { let transform = CGAffineTransform(scaleX: 10, y: 10) let scaledImage = outputImage.transformed(by: transform) let context = CIContext() if let cgImage = context.createCGImage(scaledImage, from: scaledImage.extent) { return UIImage(cgImage: cgImage) } } return nil } // Base64 ๋ฌธ์์ด๋ก ๋ณํ (๊ฐ๋จํ ๊ณต์ ) func encodeTokenToString(_ tokenData: Data) -> String { return tokenData.base64EncodedString() } // Base64 ๋ฌธ์์ด์ Token Data๋ก ๋ณํ func decodeTokenFromString(_ string: String) -> Data? { return Data(base64Encoded: string) } }
๐ฑ 5. SwiftUI ํตํฉ
Wi-Fi Aware ๊ธฐ๋ฅ์ SwiftUI ์ฑ์ ํตํฉํฉ๋๋ค.
import SwiftUI import NearbyInteraction struct WiFiAwareView: View { @State private var manager = WiFiAwareManager() @State private var dataChannel = WiFiAwareDataChannel() @State private var tokenHelper = TokenExchangeHelper() @State private var messageToSend = "" @State private var showQRCode = false @State private var qrCodeImage: UIImage? var body: some View { NavigationStack { List { // ์ธ์ ์ํ Section("์ธ์ ") { HStack { Text("์ํ") Spacer() Text(manager.isRunning ? "ํ์ฑ" : "๋นํ์ฑ") .foregroundStyle(manager.isRunning ? .green : .secondary) } if manager.isRunning { Button("๋ด QR ์ฝ๋ ํ์") { if let tokenData = manager.getDiscoveryTokenData() { qrCodeImage = tokenHelper.generateQRCode(from: tokenData) showQRCode = true } } } Button(manager.isRunning ? "์ธ์ ์ค์ง" : "์ธ์ ์์") { if manager.isRunning { manager.stopSession() dataChannel.disconnect() } else { manager.startSession() } } .foregroundStyle(manager.isRunning ? .red : .blue) } // ๋ฐ๊ฒฌ๋ ํผ์ด Section("๋ฐ๊ฒฌ๋ ๊ธฐ๊ธฐ (\(manager.discoveredPeers.count))") { if manager.discoveredPeers.isEmpty { Text("์ฃผ๋ณ์ ๊ธฐ๊ธฐ๊ฐ ์์ต๋๋ค") .foregroundStyle(.secondary) } else { ForEach(manager.discoveredPeers.indices, id: \.self) { index in HStack { Image(systemName: "wifi") .foregroundStyle(.blue) Text("๊ธฐ๊ธฐ \(index + 1)") Spacer() if dataChannel.isConnected { Image(systemName: "checkmark.circle.fill") .foregroundStyle(.green) } } } } } // ๋ฉ์์ง ์ ์ก if dataChannel.isConnected { Section("๋ฉ์์ง ์ ์ก") { TextField("๋ฉ์์ง ์ ๋ ฅ", text: $messageToSend) Button("์ ์ก") { dataChannel.sendData(messageToSend) messageToSend = "" } .disabled(messageToSend.isEmpty) } } // ์์ ๋ฉ์์ง Section("์์ ๋ฉ์์ง") { if dataChannel.receivedMessages.isEmpty { Text("์์ ๋ ๋ฉ์์ง๊ฐ ์์ต๋๋ค") .foregroundStyle(.secondary) } else { ForEach(dataChannel.receivedMessages, id: \.self) { message in Text(message) } } } } .navigationTitle("Wi-Fi Aware") .sheet(isPresented: $showQRCode) { QRCodeView(qrCodeImage: qrCodeImage) } } } } struct QRCodeView: View { let qrCodeImage: UIImage? @Environment(\.dismiss) var dismiss var body: some View { NavigationStack { VStack(spacing: 20) { Text("๋ค๋ฅธ ๊ธฐ๊ธฐ์์ ์ด QR ์ฝ๋๋ฅผ ์ค์บํ์ธ์") .font(.headline) if let image = qrCodeImage { Image(uiImage: image) .interpolation(.none) .resizable() .scaledToFit() .frame(width: 300, height: 300) .padding() .background(Color.white) .cornerRadius(12) .shadow(radius: 4) } else { Text("QR ์ฝ๋ ์์ฑ ์คํจ") .foregroundStyle(.secondary) } } .padding() .navigationTitle("๋ด QR ์ฝ๋") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .confirmationAction) { Button("์๋ฃ") { dismiss() } } } } } }
๐ฎ 6. ์ค์ ์์ - ๋ฉํฐํ๋ ์ด์ด ๊ฒ์
Wi-Fi Aware๋ฅผ ์ฌ์ฉํ ๊ฐ๋จํ ๋ฉํฐํ๋ ์ด์ด ๊ฒ์ ์์ ์ ๋๋ค.
import SwiftUI struct GameMessage: Codable { enum Action: String, Codable { case move, attack, heal } let playerName: String let action: Action let position: CGPoint? let timestamp: Date } @Observable class GameController { let wifiManager = WiFiAwareManager() let dataChannel = WiFiAwareDataChannel() var playerName = "Player" var playerPosition: CGPoint = .zero var opponentPosition: CGPoint = .zero var gameLog: [String] = [] func startGame() { wifiManager.startSession() } func sendAction(action: GameMessage.Action, position: CGPoint? = nil) { let message = GameMessage( playerName: playerName, action: action, position: position, timestamp: Date() ) if let data = try? JSONEncoder().encode(message), let string = String(data: data, encoding: .utf8) { dataChannel.sendData(string) gameLog.append("\(playerName): \(action.rawValue)") } } func processReceivedMessage(_ messageString: String) { guard let data = messageString.data(using: .utf8), let message = try? JSONDecoder().decode(GameMessage.self, from: data) else { return } if let position = message.position { opponentPosition = position } gameLog.append("\(message.playerName): \(message.action.rawValue)") } } struct WiFiAwareGameView: View { @State private var controller = GameController() var body: some View { VStack { // ๊ฒ์ ํ๋ฉด ZStack { Color.gray.opacity(0.2) // ๋ด ์บ๋ฆญํฐ Circle() .fill(Color.blue) .frame(width: 40, height: 40) .position(controller.playerPosition) // ์๋ ์บ๋ฆญํฐ Circle() .fill(Color.red) .frame(width: 40, height: 40) .position(controller.opponentPosition) } .frame(height: 300) .gesture( DragGesture() .onChanged { value in controller.playerPosition = value.location controller.sendAction(action: .move, position: value.location) } ) // ์ก์ ๋ฒํผ HStack(spacing: 20) { Button("๊ณต๊ฒฉ") { controller.sendAction(action: .attack) } .buttonStyle(.borderedProminent) .tint(.red) Button("ํ") { controller.sendAction(action: .heal) } .buttonStyle(.borderedProminent) .tint(.green) } .padding() // ๊ฒ์ ๋ก๊ทธ List(controller.gameLog, id: \.self) { log in Text(log) .font(.caption) } .frame(height: 150) } .onAppear { controller.startGame() } } }
๐ก HIG ๊ฐ์ด๋๋ผ์ธ
- ๊ถํ ์ค๋ช : Wi-Fi ์ฌ์ฉ ๋ชฉ์ ์ ๋ช ํํ ์ค๋ช
- ๋ฐฐํฐ๋ฆฌ ํจ์จ: ํ์ํ ๋๋ง ์ธ์ ํ์ฑํ
- ์ฐ๊ฒฐ ์ํ ํ์: ์ฌ์ฉ์์๊ฒ ์ฐ๊ฒฐ ์ํ๋ฅผ ์๊ฐ์ ์ผ๋ก ํ์
- ์ค๋ฅ ์ฒ๋ฆฌ: Wi-Fi ๋นํ์ฑํ ๋ฑ ์ค๋ฅ ์ํฉ ๋๋น
- ๋ฐฑ๊ทธ๋ผ์ด๋ ์ง์: ๋ฐฑ๊ทธ๋ผ์ด๋ ๋ชจ๋ ์ค์ ์ ์ฐ์ ๋ฐ๊ฒฌ ๊ฐ๋ฅ
๐ฏ ์ค์ ํ์ฉ
- ๋ฉํฐํ๋ ์ด์ด ๊ฒ์: ๊ทผ๊ฑฐ๋ฆฌ ์ค์๊ฐ ๊ฒ์
- ํ์ผ ๊ณต์ : AirDrop ๋์, ํฐ ํ์ผ ๋น ๋ฅด๊ฒ ์ ์ก
- ํ์ ๋๊ตฌ: ๊ณต๋ ํธ์ง, ํ์ดํธ๋ณด๋
- IoT ๊ธฐ๊ธฐ ์ ์ด: ์ค๋งํธ ํ, ์ผ์ ๋ฐ์ดํฐ ์์ง
- ์คํ๋ผ์ธ ๋ฉ์์ง: ์ธํฐ๋ท ์์ด ๋ฉ์์ง ๊ตํ