๐ฑ Core NFC
โญ Difficulty: โญโญโญ
โฑ๏ธ Est. Time: 2h
๐ System & Network
NFC tag read/write and NDEF message processing framework
iOS 11+NDEF ์ฝ๊ธฐ/์ฐ๊ธฐ
โจ Core NFC?
Core NFC is a framework that uses the iPhone's NFC chip to read and write NFC tags. It reads NDEF (NFC Data Exchange Format) messages to process URLs, text, contact info, or write new data to tags. It's used for all NFC use cases except Apple Pay and is supported on iPhone 7 and later.
๐ก Key Features: NDEF Tag Reading ยท Tag Writing (iOS 13+) ยท URL/Text Parsing ยท Multi-Record Support ยท Background Tag Reading ยท ISO 7816/ISO 15693 Support
๐ 1. NDEF ํ๊ทธ ์ฝ๊ธฐ
Use NFCNDEFReaderSession to scan NFC tags and read NDEF messages.
NFCReaderManager.swift โ NFC Tag Reading
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 ๋ธ๋ฆฌ๊ฒ์ดํธ
ํ๊ทธ ๋ฐ๊ฒฌ ๋ฐ ๋ฉ์์ง ์ฝ๊ธฐ handling.
NFCReaderManager+Delegate.swift โ Reader Delegate
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 Tag Writing
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 Integration
NFC ๋ฆฌ๋/๋ผ์ดํฐ UI implementation.
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 Guidelines
- Permission Settings: Must add NFCReaderUsageDescription to Info.plist
- Entitlements: Enable Near Field Communication Tag Reading capability
- ์ฌ์ฉ์ ์๋ด: alertMessage๋ก ๋ช ํํ ์ง์์ฌํญ ์ ๊ณต
- ํ์์์: 60์ด ์ค์บ ์ ํ ์๊ฐ ๊ณ ๋ ค
- ๋ฐฑ๊ทธ๋ผ์ด๋ ์ฝ๊ธฐ: Specific NDEF tags can be read even when the app is in the background
๐ฏ Practical Usage
- ์ค๋งํธ ํฌ์คํฐ: URL, ์ฐ๋ฝ์ฒ ์ ๋ณด๊ฐ ๋ด๊ธด ํฌ์คํฐ
- ์ ํ ์ธ์ฆ: NFC ํ๊ทธ๋ก ์ ํ ์ธ์ฆ
- ์ถ์ ๊ด๋ฆฌ: NFC ํ๊ทธ ๊ธฐ๋ฐ ์ถ์ ์ฆ
- ์์ฐ ์ถ์ : ์ฅ๋น/์ฌ๊ณ ๊ด๋ฆฌ
- ๋ฐ๋ฌผ๊ด ๊ฐ์ด๋: ์ ์๋ฌผ ์ ๋ณด ์ ๊ณต
๐ Learn More
โก๏ธ Performance Tips: NFC ์ค์บ์ 60์ด ํ ์๋ ์ข
๋ฃ.
invalidateAfterFirstRead to false to read multiple tags consecutively. Always check tag capacity and status before writing.