Basic Reading and Writing to Tags using CoreNFC

Someday I’ll get back to Part 2 of my UIDynamicAnimator tutorial, but today is not that day. I was super excited when Apple announced at WWDC this year that iOS 13 would let us not just read NFC tag payloads, they would let us write to them too! Shortly after this announcement, however, my company was acquired, we moved offices, and my job responsibilities changed significantly. It’s rather amazing how much our jobs can affect our motivations, because almost all my excitement to explore this gift from Apple disappeared while I figured out where I fit in my new role.

My motivation returned when I was asked to give a guest lecture for Lambda School on CoreNFC. I’ve been working with CoreNFC from the day it came out, and it has literally changed my life and career. It was time to dive back into the joys and mysteries of NFC. To my dismay, there was very little information out there on using CoreNFC to successfully write to tags. After several hours of hacking I was able to piece together writing a basic english formatted string to a chip using what I could find and what I already knew of reading tags. Here is what I learned.

My project can be found here. It’s possible it may have improved since the time I had written this blog post. This post is based on the first commit dated October 29, 2019.

There are a couple ways to start the NFC scanner to write. In this project I use this method:

readSession = NFCNDEFReaderSession(delegate: self, queue: .main, invalidateAfterFirstRead: false)

Notice how “invalidateAfterFirstRead” is set to false. This is key. We need to keep the reader open after we initially read with it, so we can connect to the tag and write to it.

The next major difference is that we will use

func readerSession(_ session: NFCNDEFReaderSession, didDetect tags: [NFCNDEFTag])

instead of

func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage])

like most tutorials will have you do. The reason is that we need access to the NFCNDEFTag object. You need this object to write, but you can also read the tag as well, although, it requires a few more steps than just using the NFCNDEFMessage method.

Here is my entire write code:

tag.queryNDEFStatus { (status, capacity, error) in
            guard error == nil else {
                print(error!)
                session.invalidate()
                return
            }

            print("Capacity: \(capacity) Bytes")

            switch status {
            case .readWrite:
                print("This tag is read/write compatible!  :D")

                // The characters "\u", "e", and "n" are prefixed on strings that are formatted for english
                // There are other prefixes for other language formats.
                guard let payload = "\uen\(self.writeToTagTextField.text ?? "")".data(using: .utf8) else {
                    session.invalidate()
                    return
                }

                let record = NFCNDEFPayload(format: .nfcWellKnown, type: Data(), identifier: Data(), payload: payload)

                // Not sure why NFCNDEFPayload.wellKnownTypeTextPayload() doesn't work...
//                guard let record = NFCNDEFPayload.wellKnownTypeTextPayload(string: "This is a test", locale: .autoupdatingCurrent) else {
//                    session.invalidate()
//                    return
//                }

                let myMessage = NFCNDEFMessage.init(records: [record])

                tag.writeNDEF(myMessage) { (error) in
                    if let error = error {
                        session.alertMessage = "Write NDEF message fail: \(error)"
                    }
                    else {
                        session.alertMessage = "Write NDEF message successful!"
                    }

                    session.invalidate()
                }
            case .readOnly:
                print("This tag can only be read.  :)")
                session.invalidate()
            case .notSupported:
                print("This tag is not supported.  :(")
                session.invalidate()
            @unknown default:
                print("I have no idea what just happened. 8O")
                session.invalidate()
            }
        }

The first thing you must do is check if the tag can even be written to. (I guess you are free to let it error out, but I like to make sure we know why it failed too.) Getting the status will also get you the capacity of the tag in bytes. I currently do not have any checks around this, but you can figure out how to add them pretty easily.

The key is to determine how to build the NFCNDEFMessage to be written. I am disappointed that Apple’s convenience function,

let record = NFCNDEFPayload.wellKnownTypeTextPayload(string: "This is a test", locale: .autoupdatingCurrent)

doesn’t seem to work as expected. I’m really not sure what it’s doing, but it definitely isn’t formatting the payload they way a normal NFC reader would understand. So instead I was able to achieve success by creating a NFCNDEFPayload object with Data and using that to initialize a NFCNDEFMessage. (I just now realize I used the long form, .init(), for NFCNDEFMessage. 🤦‍♂️)

// The characters "\u", "e", and "n" are prefixed on strings that are formatted for english
// There are other prefixes for other language formats.
guard let payload = "\uen\(self.writeToTagTextField.text ?? "")".data(using: .utf8) else {
    session.invalidate()
    return
}

let record = NFCNDEFPayload(format: .nfcWellKnown, type: Data(), identifier: Data(), payload: payload)

let myMessage = NFCNDEFMessage.init(records: [record])

Notice how you don’t need to set anything but empty data objects for “type” and “identifier”. I’m not sure if this will cause problems with other readers, but I was able to read the payload just fine after it gets written to the tag.

Once I have my NFCNDEFMessage I can then just call the write method.

tag.writeNDEF(myMessage) { (error) in
    if let error = error {
        session.alertMessage = "Write NDEF message fail: \(error)"
    }
    else {
        session.alertMessage = "Write NDEF message successful!"
    }

    session.invalidate()
}

All in all, it’s not that complex, however, it did cause me a little more grief than I expected it would, so I figured it would help you out. Let me know if there are improvements to be made, and like I said earlier, I will hopefully improve this project as I learn more.

Thanks!

Steve