Getting Started With IPWorksNFC on iOS


Requirements: IPWorksNFC

Introduction

IPWorksNFC provides a simple, event-driven API for Near-Field Communication. This library enables iOS applications to read and write to NFC tags, as well as process various NDEF record types.

This guide will walk through project setup, reacting to NFC events, and reading/writing tags.

Project Setup

First, add the IPWorksNFC framework to your Xcode project. Before the component can run successfully, the Entitlements and Info.plist files must be configured appropriately. These files can be changed directly or via Xcode; see the demo project for reference.

To ensure the correct entitlement is applied to the project, in Xcode, first select the project and the appropriate target in the project view. After navigating to 'Signing & Capabilities', you must add the 'Near Field Communication Tag Reading' capability. Alternatively, you can add the following key to the Entitlements file. For example, the file contents may look like:

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> com.apple.developer.nfc.readersession.formats TAG

Next, ensure that the Info.plist file includes the NFCReaderUsageDescription key. For example, the file contents may look like:

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> NFCReaderUsageDescription This app needs NFC access to read contactless NFC tags

You will also need to ensure the Info.plist file is referenced and is present in the 'Build Settings', under 'Packaging'.

Initialization

To use the NFC component, you must first import the IPWorks NFC module as needed:

import IPWorksNFC

Afterwards, you must create an instance of the class and assign a delegate that conforms to NFCDelegate. The delegate receives all NFC events, including when a tag is detected, when records are read, and when logs or errors occur.

A common approach is to wrap the NFC component in a manager class that implements NFCDelegate. This keeps the NFC logic separate from the UI and allows you to reference the manager in your main view:

