반응형
NFC NDEF tag 및 feliCa, iso7816, iso15693, miFare tag 읽기 / 쓰기 관련 클래스를 만들어 보았다.
//
// HiNfcManager.swift
// NFC
//
// Created by netcanis on 2023/04/18.
//
import UIKit
import CoreNFC
public enum HiNFCError: Error {
case unavailable
case notSupported
case readOnly
case invalidPayloadSize
case invalidated(errorDescription: String)
}
open class HiNFCManager: NSObject {
static let shared = HiNFCManager()
public typealias DidBecomeActive = (HiNFCManager) -> Void
public typealias DidDetect = (HiNFCManager, Result<[String: Any]?, HiNFCError>) -> Void
private enum HiNFCAction {
case read
case write(message: NFCNDEFMessage)
}
// MARK: - Properties
private var didBecomeActive: DidBecomeActive?
private var didDetect: DidDetect?
private var action: HiNFCAction?
// MARK: - Properties (NDEF, Tag)
open private(set) var ndefSession: NFCNDEFReaderSession?
open private(set) var tagSession: Any?
// MARK: - NFCNDEFTag
open func read(didBecomeActive: DidBecomeActive? = nil, didDetect: @escaping DidDetect) {
guard NFCNDEFReaderSession.readingAvailable else {
self.didDetect?(self, .failure(.unavailable))
return
}
let session = NFCNDEFReaderSession(delegate: self,
queue: nil,
invalidateAfterFirstRead: true)
action = .read
startSession(session: session, didBecomeActive: didBecomeActive, didDetect: didDetect)
}
open func write(message: [String: Any], didBecomeActive: DidBecomeActive? = nil, didDetect: @escaping DidDetect) {
guard NFCNDEFReaderSession.readingAvailable else {
self.didDetect?(self, .failure(.unavailable))
return
}
if #available(iOS 13.0, *) {
let session = NFCNDEFReaderSession(delegate: self,
queue: nil,
invalidateAfterFirstRead: false)
let payload = message["payload"] as? String ?? ""
let type = message["type"] as? String ?? "T"
let format: NFCTypeNameFormat = (type == "T") ? (.media) : (.absoluteURI)
let payloadData = payload.data(using: .utf8)!
let typeData = type.data(using: .utf8)!
let ndefPayload = NFCNDEFPayload(format: format, type: typeData, identifier: Data(), payload: payloadData)
let ndefMessage = NFCNDEFMessage(records: [ndefPayload])
action = .write(message: ndefMessage)
startSession(session: session, didBecomeActive: didBecomeActive, didDetect: didDetect)
}
}
open func setMessage(_ alertMessage: String) {
ndefSession?.alertMessage = alertMessage
}
// MARK: - NFCTag
open func readTag(didBecomeActive: DidBecomeActive? = nil, didDetect: @escaping DidDetect) {
guard NFCReaderSession.readingAvailable else {
self.didDetect?(self, .failure(.unavailable))
return
}
if #available(iOS 13.0, *) {
let session = NFCTagReaderSession(pollingOption: [.iso14443, .iso15693, .iso18092],
delegate: self,
queue: nil)!
action = .read
startSession(session: session, didBecomeActive: didBecomeActive, didDetect: didDetect)
}
}
open func writeTag(message: [String: Any], didBecomeActive: DidBecomeActive? = nil, didDetect: @escaping DidDetect) {
guard NFCReaderSession.readingAvailable else {
self.didDetect?(self, .failure(.unavailable))
return
}
if #available(iOS 13.0, *) {
let session = NFCTagReaderSession(pollingOption: [.iso14443, .iso15693, .iso18092],
delegate: self,
queue: nil)!
let payload = message["payload"] as? String ?? ""
let type = message["type"] as? String ?? "T"
let format: NFCTypeNameFormat = (type == "T") ? (.media) : (.absoluteURI)
let payloadData = payload.data(using: .utf8)!
let typeData = type.data(using: .utf8)!
let ndefPayload = NFCNDEFPayload(format: format, type: typeData, identifier: Data(), payload: payloadData)
let ndefMessage = NFCNDEFMessage(records: [ndefPayload])
action = .write(message: ndefMessage)
startSession(session: session, didBecomeActive: didBecomeActive, didDetect: didDetect)
}
}
open func setTagMessage(_ alertMessage: String) {
if #available(iOS 13.0, *) {
if let session = self.tagSession as? NFCTagReaderSession {
session.alertMessage = alertMessage
}
}
}
}
// MARK: - NFCNDEFReaderSessionDelegate
extension HiNFCManager : NFCNDEFReaderSessionDelegate {
open func readerSessionDidBecomeActive(_ session: NFCNDEFReaderSession) {
self.didBecomeActive?(self)
}
open func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {
// iOS 13미만
guard let message = messages.first, let record = message.records.first else {
self.didDetect?(self, .failure(.invalidated(errorDescription: "NDEF message나 message record가 없습니다.")))
self.invalidate(errorMessage: "NDEF message나 message record가 없습니다.")
return
}
let language = String(data: record.payload.advanced(by: 1), encoding: .utf8)
let encoding = record.payload[0] & NFCTypeNameFormat.nfcWellKnown.rawValue
let textData = record.payload.advanced(by: 3)
let text = String(data: textData, encoding: .utf8)
let result: [String: Any] = [
"Type":record.type.string,
"Format": self.formattedTNF(from: record.typeNameFormat),
"Value": [
"Encoding": "\(encoding)",
"Language": "\(language ?? "")",
"Text": "\(text ?? "")"
],
"Raw value": record.payload.string,
"Payload": "\(record.payload.hexStringFormatted)",
"Size": "\(record.payload.count)",
]
print("\(String(describing: result.toJsonString()))")
self.didDetect?(self, .success(result))
self.invalidate(errorMessage: "NDEF message read successfully.")
}
@available(iOS 13.0, *)
open func readerSession(_ session: NFCNDEFReaderSession, didDetect tags: [NFCNDEFTag]) {
guard tags.count == 1, let tag = tags.first else {
DispatchQueue.global().asyncAfter(deadline: .now() + .microseconds(500)) {
session.restartPolling()
}
return
}
session.connect(to: tag) { [weak self] error in
guard let self = self else { return }
if error != nil {
self.didDetect?(self, .failure(.invalidated(errorDescription: error!.localizedDescription)))
self.invalidate(errorMessage: error?.localizedDescription)
return
}
tag.queryNDEFStatus { status, capacity, error in
switch (status, self.action) {
case (.notSupported, _):
self.didDetect?(self, .failure(.notSupported))
self.invalidate(errorMessage: error?.localizedDescription)
case (.readOnly, _):
self.didDetect?(self, .failure(.readOnly))
case (.readWrite, .read):
tag.readNDEF { message, error in
if error != nil {
self.didDetect?(self, .failure(.invalidated(errorDescription: error!.localizedDescription)))
self.invalidate(errorMessage: error?.localizedDescription)
return
}
let record = message?.records.first
let result: [String: Any] = [
"Type":record?.type.string ?? "",
"Format": self.formattedTNF(from: record!.typeNameFormat),
"Raw value": record?.payload.string ?? "",
"Payload": "\(record?.payload.hexStringFormatted ?? "")",
"Size": "\(record?.payload.count ?? 0)",
]
self.didDetect?(self, .success(result))
self.invalidate(errorMessage: error?.localizedDescription)
}
case (.readWrite, .write(let message)):
guard message.length <= capacity else {
self.didDetect?(self, .failure(.invalidPayloadSize))
self.invalidate(errorMessage: "Invalid payload size")
return
}
tag.writeNDEF(message) { error in
if error != nil {
self.didDetect?(self, .failure(.invalidated(errorDescription: error!.localizedDescription)))
self.invalidate(errorMessage: error!.localizedDescription)
return
}
let result: [String: Any] = [
"result":message.records.first?.payload.string ?? ""
]
self.didDetect?(self, .success(result))
self.invalidate(errorMessage: error?.localizedDescription)
}
default:
return
}
}
}
}
open func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {
if let error = error as? NFCReaderError,
error.code != .readerSessionInvalidationErrorFirstNDEFTagRead &&
error.code != .readerSessionInvalidationErrorUserCanceled {
self.didDetect?(self, .failure(.invalidated(errorDescription: error.localizedDescription)))
}
self.ndefSession = nil
self.tagSession = nil
self.didBecomeActive = nil
self.didDetect = nil
}
}
// MARK: - Private helper functions
extension HiNFCManager {
private func invalidate(errorMessage: String?) {
if errorMessage != nil {
if #available(iOS 13.0, *) {
if let ns = ndefSession {
ns.invalidate(errorMessage: errorMessage!)
}
if let ts = tagSession as? NFCTagReaderSession {
ts.invalidate(errorMessage: errorMessage!)
}
} else {
if let ns = ndefSession {
ns.invalidate()
}
if #available(iOS 13.0, *) {
if let ts = tagSession as? NFCTagReaderSession {
ts.invalidate()
}
}
}
} else {
if let ns = ndefSession {
ns.invalidate()
}
if #available(iOS 13.0, *) {
if let ts = tagSession as? NFCTagReaderSession {
ts.invalidate()
}
}
}
ndefSession = nil
tagSession = nil
didBecomeActive = nil
didDetect = nil
}
private func startSession(session: NFCNDEFReaderSession,
didBecomeActive: DidBecomeActive?,
didDetect: @escaping DidDetect) {
self.ndefSession = session
self.didBecomeActive = didBecomeActive
self.didDetect = didDetect
session.begin()
}
@available(iOS 13.0, *)
private func startSession(session: NFCTagReaderSession,
didBecomeActive: DidBecomeActive?,
didDetect: @escaping DidDetect) {
self.tagSession = session
self.didBecomeActive = didBecomeActive
self.didDetect = didDetect
session.begin()
}
private func formattedTNF(from tnf: NFCTypeNameFormat) -> String {
switch tnf {
case .empty: // 0: 이름 없는 레코드 (빈 문자열로 표시됨)
return "Empty (0x00)"
case .nfcWellKnown: // 1: NFC Forum에서 정의한 레코드 형식
return "NFC Well Known (0x01)"
case .media: // 2: 미디어 타입의 레코드 (ex. 'audio/mp3')
return "Media (0x02)"
case .absoluteURI: // 3: URI 형식의 레코드
return "Absolute URI (0x03)"
case .nfcExternal: // 4: NFC 포럼에서 정의한 외부 레코드
return "NFC External (0x04)"
case .unchanged: // 6: 이름 형식 변경 없음. (현재의 이름 형식을 유지)
return "Unchanged (0x06)"
default: // 5: 알 수 없는 레코드
return "Unknown (0x05)"
}
}
}
// MARK: - NFCTagReaderSessionDelegate
@available(iOS 13.0, *)
extension HiNFCManager : NFCTagReaderSessionDelegate {
public func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession) {
self.didBecomeActive?(self)
}
public func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
guard tags.count == 1, let tag = tags.first else {
DispatchQueue.global().asyncAfter(deadline: .now() + .microseconds(500)) {
session.restartPolling()
}
return
}
var ndefTag: NFCNDEFTag
switch tag {
case let .feliCa(tag): /// FeliCa tag. (NFCFeliCaTag)
ndefTag = tag
self.parseFeliCaTag(tag)
case let .iso7816(tag): /// ISO14443-4 type A / B tag with ISO7816 communication. (NFCISO7816Tag)
ndefTag = tag
self.parseISO7816Tag(tag)
case let .iso15693(tag):/// ISO15693 tag.
ndefTag = tag
self.parseISO15693Tag(tag)
case let .miFare(tag): /// MiFare technology tag (MIFARE Plus, UltraLight, DESFire) base on ISO14443. (NFCMiFareTag)
ndefTag = tag
self.parseMIFARETag(tag)
@unknown default:
self.didDetect?(self, .failure(.invalidated(errorDescription: "Tag not valid.")))
self.invalidate(errorMessage: "Tag not valid.")
return
}
session.connect(to: tag) { [weak self] error in
guard let self = self else { return }
if error != nil {
self.didDetect?(self, .failure(.invalidated(errorDescription: error!.localizedDescription)))
self.invalidate(errorMessage: error?.localizedDescription)
return
}
ndefTag.queryNDEFStatus { status, capacity, error in
switch (status, self.action) {
case (.notSupported, _):
self.didDetect?(self, .failure(.notSupported))
self.invalidate(errorMessage: error?.localizedDescription)
case (.readOnly, _):
self.didDetect?(self, .failure(.readOnly))
case (.readWrite, .read):
ndefTag.readNDEF { (message, error) in
if error != nil || message == nil {
self.didDetect?(self, .failure(.invalidated(errorDescription: error!.localizedDescription)))
self.invalidate(errorMessage: error?.localizedDescription)
return
}
let record = message?.records.first
let result: [String: Any] = [
"Type":record?.type.string ?? "",
"Format": self.formattedTNF(from: record!.typeNameFormat),
"Raw value": record?.payload.string ?? "",
"Payload": "\(record?.payload.hexStringFormatted ?? "")",
"Size": "\(record?.payload.count ?? 0)",
]
self.didDetect?(self, .success(result))
self.invalidate(errorMessage: error?.localizedDescription)
}
case (.readWrite, .write(let message)):
guard message.length <= capacity else {
self.didDetect?(self, .failure(.invalidPayloadSize))
self.invalidate(errorMessage: "Invalid payload size")
return
}
ndefTag.writeNDEF(message) { error in
if error != nil {
self.didDetect?(self, .failure(.invalidated(errorDescription: error!.localizedDescription)))
self.invalidate(errorMessage: error!.localizedDescription)
return
}
let result: [String: Any] = [
"result":message.records.first?.payload.string ?? ""
]
self.didDetect?(self, .success(result))
self.invalidate(errorMessage: error?.localizedDescription)
}
default:
return
}
}
}
}
public func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) {
// NFC 태그 읽기가 중단되었을 때, 호출된다.
print("NFCTag 읽기 중단: \(error.localizedDescription)")
}
}
// MARK: - Private helper functions (for NFCTag)
@available(iOS 13.0, *)
extension HiNFCManager {
/// FeliCa tag. (NFCFeliCaTag)
private func parseFeliCaTag(_ tag: NFCFeliCaTag) {
let log = """
------------------------------------
:::::::::: [ FeliCa tag ] ::::::::::
"- Type : FeliCa tag."
"- currentIDm : \(String(describing: tag.currentIDm))"
"- currentSystemCode : \(String(describing: tag.currentSystemCode))"
"- description : \(String(describing: tag.description))"
------------------------------------
"""
print(log)
}
/// ISO14443-4 type A / B tag with ISO7816 communication. (NFCISO7816Tag)
private func parseISO7816Tag(_ tag: NFCISO7816Tag) {
let log = """
------------------------------------
:::::::::: [ ISO7816 tag ] ::::::::::
"- Type : ISO14443-4 type A / B tag with ISO7816 communication."
"- Identifier : \(String(describing: tag.identifier))"
"- historicalBytes : \(String(describing: tag.historicalBytes?.hexStringFormatted))"
"- applicationData : \(String(describing: tag.applicationData?.hexStringFormatted))"
"- initialSelectedAID : \(tag.initialSelectedAID)"
"- proprietaryApplicationDataCoding : \(tag.proprietaryApplicationDataCoding)"
"- description : \(String(describing: tag.description))"
------------------------------------
"""
print(log)
}
/// ISO15693 tag.
private func parseISO15693Tag(_ tag: NFCISO15693Tag) {
let log = """
------------------------------------
:::::::::: [ ISO15693 tag ] ::::::::::
"- Type : ISO15693 tag."
"- Identifier : \(tag.identifier.hexString)"
"- icSerialNumber : \(String(describing: tag.icSerialNumber.hexStringFormatted))" // IC 시리얼 번호(IC serial number)
"- icManufacturerCode : \(String(describing: tag.icManufacturerCode))" // C 제조사 코드(IC manufacturer code)
"- description : \(String(describing: tag.description))"
------------------------------------
"""
print(log)
}
/// MiFare technology tag (MIFARE Plus, UltraLight, DESFire) base on ISO14443. (NFCMiFareTag)
private func parseMIFARETag(_ tag: NFCMiFareTag) {
let log = """
------------------------------------
:::::::::: [ MiFare tag ] ::::::::::
"- Type : MiFare technology tag (MIFARE Plus, UltraLight, DESFire) base on ISO14443."
"- Identifier : \(tag.identifier.hexString)"
"- Historical bytes : \(tag.historicalBytes?.hexString ?? "None")"
"- mifareFamily : \(self.formattedMiFareFamily(tag))"
"- description : \(String(describing: tag.description))"
"- Block count : \(tag.mifareFamily == .plus ? 256 : 16)"
------------------------------------
"""
print(log)
}
private func formattedMiFareFamily (_ tag: NFCMiFareTag) -> String {
switch (tag.mifareFamily) {
case .unknown:
return "MiFare compatible ISO14443 Type A tag." // ISO14443 Type A 호환제품
case .ultralight:
return "MiFare Ultralight series."
case .plus:
return "MiFare Plus series."
case .desfire:
return "MiFare DESFire series."
default:
return "Unknown"
}
}
}
extension Data {
// Data([0x12, 0x34, 0x56]) -> 123456
var hexString: String {
return map { String(format: "%02hhx", $0) }.joined() // big-endian byte order : reversed().map
}
// Data([0x12, 0x34, 0x56]) -> 0x12 0x34 0x56
var hexStringFormatted: String {
let hexArray = map { String(format: "0x%02hhx", $0) }
return hexArray.joined(separator: " ")
}
// Data([0x12, 0x34, 0x56]) -> 313233343536
public func hexEncodedString() -> String {
let hexDigits = Array("0123456789abcdef".utf16)
var hexChars = [UTF16.CodeUnit]()
hexChars.reserveCapacity(count * 2)
for byte in self {
let (index1, index2) = Int(byte).quotientAndRemainder(dividingBy: 16)
hexChars.append(hexDigits[index1])
hexChars.append(hexDigits[index2])
}
return String(utf16CodeUnits: hexChars, count: hexChars.count)
}
var string: String {
return String(data: self, encoding: .utf8)!
}
var jsonString: String {
do {
let json = try JSONSerialization.jsonObject(with: self, options: [])
let data = try JSONSerialization.data(withJSONObject: json, options: .prettyPrinted)
return String(data: data, encoding: .utf8) ?? ""
} catch let error {
print("JSON serialization error: \(error.localizedDescription)")
return ""
}
}
func isJsonString() -> Bool {
do {
let _ = try JSONSerialization.jsonObject(with: self, options: [])
return true
} catch {
return false
}
}
}
extension Dictionary {
func toJsonString() -> String? {
do {
let jsonData = try JSONSerialization.data(withJSONObject: self, options: [.prettyPrinted, .sortedKeys])
return String(data: jsonData, encoding: .utf8)
} catch {
print(error.localizedDescription)
return nil
}
}
}
extension Array where Element == Data {
func toStrings() -> [String] {
return self.map { String(data: $0, encoding: .utf8) ?? "" }
}
func toHexStrings() -> [String] {
return self.map { $0.reduce("") { $0 + String(format: "%02x", $1) } }
}
}
extension NSObject {
func rootWindow() -> UIWindow? {
var window: UIWindow?
if #available(iOS 15.0, *) {
window = UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.flatMap { $0.windows }
.first(where: { $0.isKeyWindow })
} else {
window = UIApplication.shared.windows.first(where: { $0.isKeyWindow })
}
return window
}
func showAlert(title: String?, message: String?, actions: [UIAlertAction] = [UIAlertAction(title: "OK", style: .default, handler: nil)], preferredStyle: UIAlertController.Style = .alert) {
let alertController = UIAlertController(title: title, message: message, preferredStyle: preferredStyle)
for action in actions {
alertController.addAction(action)
}
guard let rootViewController = self.rootWindow()?.rootViewController else { return }
rootViewController.present(alertController, animated: true)
}
}
사용법 :
// NFC NDEF 테그 쓰기
let str = "테스트 메시지 입니다."
let type = str.hasPrefix("http") == true ? "U" : "T"
let message: [String: Any] = [
"payload": str,
"type": type
]
HiNFCManager.shared.write(message: message) { manager in
manager.setMessage("Place iPhone near the tag to be written on")
} didDetect: { manager, result in
switch result {
case .failure(let error):
manager.setMessage("Failed to write tag")
print("\(error.localizedDescription)")
case .success(let payload):
manager.setMessage("Tag successfully written")
print("\(payload?.toJsonString() ?? "")")
DispatchQueue.main.async {
self.infoText.text = "\(payload?.toJsonString() ?? "")" + "\n" + self.infoText.text
}
}
}
// NFC NDEF 테그 읽기
HiNFCManager.shared.read { manager, result in
switch result {
case .failure(let error):
manager.setMessage("Failed to read tag")
print("\(error.localizedDescription)")
case .success(let payload):
manager.setMessage("Tag read successfully")
print("\(payload?.toJsonString() ?? "")")
DispatchQueue.main.async {
self.infoText.text = "\(payload?.toJsonString() ?? "")" + "\n" + self.infoText.text
}
}
}
// NFC Tag 쓰기
let str = "https://www.apple.com"
let type = str.hasPrefix("http") == true ? "U" : "T"
let message: [String: Any] = [
"payload": str,
"type": type
]
HiNFCManager.shared.writeTag(message: message) { manager in
manager.setMessage("Place iPhone near the tag to be written on")
} didDetect: { manager, result in
switch result {
case .failure(let error):
manager.setMessage("Failed to write tag")
print("\(error.localizedDescription)")
case .success(let payload):
manager.setMessage("Tag successfully written")
print("\(payload?.toJsonString() ?? "")")
DispatchQueue.main.async {
self.infoText.text = "\(payload?.toJsonString() ?? "")" + "\n" + self.infoText.text
}
}
}
// NFC Tag 읽기
HiNFCManager.shared.readTag { manager, result in
switch result {
case .failure(let error):
manager.setMessage("Failed to read tag")
print("\(error.localizedDescription)")
case .success(let payload):
manager.setMessage("Tag read successfully")
print("\(payload?.toJsonString() ?? "")")
DispatchQueue.main.async {
self.infoText.text = "\(payload?.toJsonString() ?? "")" + "\n" + self.infoText.text
}
}
}
결과값 로그 :
// NFC NDEF tag 읽기결과 로그
{
"Format" : "Media (0x02)",
"Payload" : "0x74 0x65 0x73 0x74 0x20 0x6d 0x65 0x73 0x73 0x61 0x67 0x65",
"Raw value" : "test message",
"Size" : "12",
"Type" : "T"
}
// NFC NDEF tag 쓰기결과 로그
{
"result" : "test message"
}
// NFC Tag 읽기결과 로그
{
"Format" : "Absolute URI (0x03)",
"Payload" : "0x68 0x74 0x74 0x70 0x73 ... 0x65 0x2e 0x63 0x6f 0x6d",
"Raw value" : "https://www.apple.com",
"Size" : "21",
"Type" : "U"
}
// NFC Tag 쓰기결과 로그
{
"result" : "https:\/\/www.apple.com"
}
2023.04.26 - [iOS] - NFC tag read/write Manager Class (1/2)
2023.04.26 - [iOS] - NFC tag read/write Manager Class (2/2)
반응형
'개발 > iOS' 카테고리의 다른 글
XOR 연산을 이용한 문자열 비교 (0) | 2023.12.07 |
---|---|
Rosetta2 (0) | 2023.04.26 |
NFC tag read/write Manager Class (1/2) (0) | 2023.04.26 |
Carthage 설치 및 제거 (0) | 2023.01.11 |
NSURLSessionTask 캐싱 비활성화 (0) | 2022.10.24 |