After you build a "tap a tag and a URL opens" app with Rork Max, the next thing you usually want is the reverse: writing information to a tag from the app. A visit stamp, a settings handoff, a unique ID tied to an exhibit. While it only reads, the app is a convenient entrance; the moment you add writing, it turns into a tool that links your app to physical objects and the world. As an indie developer, I was struck by the weight of that single step when I tried a "tap to transfer settings" trick for an in-store display of one of my wallpaper apps.
But asking Rork Max in plain English for "a feature to write to NFC tags" doesn't guarantee the code runs as-is on a device. Writing is a notch more delicate than reading, can't be checked in the simulator at all, and depends on the tag's capacity and state. Below, building on the Swift that Rork Max generates, I lay out the essentials of writing NDEF to a tag and the pitfalls only a real-device test reveals.
Design reading and writing as different things
In CoreNFC, both reading and writing enter through NFCNDEFReaderSession. The name makes it look read-only, but calling writeNDEF on a connected tag makes it a write. There's a design fork here. Reading ends once a message arrives, but writing must go through several stages: check the tag state, check capacity, perform the write, and verify.
When you ask Rork Max to "read," it returns straightforward code, but for "write" it sometimes produces a version that skips the verification stage. Skip verification and you can't tell when a write you thought succeeded actually failed. So from the start I think of writing in four stages: status check, capacity check, write, and read-back verification.
Build the NDEF and write it to the tag
First, assemble the payload as an NFCNDEFMessage. Writing one URL is the most practical case, so let's center on that.
import CoreNFC
final class NFCWriter: NSObject, NFCNDEFReaderSessionDelegate {
private var session: NFCNDEFReaderSession?
private var payload: NFCNDEFMessage?
func write(urlString: String) {
guard NFCNDEFReaderSession.readingAvailable else {
print("This device does not support NFC")
return
}
guard let url = URL(string: urlString),
let record = NFCNDEFPayload.wellKnownTypeURIPayload(url: url) else {
print("Could not convert the URL into an NDEF record")
return
}
payload = NFCNDEFMessage(records: [record])
session = NFCNDEFReaderSession(delegate: self,
queue: nil,
invalidateAfterFirstRead: false)
session?.alertMessage = "Hold your iPhone near the tag to write"
session?.begin()
}
// Tag-detected callback. Check state and capacity here before writing
func readerSession(_ session: NFCNDEFReaderSession,
didDetect tags: [NFCNDEFTag]) {
guard let tag = tags.first else { return }
if tags.count > 1 {
session.alertMessage = "Multiple tags detected. Please present only one."
session.restartPolling()
return
}
session.connect(to: tag) { error in
if let error = error {
session.invalidate(errorMessage: "Connect failed: \(error.localizedDescription)")
return
}
self.queryAndWrite(tag: tag, session: session)
}
}
func readerSessionDidBecomeActive(_ session: NFCNDEFReaderSession) {}
func readerSession(_ session: NFCNDEFReaderSession,
didInvalidateWithError error: Error) {}
func readerSession(_ session: NFCNDEFReaderSession,
didDetectNDEFs messages: [NFCNDEFMessage]) {}
}Using wellKnownTypeURIPayload(url:) lets CoreNFC handle URI identifier shortening (the scheme that compresses prefixes like https://www. into a single byte). It's safer than hand-assembling the TNF and type, and it doesn't waste the tag's limited capacity. I set invalidateAfterFirstRead to false because writing needs to keep working after detection; leave it true and the session closes the instant the tag is touched, so you can't write.
Confirm state and capacity before writing
Here's the body of the write. Use queryNDEFStatus first to confirm whether the tag is writable and how much capacity it has.
extension NFCWriter {
func queryAndWrite(tag: NFCNDEFTag, session: NFCNDEFReaderSession) {
tag.queryNDEFStatus { status, capacity, error in
if let error = error {
session.invalidate(errorMessage: "Status query failed: \(error.localizedDescription)")
return
}
guard let message = self.payload else {
session.invalidate(errorMessage: "No data to write")
return
}
switch status {
case .notSupported:
session.invalidate(errorMessage: "This tag does not support NDEF")
case .readOnly:
session.invalidate(errorMessage: "This tag is write-protected")
case .readWrite:
// Capacity check. Reject overflow up front, not at write time
let needed = message.length
if needed > capacity {
session.invalidate(
errorMessage: "Insufficient capacity: need \(needed) bytes / only \(capacity) available")
return
}
tag.writeNDEF(message) { error in
if let error = error {
session.invalidate(errorMessage: "Write failed: \(error.localizedDescription)")
} else {
session.alertMessage = "Write complete"
session.invalidate()
}
}
@unknown default:
session.invalidate(errorMessage: "Unknown tag state")
}
}
}
}The length property of NFCNDEFMessage is the number of bytes actually written to the tag. Comparing it against the capacity from queryNDEFStatus up front lets you reject an overflow before the moment of writing rather than during it. A cheap NTAG213 holds only around 144 bytes, so a long URL with query parameters overflows easily. For store use I prepared a separate short URL and wrote only the short one to the tag.
Note that Info.plist must contain NFCReaderUsageDescription, and writing also requires enabling Near Field Communication Tag Reading under Capabilities and the NDEF format entitlement. If these are missing in the Rork Max project settings, you get a confusing failure: the build passes, but on a device the session closes immediately.
Anticipate the failures that only appear on a device
NFC doesn't work in the simulator at all. That means a write feature assumes real-device testing from the moment you write it. This is where Rork Companion helped. Even before preparing a paid Apple Developer account, you can load the app onto a real iPhone and confirm writing to a tag, which lets you iterate more. The more a feature deals with physical tags, like writing, the more this "try it on a device right away" value matters.
The failures worth getting ahead of on a device collapse roughly into these.
| Symptom | Likely cause | Fix |
|---|---|---|
| Session ends instantly on tap | Missing entitlement / Info.plist setting | Check NFC Capabilities and NFCReaderUsageDescription |
| Fails on insufficient capacity | URL too long / small-capacity tag | Use a short URL / choose NTAG215 or larger |
| Writes but disappears later | No write-lock applied | Implement read-only locking once operations stabilize |
| Some tags occasionally won't write | Tag not formatted/initialized | Insert an NDEF format stage beforehand |
That last one — "not formatted" — can crop up even in brand-new tags, and without read-back verification it becomes the worst experience: "thought it succeeded, but empty." I recommend always adding, in production, a verification stage that reads the tag again in the same session and confirms the written URL comes back.
Where to go after adding writing
Extending from reading to writing changes the app's role from "receiving information" to "placing information into the world." The core of the implementation is finishing the status and capacity checks first, then reading back to verify after the write. Keep those four stages intact and you only need to add the verification stage to Rork Max's output to get a write feature that holds up in practice.
As a next step, confirm a minimal version that writes a single short URL on a device via Companion, and keep logs of capacity and verification. With those logs you can decide which NTAG type to choose and how short to make the URL from measurements rather than guesses. I hope it gives a first foothold to anyone taking on a feature that links software to physical objects, the same way I have in indie development.