NFC — read/write NDEF tags, read raw tag UIDs, and emulate a tag with Android card emulation (HCE).
iOS uses CoreNFC (NFCNDEFReaderSession / NFCTagReaderSession, iPhone 7+);
Android uses NfcAdapter reader mode (enableReaderMode). Read NDEF messages
(start_reading/2), write them (write_ndef/3), read a raw tag UID
(start_reading/2 with mode: :tag), and — on Android only — emulate an
NDEF tag with emulate_ndef/3 (HCE; iOS has no third-party host card
emulation, so emulate_ndef/3 sends {:nfc, :error, :unsupported} there).
API style
Same as the rest of Mob: calls return the socket unchanged and results
arrive in handle_info/2 as {:nfc, ...} messages delivered to the process
that started the session.
def handle_event("scan", _p, socket) do
{:noreply, MobNfc.start_reading(socket)}
end
def handle_info({:nfc, :ndef, %{ndef: bytes}}, socket) do
{:noreply, assign(socket, :last_tag, MobNfc.Ndef.parse(bytes))}
endEvents (tagged :nfc)
{:nfc, :session_started}
{:nfc, :ndef, %{tag_id: binary, ndef: binary, writable: boolean, max_size: integer}}
{:nfc, :tag, %{tag_id: binary, tech: binary}} # a tag with no NDEF data
{:nfc, :written, %{bytes: integer}} # write_ndef/3 succeeded
{:nfc, :session_ended, reason} # :done | :user_cancel | :error | ...
{:nfc, :error, reason} # :disabled | :unavailable | :read_failed | :read_only | :too_small | :not_ndef | :unsupported | ...Android card emulation (HCE) via emulate_ndef/3 adds:
{:nfc, :emulation_started} # HCE active
{:nfc, :hce_read} # a reader read the emulated tag
{:nfc, :hce_written, %{ndef: binary}} # a reader wrote to it (writable HCE)
{:nfc, :emulation_stopped} # stop_emulation/1ndef is the raw NDEF message bytes. Turn it into records with
MobNfc.Ndef.parse/1 (one tested parser shared across platforms), and decode
the common Text/URI records with MobNfc.Ndef.decode_text/1 / decode_uri/1:
def handle_info({:nfc, :ndef, %{ndef: bytes}}, socket) do
uris =
bytes
|> MobNfc.Ndef.parse()
|> Enum.flat_map(fn r ->
case MobNfc.Ndef.decode_uri(r) do
{:ok, uri} -> [uri]
:error -> []
end
end)
{:noreply, assign(socket, :uris, uris)}
endFor a non-NDEF tag, {:nfc, :tag, %{tech: "..."}} carries the comma-joined
Android tech-list (tech is empty on iOS).
Availability
available?/0 is true only on a device whose NFC radio is present and
switched on. Reading does nothing on a simulator/emulator (no radio) and on a
device with NFC turned off in system settings.
Permissions & setup
Android NFC is an install-time permission (no runtime dialog); the plugin adds
it to the manifest. iOS requires the com.apple.developer.nfc.readersession .formats entitlement and an NFCReaderUsageDescription Info.plist string
— the plist key is added by the plugin, but the entitlement must be added to
ios/<app>.entitlements by hand for now (the build prints the obligation; see
the plugin host_requirements).
Summary
Functions
True when the device has an NFC radio that is present and enabled.
Emulate an NDEF tag (Host Card Emulation) — make this phone read like an NFC
tag serving content to any nearby reader. Android only.
Start an NFC reader session; NDEF/tag events flow to the calling process.
Stop tag emulation started by emulate_ndef/3. Android only; no-op elsewhere.
Stop the reader session started by the calling process.
Write an NDEF message to the next tag tapped; result flows to the caller.
Functions
@spec available?() :: boolean()
True when the device has an NFC radio that is present and enabled.
Returns false on the host/simulator and on a device with NFC switched off.
@spec emulate_ndef(Mob.Socket.t(), binary() | map() | [map()], keyword()) :: Mob.Socket.t()
Emulate an NDEF tag (Host Card Emulation) — make this phone read like an NFC
tag serving content to any nearby reader. Android only.
content is raw NDEF bytes or record(s) (encoded via MobNfc.Ndef.encode/1),
the same as write_ndef/3. The phone advertises an NFC Forum Type-4 tag over
the NDEF application AID D2760000850101; a reader (another phone, an NFC
reader, an iPhone running start_reading/2) sees a normal read-only NDEF tag.
MobNfc.emulate_ndef(socket, MobNfc.Ndef.uri_record("https://mob.dev"))iOS has no third-party HCE API (the Secure Element is reserved), so this sends
{:nfc, :error, :unsupported} there. Events:
{:nfc, :emulation_started}
{:nfc, :hce_read} # a reader read the emulated tag
{:nfc, :hce_written, %{ndef: bin}} # a reader wrote to it (`:writable` only)
{:nfc, :emulation_stopped}Options
:writable— whentrue, advertise the emulated tag as writable so a reader (e.g. another phone'swrite_ndef/3) can write into it; the new message arrives as{:nfc, :hce_written, %{ndef: bytes}}and becomes what the tag subsequently serves. Defaults tofalse(read-only tag).
Requires the HostApduService + res/xml + AndroidManifest <service> the
plugin host_requirements describe (mob_dev can't contribute those yet).
@spec start_reading( Mob.Socket.t(), keyword() ) :: Mob.Socket.t()
Start an NFC reader session; NDEF/tag events flow to the calling process.
On iOS this presents the system reader sheet with opts[:alert] as its
prompt (default provided). On Android it enables foreground reader mode; there
is no sheet. Returns socket unchanged. No-op returning {:error, :unsupported} semantics are surfaced as a {:nfc, :error, :unsupported}
message rather than a raise, to match the async contract.
Options
:alert— iOS reader-sheet prompt string. Ignored on Android.:mode—:ndef(default) or:tag. iOS only — picks the CoreNFC session::ndef(NFCNDEFReaderSession, reads NDEF messages) or:tag(NFCTagReaderSession, reads any tag's UID + type, incl. non-NDEF smartcards like payment cards). Android's reader mode always surfaces both ({:nfc, :ndef, ...}for NDEF,{:nfc, :tag, ...}otherwise) regardless of:mode.
@spec stop_emulation(Mob.Socket.t()) :: Mob.Socket.t()
Stop tag emulation started by emulate_ndef/3. Android only; no-op elsewhere.
@spec stop_reading(Mob.Socket.t()) :: Mob.Socket.t()
Stop the reader session started by the calling process.
@spec write_ndef(Mob.Socket.t(), binary() | map() | [map()], keyword()) :: Mob.Socket.t()
Write an NDEF message to the next tag tapped; result flows to the caller.
content is either the raw NDEF message bytes (a binary) or a record / list
of records to encode via MobNfc.Ndef.encode/1:
MobNfc.write_ndef(socket, MobNfc.Ndef.uri_record("https://mob.dev"))
MobNfc.write_ndef(socket, [MobNfc.Ndef.text_record("hi"), uri_rec])This opens the same reader UI as start_reading/2 (iOS system sheet; Android
foreground reader mode) but, on tag detection, writes instead of reads. The
tag must be NDEF-formatted and writable and have enough capacity. Results:
{:nfc, :written, %{bytes: integer}} # wrote N bytes successfully
{:nfc, :error, :read_only} # tag is locked / not writable
{:nfc, :error, :too_small} # message exceeds tag capacity
{:nfc, :error, :not_ndef} # tag isn't NDEF-formatted
{:nfc, :error, :write_failed} # I/O error mid-writeOptions
:alert— iOS reader-sheet prompt string. Ignored on Android.