How do you see what an app and a tag actually exchange?
Log it yourself: every command sent, every answer received, with a timestamp, on both sides of the interface that wraps the radio calls. Android also keeps a trace of its own controller traffic, filtered by default. iOS offers nothing equivalent, only errors.
The log you write
The trace that always exists is the one your code produces. Every command as bytes, every answer as bytes, the exception when there is one, and a timestamp on each: that is enough to replay an exchange, count the round trips and see where a tag was lost.
It belongs at the same seam the testing page recommends, around the calls that touch the radio, which means it costs one place in the code rather than a line per call site. Keep the bytes in hexadecimal and keep them whole: a truncated log of a failing exchange answers nothing.
The trace Android keeps
The platform keeps its own. Android's own NCI trace, in the open source NFC stack, holds the packets exchanged with the controller in a ring buffer of 256 KB, both directions, each with a timestamp, and the service dumps it on request.
Two properties decide what is in it. The mode, persist.nfc.snoop_log_mode, is filtered unless it is set to full, and a debuggable build also spills the buffer to a file under /data/misc/nfc/logs. On an ordinary retail phone, what you get is therefore a filtered view; a development build shows the frames.
This is the controller's traffic, not your app's calls. It answers a different question, and the useful one: whether the command your code believes it sent is the command that went out.
What iOS gives you
Core NFC reports errors, not frames. The list is precise enough to diagnose with, a connection lost, too many retries, a tag that responded with an error, a packet longer than the tag accepts, a session invalidated under the call, and the invalidation page covers the session side of it.
So on iOS the log you write is the whole trace, which is an argument for writing it before you need it.
Reading what comes back
Know what each protocol puts in an answer. An APDU ends with two status bytes, and 90 00 is the only one that means completed, which the APDU page tabulates. An ISO 15693 answer opens with a flags byte whose lowest bit means error, followed by a code, as the 15693 page describes.
And read the timestamps, not only the bytes. A gap of tens of milliseconds between a command and the next one is your own code thinking, and it is the part of a slow tap that you can fix.
Frequently asked questions
Do I need a sniffer to see the exchange?
Not for most bugs. A log of the commands and answers your code sends and receives answers them, and it works on both platforms.
What does the Android trace contain?
The packets exchanged with the controller, in a ring buffer, timestamped in both directions, filtered unless the snoop mode property is set to full.
Is there an equivalent on iOS?
No. Core NFC gives errors rather than frames, so the trace on iOS is the one the app writes.
What should the log record?
The bytes sent, the bytes received, the exception if any, and a timestamp on each line, whole rather than truncated.