class NFCManager: ObservableObject, NFCDelegate { private var nfc: NFC?
init() { do { nfc = try NFC() nfc?.delegate = self // other settings } catch { print("Error initializing NFC: \(error)") } }
// Delegated events func onError(errorCode: Int32, description: String) { } func onLog(logLevel: Int32, message: String, logType: String) { } func onRecord(id: Data, payload: Data, recordType: Data, recordTypeFormat: Int32) { } func onTag(forumType: Int32, maxSize: Int32, serialNumber: String, size: Int32, writable: Bool) { } }
struct ContentView: View { @StateObject private var nfcManager = NFCManager()
var body: some View { // UI code here } }

Alternatively, you can have your SwiftUI view itself conform to NFCDelegate:

struct ContentView: View, NFCDelegate { private var nfc: NFC?
// Delegated events func onError(errorCode: Int32, description: String) { } func onLog(logLevel: Int32, message: String, logType: String) { } func onRecord(id: Data, payload: Data, recordType: Data, recordTypeFormat: Int32) { } func onTag(forumType: Int32, maxSize: Int32, serialNumber: String, size: Int32, writable: Bool) { }
init() { do { nfc = try NFC() nfc?.delegate = self // other settings } catch { print("Error initializing NFC: \(error)") } }
var body: some View { // UI code here } }

Reading a Tag

To read the data from a tag, you must call the Read method. After doing so, a prompt will appear on your iOS device, indicating to place the tag near the device. Please note that the Read method is blocking. To prevent the UI from freezing, the method can be dispatched like so:

DispatchQueue.global().async { do { try nfc!.read() } catch let e { print("(e)") } }

After the tag is detected, the component will attempt to parse the tag information and any stored records. As soon as the component has read all of the tag information, the Tag event will fire, containing relevant details about the tag, such as the tag's type and serial number, current and maximum size (i.e., the amount of data the tag is currently storing or can store), and whether the tag can be written to. For example, in the onTag function of the NFCManager class above:

func onTag(forumType: Int32, maxSize: Int32, serialNumber: String, size: Int32, writable: Bool) { print("Tag Detected. Details:") print("Tag Type: \(forumType)") print("Serial Number: \(serialNumber)") print("Size: \(size) / \(maxSize) Bytes") print("Writable: \(writable)") }

After the Tag event fires, the Record event will fire for each record that is detected. The Record event will contain relevant details about the record, such as the record TNF (type name format, or record type format), the actual record type (which is interpreted based on the TNF), the payload (actual record content), and the optional record ID (which is typically empty). For example, in the onRecord function of the NFCManager class above:

func onRecord(id: Data, payload: Data, recordType: Data, recordTypeFormat: Int32) { print("Record Detected.") print("TNF: \(recordTypeFormat)") // E.g., 0 -> 0x00 (Empty), 1 -> 0x01 (Well-known) print("Record Type: \(recordType)") // E.g., if TNF is 0x01 (Empty), this could be "T" (text record) print("Record Payload: \(payload)") }

Note that, in addition to the tag and record details being made available via their corresponding events, the Tag property and Records collection will be populated after the Read method returns successfully. Tag and record details can also be accessed like so:

// Printing tag info private func printTagInfo() { guard let tag = nfc?.tag else { return } print("Tag Type: \(tag.forumType)") print("Serial Number: \(tag.serialNumber)") print("Size: \(tag.size) / \(tag.maxSize) Bytes") print("Writable: \(tag.writable)") }
// Printing record info private func printRecordInfo() { var recordNum = 1 for record in nfc!.records { print("Record \(recordNum) Info") print("TNF: \(record.recordTypeFormat)") print("Record Type: \(record.recordType)") print("Record Payload: \(record.Payload)") print("") recordNum += 1 } }

Parsing Records

As mentioned, the Record event provides the raw components of each NDEF record detected on a tag. An NDEF (NFC Data Exchange Format) record is the standard way NFC tags encode and store data. Each record contains four key parts exposed by the Record event.

Typically, the RecordTypeFormat will be checked first. This parameter represents the TNF (Type Name Format) associated with the record. Possible values for this parameter are:

Value Name Description
0x00 Empty Empty record (padding or placeholder)
0x01 Well-Known Standard types like text (T) and URI (U)
0x02 MIME Media Type Type is a MIME type (e.g., text/plain, image/jpeg)
0x03 Absolute URI Type field contains a full URI
0x04 External Type Namespaced external types (e.g., com.example:type)
0x05 Unknown Type is not provided or known
0x06 Unchanged Used for chunked data (not common)

After checking the value of the RecordTypeFormat, the RecordType can then be interpreted. As an example, if the TNF of a record is 0x01 (Well-Known), the RecordType is usually a single character that identifies the type of record. This value would be "T" to indicate a text record, or "U" to indicate a URI/URL record.

In either case, the mentioned parameters can be used to then determine how to interpret the Payload parameter, which is ultimately left up to the developer. For example, given a RecordTypeFormat of 0x01 and a RecordType of "T" (Well-Known Text Record), the following code (when iterating through the collection of records) can be used to extract the relevant data out of the Payload:

if record.recordTypeFormat == RecordTypeFormats.rtfWellKnown && record.recordType == "T" { // Handle Text Record let payload = record.payloadB let status = payload[0] let isUtf16 = (status & 0x80) != 0 let langCodeLen = Int(status & 0x3F)
let encoding: String.Encoding = isUtf16 ? .utf16 : .utf8
let languageCodeRange = 1..<1+langCodeLen let languageCodeData = payload.subdata(in: languageCodeRange) let languageCode = String(data: languageCodeData, encoding: .ascii) ?? ""
let textRange = (1 + langCodeLen)..<payload.count let textData = payload.subdata(in: textRange) let text = String(data: textData, encoding: encoding) ?? ""
print("Text Record (LangCode = \(languageCode)) - \(text)") }

The format of a text record is defined and standardized, so the payload can be parsed in a predictable way (status byte, language code, then the actual text). Other record types follow similar conventions. For example:

  • For a URI record (TNF = 0x01, RecordType = "U"), the first byte of the payload is the URI prefix code (e.g., 0x01 for "http://www.), followed by the rest of the URI string in UTF-8.
  • For a MIME Media record (TNF = 0x02), the record type is a MIME type (e.g., "text/plain"), and the payload contains raw data of that type.

In practice, this means that the RecordTypeFormat (TNF) tells you how to interpret the RecordType, which in turn tells you how to parse the Payload. By combining these, you can effectively parse any NDEF record type.

Writing to a Tag

To write records to a tag, the Records collection should first be populated with any records that are to be written to the tag. To do so, the API exposes three methods:

  • AddTextRecord
  • AddURLRecord
  • AddRecord

The AddTextRecord method can be used to add a well-formed text record to the Records collection. This is typically used to encode human-readable text. This method takes two parameters: the actual text to store on the tag, and the language code that specifies the language of the text (for example, "en" for English).

The AddURLRecord method can be used to add a URI or URL record to the Records collection. This method takes a single parameter, which is the URL string to store on the tag.

For all other types of records, including custom or MIME-based records, the AddRecord method may be used. This method requires specifying the TNF (Type Name Format), the record type, an optional record ID, and the record payload. As there are many possible types of records, this method is intended to give developers full control over record creation. While AddTextRecord and AddURLRecord cover some common scenarios, AddRecord allows developers to handle any custom, proprietary, or less common NDEF records without introducing additional specialized methods. As an example, you could create a MIME Media record like containing JSON like so:

do { let recordTypeFormat: Int32 = 0x02 let recordType = "application/json".data(using: .ascii)! let id = Data() // empty
// Prepare JSON let jsonObject: [String: Any] = [ "name": "NFC Example", "version": "1.0", "data": "some_json_data" ] let payload = try JSONSerialization.data(withJSONObject: jsonObject, options: [])
try nfc?.addRecord(recordTypeFormat: recordTypeFormat, type: recordType, payload: payload, id: id) } catch { print("Failed to add JSON MIME record: (error)") }

Once the desired records are added to the Records collection, the Write method should be called to write the records to the tag. If multiple records are added, they will be written together as a single NDEF message. Please note that the Write method is blocking. To prevent the UI from freezing, the method can be dispatched like so:

DispatchQueue.global().async { do { try nfc!.write() } catch let e { print("(e)") } }

Note that if you previously read a tag, the Records collection will likely be populated. Unless you are preserving the existing records from the prior read operation, you can clear the records from the collection like so:

nfc!.records.removeAll()

After clearing the records, you can add new records to write using the methods described previously.

Advanced Topics

This section contains information regarding any special configurations or considerations using IPWorks NFC for iOS.

FeliCa Tags

By default, the NFC component does not enable polling for FeliCa (NFC-F) tags, as these tags are not widely used and require additional project configuration (specifically, adding a key to the project's entitlements).

To enable support for FeliCa tags, the following key can be added to the project's Entitlements file:

com.apple.developer.nfc.readersession.felica.systemcodes 12FC

After doing so, please ensure that the .iso18092 option is enabled via the PollingOptions configuration setting in the NFC component:

try nfc!.config(configurationString: "iso14443,iso15693,iso18092")

After doing so, the component should be able to read and write to FeliCa tags. Note that by default, PollingOptions is set to iso14443,iso15693.

We appreciate your feedback. If you have any questions, comments, or suggestions about this article please contact our support team at support@nsoftware.com.