๐ฑ Core NFC
NFC ํ๊ทธ ์ฝ๊ธฐ/์ฐ๊ธฐ ๋ฐ NDEF ๋ฉ์์ง ์ฒ๋ฆฌ ํ๋ ์์ํฌ
iOS 11+NDEF ์ฝ๊ธฐ/์ฐ๊ธฐ
โจ Core NFC๋?
Core NFC๋ iPhone์ NFC ์นฉ์ ์ฌ์ฉํ์ฌ NFC ํ๊ทธ๋ฅผ ์ฝ๊ณ ์ธ ์ ์๋ ํ๋ ์์ํฌ์ ๋๋ค. NDEF(NFC Data Exchange Format) ๋ฉ์์ง๋ฅผ ์ฝ์ด URL, ํ ์คํธ, ์ฐ๋ฝ์ฒ ์ ๋ณด ๋ฑ์ ์ฒ๋ฆฌํ๊ฑฐ๋ ์๋ก์ด ๋ฐ์ดํฐ๋ฅผ ํ๊ทธ์ ์ธ ์ ์์ต๋๋ค. Apple Pay๋ฅผ ์ ์ธํ ๋ชจ๋ NFC ํ์ฉ ์ฌ๋ก์ ์ฌ์ฉ๋๋ฉฐ, iPhone 7 ์ด์์์ ์ง์๋ฉ๋๋ค.
๐ก ํต์ฌ ๊ธฐ๋ฅ: NDEF ํ๊ทธ ์ฝ๊ธฐ ยท ํ๊ทธ ์ฐ๊ธฐ(iOS 13+) ยท URL/ํ
์คํธ ํ์ฑ ยท ๋ค์ค ๋ ์ฝ๋ ์ง์ ยท ๋ฐฑ๊ทธ๋ผ์ด๋ ํ๊ทธ ์ฝ๊ธฐ ยท ISO 7816/ISO 15693 ์ง์
๐ 1. NDEF ํ๊ทธ ์ฝ๊ธฐ
NFCNDEFReaderSession์ ์ฌ์ฉํ์ฌ NFC ํ๊ทธ๋ฅผ ์ค์บํ๊ณ NDEF ๋ฉ์์ง๋ฅผ ์ฝ์ต๋๋ค.
NFCReaderManager.swift โ NFC ํ๊ทธ ์ฝ๊ธฐ
import CoreNFC import SwiftUI @Observable class NFCReaderManager: NSObject { var session: NFCNDEFReaderSession? var detectedMessages: [NFCNDEFMessage] = [] var isScanning = false var lastError: String? // NFC ์ง์ ์ฌ๋ถ ํ์ธ var isNFCAvailable: Bool { NFCNDEFReaderSession.readingAvailable } // ํ๊ทธ ์ฝ๊ธฐ ์์ func startScanning() { guard isNFCAvailable else { lastError = "์ด ๊ธฐ๊ธฐ๋ NFC๋ฅผ ์ง์ํ์ง ์์ต๋๋ค" print("โ NFC ๋ฏธ์ง์") return } detectedMessages.removeAll() lastError = nil // NDEF Reader Session ์์ฑ session = NFCNDEFReaderSession( delegate: self, queue: nil, invalidateAfterFirstRead: false ) // ์ฌ์ฉ์์๊ฒ ํ์๋ ๋ฉ์์ง session?.alertMessage = "NFC ํ๊ทธ๋ฅผ iPhone ์๋จ์ ๊ฐ๊น์ด ๋์ฃผ์ธ์" // ์ค์บ ์์ session?.begin() isScanning = true print("๐ NFC ์ค์บ ์์") } // ์ค์บ ์ค์ง func stopScanning() { session?.invalidate() isScanning = false print("โน๏ธ NFC ์ค์บ ์ค์ง") } // NDEF ๋ ์ฝ๋ ํ์ฑ func parseNDEFRecords(from message: NFCNDEFMessage) -> [String] { var results: [String] = [] for record in message.records { // Type Name Format let tnf = record.typeNameFormat switch tnf { case .nfcWellKnown: // URI ๋ ์ฝ๋ if let url = record.wellKnownTypeURIPayload() { results.append("๐ URL: \(url.absoluteString)") } // Text ๋ ์ฝ๋ else if let text = String(data: record.payload, encoding: .utf8) { results.append("๐ Text: \(text)") } case .absoluteURI: if let text = String(data: record.payload, encoding: .utf8) { results.append("๐ URI: \(text)") } case .media: let mimeType = String(data: record.type, encoding: .utf8) ?? "unknown" results.append("๐ Media Type: \(mimeType)") case .empty: results.append("โช๏ธ Empty record") @unknown default: results.append("โ Unknown record type") } // Payload ํฌ๊ธฐ results.append(" Size: \(record.payload.count) bytes") } return results } }
๐ก 2. NDEF Reader ๋ธ๋ฆฌ๊ฒ์ดํธ
ํ๊ทธ ๋ฐ๊ฒฌ ๋ฐ ๋ฉ์์ง ์ฝ๊ธฐ๋ฅผ ์ฒ๋ฆฌํฉ๋๋ค.
NFCReaderManager+Delegate.swift โ Reader ๋ธ๋ฆฌ๊ฒ์ดํธ
import CoreNFC extension NFCReaderManager: NFCNDEFReaderSessionDelegate { // ํ๊ทธ ๋ฐ๊ฒฌ func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) { print("๐ฑ ํ๊ทธ ๋ฐ๊ฒฌ! ๋ฉ์์ง ๊ฐ์: \(messages.count)") // ๋ฉ์์ง ์ ์ฅ detectedMessages.append(contentsOf: messages) // ๊ฐ ๋ฉ์์ง ์ฒ๋ฆฌ for (index, message) in messages.enumerated() { print(" ๋ฉ์์ง \(index + 1): \(message.records.count)๊ฐ ๋ ์ฝ๋") for (recordIndex, record) in message.records.enumerated() { print(" ๋ ์ฝ๋ \(recordIndex + 1):") // URL ํ์ฑ if let url = record.wellKnownTypeURIPayload() { print(" ๐ URL: \(url.absoluteString)") } // Text ํ์ฑ if let text = parseTextPayload(record.payload) { print(" ๐ Text: \(text)") } } } // ์ฑ๊ณต ๋ฉ์์ง session.alertMessage = "โ \(messages.count)๊ฐ ๋ฉ์์ง๋ฅผ ์ฝ์์ต๋๋ค" } // ํ๊ทธ ๋ฐ๊ฒฌ (๊ณ ๊ธ API) func readerSession(_ session: NFCNDEFReaderSession, didDetect tags: [NFCNDEFTag]) { guard tags.count == 1 else { session.alertMessage = "โ ๏ธ ํ๊ทธ๊ฐ ๋๋ฌด ๋ง์ต๋๋ค. ํ๋๋ง ๊ฐ๊น์ด ๋์ฃผ์ธ์" session.restartPolling() return } let tag = tags.first! // ํ๊ทธ ์ฐ๊ฒฐ session.connect(to: tag) { error in if let error = error { session.invalidate(errorMessage: "โ ํ๊ทธ ์ฐ๊ฒฐ ์คํจ: \(error.localizedDescription)") return } print("โ ํ๊ทธ ์ฐ๊ฒฐ ์ฑ๊ณต") // NDEF ๋ฉ์์ง ์ฝ๊ธฐ tag.queryNDEFStatus { status, capacity, error in if let error = error { session.invalidate(errorMessage: "โ ์ํ ํ์ธ ์คํจ: \(error.localizedDescription)") return } print("๐ ํ๊ทธ ์ํ: \(status.rawValue), ์ฉ๋: \(capacity) bytes") switch status { case .notSupported: session.invalidate(errorMessage: "โ NDEF๋ฅผ ์ง์ํ์ง ์๋ ํ๊ทธ์ ๋๋ค") case .readOnly: self.readNDEFMessage(from: tag, session: session) case .readWrite: self.readNDEFMessage(from: tag, session: session) @unknown default: session.invalidate(errorMessage: "โ ์ ์ ์๋ ํ๊ทธ ์ํ") } } } } // NDEF ๋ฉ์์ง ์ฝ๊ธฐ private func readNDEFMessage(from tag: NFCNDEFTag, session: NFCNDEFReaderSession) { tag.readNDEF { message, error in if let error = error { session.invalidate(errorMessage: "โ ์ฝ๊ธฐ ์คํจ: \(error.localizedDescription)") return } if let message = message { self.detectedMessages.append(message) session.alertMessage = "โ \(message.records.count)๊ฐ ๋ ์ฝ๋๋ฅผ ์ฝ์์ต๋๋ค" print("๐ NDEF ๋ฉ์์ง ์ฝ๊ธฐ ์ฑ๊ณต") } session.invalidate() } } // ์ธ์ ๋ฌดํจํ func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) { isScanning = false if let nfcError = error as? NFCReaderError { switch nfcError.code { case .readerSessionInvalidationErrorUserCanceled: print("โน๏ธ ์ฌ์ฉ์๊ฐ ์ค์บ์ ์ทจ์ํ์ต๋๋ค") case .readerSessionInvalidationErrorSessionTimeout: print("โฑ๏ธ ์ธ์ ์๊ฐ ์ด๊ณผ") lastError = "์ค์บ ์๊ฐ์ด ์ด๊ณผ๋์์ต๋๋ค" case .readerSessionInvalidationErrorFirstNDEFTagRead: print("โ ์ฒซ ๋ฒ์งธ ํ๊ทธ ์ฝ๊ธฐ ์๋ฃ") default: print("โ NFC ์๋ฌ: \(nfcError.localizedDescription)") lastError = nfcError.localizedDescription } } else { print("โ ์๋ฌ: \(error.localizedDescription)") lastError = error.localizedDescription } } // Text Payload ํ์ฑ private func parseTextPayload(_ payload: Data) -> String? { guard payload.count > 1 else { return nil } // Status byte: bit 7 = UTF-16 ์ฌ๋ถ, bit 0-5 = ์ธ์ด ์ฝ๋ ๊ธธ์ด let statusByte = payload[0] let isUTF16 = (statusByte & 0x80) != 0 let langCodeLength = Int(statusByte & 0x3F) guard payload.count > langCodeLength + 1 else { return nil } // ํ ์คํธ ์ถ์ถ let textData = payload.subdata(in: (1 + langCodeLength)..<payload.count) let encoding: String.Encoding = isUTF16 ? .utf16 : .utf8 return String(data: textData, encoding: encoding) } }
โ๏ธ 3. NFC ํ๊ทธ ์ฐ๊ธฐ
NDEF ๋ฉ์์ง๋ฅผ ํ๊ทธ์ ์๋๋ค (iOS 13+).
NFCWriterManager.swift โ NFC ํ๊ทธ ์ฐ๊ธฐ
import CoreNFC @Observable class NFCWriterManager: NSObject { var session: NFCNDEFReaderSession? var messageToWrite: NFCNDEFMessage? var writeSuccess = false // URL์ ํ๊ทธ์ ์ฐ๊ธฐ func writeURL(_ url: URL) { guard NFCNDEFReaderSession.readingAvailable else { print("โ NFC ๋ฏธ์ง์") return } // NDEF Payload ์์ฑ guard let payload = NFCNDEFPayload.wellKnownTypeURIPayload(url: url) else { print("โ Payload ์์ฑ ์คํจ") return } messageToWrite = NFCNDEFMessage(records: [payload]) // ์ธ์ ์์ session = NFCNDEFReaderSession(delegate: self, queue: nil, invalidateAfterFirstRead: false) session?.alertMessage = "์ฐ๊ธฐ๋ฅผ ์ํ๋ NFC ํ๊ทธ๋ฅผ ๊ฐ๊น์ด ๋์ฃผ์ธ์" session?.begin() print("โ๏ธ URL ์ฐ๊ธฐ ์์: \(url.absoluteString)") } // ํ ์คํธ๋ฅผ ํ๊ทธ์ ์ฐ๊ธฐ func writeText(_ text: String) { guard NFCNDEFReaderSession.readingAvailable else { return } // Text Payload ์์ฑ guard let payload = createTextPayload(text: text, languageCode: "en") else { print("โ Text Payload ์์ฑ ์คํจ") return } messageToWrite = NFCNDEFMessage(records: [payload]) session = NFCNDEFReaderSession(delegate: self, queue: nil, invalidateAfterFirstRead: false) session?.alertMessage = "์ฐ๊ธฐ๋ฅผ ์ํ๋ NFC ํ๊ทธ๋ฅผ ๊ฐ๊น์ด ๋์ฃผ์ธ์" session?.begin() print("โ๏ธ ํ ์คํธ ์ฐ๊ธฐ ์์: \(text)") } // ์ฌ๋ฌ ๋ ์ฝ๋๋ฅผ ํ๊ทธ์ ์ฐ๊ธฐ func writeMultipleRecords(url: URL, text: String) { guard NFCNDEFReaderSession.readingAvailable else { return } var records: [NFCNDEFPayload] = [] // URL Payload if let urlPayload = NFCNDEFPayload.wellKnownTypeURIPayload(url: url) { records.append(urlPayload) } // Text Payload if let textPayload = createTextPayload(text: text, languageCode: "en") { records.append(textPayload) } messageToWrite = NFCNDEFMessage(records: records) session = NFCNDEFReaderSession(delegate: self, queue: nil, invalidateAfterFirstRead: false) session?.alertMessage = "์ฐ๊ธฐ๋ฅผ ์ํ๋ NFC ํ๊ทธ๋ฅผ ๊ฐ๊น์ด ๋์ฃผ์ธ์" session?.begin() print("โ๏ธ ๋ค์ค ๋ ์ฝ๋ ์ฐ๊ธฐ ์์") } // Text Payload ์์ฑ private func createTextPayload(text: String, languageCode: String) -> NFCNDEFPayload? { guard let textData = text.data(using: .utf8) else { return nil } guard let langData = languageCode.data(using: .utf8) else { return nil } // Status byte: UTF-8, ์ธ์ด ์ฝ๋ ๊ธธ์ด var payload = Data() payload.append(UInt8(langData.count)) payload.append(langData) payload.append(textData) return NFCNDEFPayload( format: .nfcWellKnown, type: "T".data(using: .utf8)!, identifier: Data(), payload: payload ) } } extension NFCWriterManager: NFCNDEFReaderSessionDelegate { // ํ๊ทธ ๋ฐ๊ฒฌ (์ฐ๊ธฐ์ฉ) func readerSession(_ session: NFCNDEFReaderSession, didDetect tags: [NFCNDEFTag]) { guard tags.count == 1, let tag = tags.first else { session.alertMessage = "โ ๏ธ ํ๊ทธ๊ฐ ๋๋ฌด ๋ง์ต๋๋ค" session.restartPolling() return } session.connect(to: tag) { error in if let error = error { session.invalidate(errorMessage: "โ ์ฐ๊ฒฐ ์คํจ: \(error.localizedDescription)") return } // ํ๊ทธ ์ํ ํ์ธ tag.queryNDEFStatus { status, capacity, error in if let error = error { session.invalidate(errorMessage: "โ ์ํ ํ์ธ ์คํจ: \(error.localizedDescription)") return } guard status == .readWrite else { session.invalidate(errorMessage: "โ ์ฝ๊ธฐ ์ ์ฉ ํ๊ทธ์ ๋๋ค") return } // ๋ฉ์์ง ์ฐ๊ธฐ guard let message = self.messageToWrite else { session.invalidate(errorMessage: "โ ์ธ ๋ฉ์์ง๊ฐ ์์ต๋๋ค") return } tag.writeNDEF(message) { error in if let error = error { session.invalidate(errorMessage: "โ ์ฐ๊ธฐ ์คํจ: \(error.localizedDescription)") } else { session.alertMessage = "โ ์ฐ๊ธฐ ์ฑ๊ณต!" self.writeSuccess = true print("โ NFC ํ๊ทธ ์ฐ๊ธฐ ์๋ฃ") session.invalidate() } } } } } func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) { // ์ฐ๊ธฐ ๋ชจ๋์์๋ ์ฌ์ฉํ์ง ์์ } func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) { if let nfcError = error as? NFCReaderError, nfcError.code != .readerSessionInvalidationErrorUserCanceled { print("โ ์ธ์ ์ข ๋ฃ: \(error.localizedDescription)") } } }
๐ฑ 4. SwiftUI ํตํฉ
NFC ๋ฆฌ๋/๋ผ์ดํฐ UI๋ฅผ ๊ตฌํํฉ๋๋ค.
NFCReaderView.swift โ NFC UI
import SwiftUI struct NFCReaderView: View { @State private var readerManager = NFCReaderManager() @State private var writerManager = NFCWriterManager() @State private var urlText = "https://developer.apple.com" @State private var writeText = "Hello NFC!" var body: some View { NavigationStack { Form { // ์ฝ๊ธฐ ์น์ Section("ํ๊ทธ ์ฝ๊ธฐ") { Button { readerManager.startScanning() } label: { Label("NFC ํ๊ทธ ์ค์บ", systemImage: "wave.3.right") } .disabled(!readerManager.isNFCAvailable || readerManager.isScanning) if readerManager.isScanning { HStack { ProgressView() Text("์ค์บ ์ค...") .foregroundStyle(.secondary) } } } // ์ฝ์ ๋ฉ์์ง if !readerManager.detectedMessages.isEmpty { Section("์ค์บ ๊ฒฐ๊ณผ") { ForEach(readerManager.detectedMessages.indices, id: \.self) { index in let message = readerManager.detectedMessages[index] VStack(alignment: .leading, spacing: 8) { Text("๋ฉ์์ง \(index + 1)") .font(.headline) ForEach(readerManager.parseNDEFRecords(from: message), id: \.self) { record in Text(record) .font(.caption) .foregroundStyle(.secondary) } } } } } // ์ฐ๊ธฐ ์น์ Section("ํ๊ทธ ์ฐ๊ธฐ") { TextField("URL", text: $urlText) .keyboardType(.URL) .autocapitalization(.none) Button { if let url = URL(string: urlText) { writerManager.writeURL(url) } } label: { Label("URL ์ฐ๊ธฐ", systemImage: "link") } .disabled(!readerManager.isNFCAvailable) TextField("ํ ์คํธ", text: $writeText) Button { writerManager.writeText(writeText) } label: { Label("ํ ์คํธ ์ฐ๊ธฐ", systemImage: "text.cursor") } .disabled(!readerManager.isNFCAvailable) } // ์ํ Section { Text("NFC ์ง์: \(readerManager.isNFCAvailable ? "โ " : "โ")") .foregroundStyle(.secondary) if let error = readerManager.lastError { Text("์๋ฌ: \(error)") .foregroundStyle(.red) .font(.caption) } } } .navigationTitle("NFC ๋ฆฌ๋/๋ผ์ดํฐ") } } }
๐ก HIG ๊ฐ์ด๋๋ผ์ธ
- ๊ถํ ์ค์ : Info.plist์ NFCReaderUsageDescription ์ถ๊ฐ ํ์
- Entitlements: Near Field Communication Tag Reading ๊ถํ ํ์ฑํ
- ์ฌ์ฉ์ ์๋ด: alertMessage๋ก ๋ช ํํ ์ง์์ฌํญ ์ ๊ณต
- ํ์์์: 60์ด ์ค์บ ์ ํ ์๊ฐ ๊ณ ๋ ค
- ๋ฐฑ๊ทธ๋ผ์ด๋ ์ฝ๊ธฐ: ์ฑ์ด ๋ฐฑ๊ทธ๋ผ์ด๋์ ์์ด๋ ํน์ NDEF ํ๊ทธ ์ฝ๊ธฐ ๊ฐ๋ฅ
๐ฏ ์ค์ ํ์ฉ
- ์ค๋งํธ ํฌ์คํฐ: URL, ์ฐ๋ฝ์ฒ ์ ๋ณด๊ฐ ๋ด๊ธด ํฌ์คํฐ
- ์ ํ ์ธ์ฆ: NFC ํ๊ทธ๋ก ์ ํ ์ธ์ฆ
- ์ถ์ ๊ด๋ฆฌ: NFC ํ๊ทธ ๊ธฐ๋ฐ ์ถ์ ์ฆ
- ์์ฐ ์ถ์ : ์ฅ๋น/์ฌ๊ณ ๊ด๋ฆฌ
- ๋ฐ๋ฌผ๊ด ๊ฐ์ด๋: ์ ์๋ฌผ ์ ๋ณด ์ ๊ณต
๐ ๋ ์์๋ณด๊ธฐ
โก๏ธ ์ฑ๋ฅ ํ: NFC ์ค์บ์ 60์ด ํ ์๋ ์ข
๋ฃ๋ฉ๋๋ค.
invalidateAfterFirstRead๋ฅผ false๋ก ์ค์ ํ๋ฉด ์ฌ๋ฌ ํ๊ทธ๋ฅผ ์ฐ์์ผ๋ก ์ฝ์ ์ ์์ต๋๋ค. ์ฐ๊ธฐ ์์
์ ์๋ ๋ฐ๋์ ํ๊ทธ์ ์ฉ๋๊ณผ ์ํ๋ฅผ ํ์ธํ์ธ์.