๐ŸŒ KO

๐Ÿ“ฑ 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

๐ŸŽฏ Practical Usage

๐Ÿ“š Learn More

โšก๏ธ Performance Tips: NFC ์Šค์บ”์€ 60์ดˆ ํ›„ ์ž๋™ ์ข…๋ฃŒ. invalidateAfterFirstRead to false to read multiple tags consecutively. Always check tag capacity and status before writing.

๐Ÿ“Ž Apple Official Resources

๐Ÿ“˜ Documentation ๐Ÿ’ป Sample Code ๐ŸŽฌ WWDC Sessions