<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>The Long Answer</title><link>https://7strikes.dev/</link><atom:link href="https://7strikes.dev/feed.xml" rel="self" type="application/rss+xml"/><description>Notes on hard Apple platform problems.</description><language>en</language><lastBuildDate>Mon, 21 Sep 2026 19:55:04 +0000</lastBuildDate><item><title>Why Core Bluetooth state restoration never fires</title><link>https://7strikes.dev/articles/core-bluetooth-state-restoration/</link><guid isPermaLink="true">https://7strikes.dev/articles/core-bluetooth-state-restoration/</guid><pubDate>Mon, 21 Sep 2026 00:00:00 +0000</pubDate><category>Core Bluetooth</category><description>The restoration identifier is necessary but not sufficient. Four other conditions have to hold, and three of them are not in the documentation.</description><content:encoded>&lt;p&gt;Restoration is the single most misunderstood part of Core Bluetooth. Teams set &lt;code&gt;CBCentralManagerOptionRestoreIdentifierKey&lt;/code&gt;, see nothing happen, and conclude the feature is broken.&lt;/p&gt;
&lt;h2 id="what-the-option-actually-does"&gt;What the option actually does&lt;/h2&gt;
&lt;p&gt;Setting the identifier registers your central or peripheral manager with the system, so it is handled while your app is not running. But it does not by itself guarantee your process will be relaunched.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;let manager = CBCentralManager(
    delegate: self,
    queue: nil,
    options: [CBCentralManagerOptionRestoreIdentifierKey: "com.example.central"]
)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="the-conditions-nobody-lists-together"&gt;The conditions nobody lists together&lt;/h2&gt;
&lt;ul&gt;&lt;li&gt;The identifier must be stable across launches. A UUID generated at runtime silently defeats restoration.&lt;/li&gt;&lt;li&gt;&lt;code&gt;centralManager(_:willRestoreState:)&lt;/code&gt; must be implemented on the delegate, and it is called before &lt;code&gt;centralManagerDidUpdateState(_:)&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;The app must have been terminated by the system, not by the user swiping it away in the app switcher. Although there is an exception to this.&lt;/li&gt;&lt;li&gt;There must be pending work — an active scan or advertising, active I/O, a connection, or a connection request in progress — for there to be anything to restore. Restoration happens when the CB Manager requires work to be done by the app.&lt;/li&gt;&lt;/ul&gt;
&lt;blockquote class="callout important"&gt;&lt;p&gt;&lt;strong class="label"&gt;Important:&lt;/strong&gt; If the user force-quits the app, restoration does not happen and will not happen again until the user launches it manually. This is deliberate. The exception to this is if the app has opted in to use &lt;a href="https://developer.apple.com/documentation/accessorysetupkit"&gt;Accessory Setup Kit&lt;/a&gt; on iOS 26+, this will overcome the restriction on user swiping up the app.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h2 id="how-to-tell-which-condition-you-are-failing"&gt;How to tell which condition you are failing&lt;/h2&gt;
&lt;p&gt;Capture a sysdiagnose while reproducing and look for the relevant subsystem. The logs distinguish these cases clearly, which is more than can be said for the API.&lt;/p&gt;
&lt;p&gt;For diagnosing CoreBluetooth issues, you will want to install the &lt;a href="https://developer.apple.com/feedback-assistant/profiles-and-logs/"&gt;Bluetooth Logging Profile&lt;/a&gt; before reproducing the problem.&lt;/p&gt;</content:encoded></item><item><title>Why an NFC reader session has a time limit</title><link>https://7strikes.dev/articles/nfc-reader-session-time-limit/</link><guid isPermaLink="true">https://7strikes.dev/articles/nfc-reader-session-time-limit/</guid><pubDate>Mon, 21 Sep 2026 00:00:00 +0000</pubDate><category>NFC and contactless</category><description>There is no continuous NFC mode on iOS. A reader session is a short, user-visible burst with a fixed lifetime, and every way of pretending otherwise makes the app worse.</description><content:encoded>&lt;p&gt;A recurring request: keep Core NFC listening. The user walks through a building tapping tags, and the app should pick each one up without a sheet appearing and disappearing every time. Is there a session configuration for that, or a way to restart the session so quickly that nobody notices?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;There is not&lt;/strong&gt;. The time limits are hard, they are documented, and the design that tries to hide them is a worse experience than the design that accepts them. Also, abusing the NFC subsystem on an iPhone will shorten the session time due to thermal stress.&lt;/p&gt;
&lt;h2 id="what-is-fixed"&gt;What is fixed&lt;/h2&gt;
&lt;p&gt;The public &lt;code&gt;CoreNFC&lt;/code&gt; headers state the rules plainly.&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;A reader session has a &lt;strong&gt;time limit&lt;/strong&gt; from the moment &lt;code&gt;init()&lt;/code&gt; is called. When it expires, the delegate receives &lt;code&gt;readerSessionInvalidationErrorSessionTimeout&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;One reader session&lt;/strong&gt; can be active in the system at a time. A second &lt;code&gt;init()&lt;/code&gt; while one is active fails with &lt;code&gt;readerSessionInvalidationErrorSystemIsBusy&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;Calling &lt;code&gt;init()&lt;/code&gt; presents a &lt;strong&gt;modal system sheet&lt;/strong&gt;. The sheet is dismissed when the session is invalidated, by your code, by the timeout, or by the user tapping Done, which produces &lt;code&gt;readerSessionInvalidationErrorUserCanceled&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;If the app leaves the foreground, the session ends with &lt;code&gt;readerSessionInvalidationErrorSessionTerminatedUnexpectedly&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;An invalidated session cannot be reused. &lt;code&gt;restartPolling()&lt;/code&gt; on it does nothing. A new session means a new &lt;code&gt;init()&lt;/code&gt;, and a new sheet.&lt;/li&gt;&lt;/ul&gt;
&lt;p&gt;None of these has a knob. There is no entitlement that extends the timeout, no configuration that suppresses the sheet, and no background mode for reader sessions.&lt;/p&gt;
&lt;h2 id="why-it-is-built-this-way"&gt;Why it is built this way&lt;/h2&gt;
&lt;p&gt;Reader mode is not simply listening. It is transmitting. For as long as a session is active, the phone is driving its antenna to generate the RF field that powers whatever tag comes into range, as the tag has no battery and takes its energy from the phone. The energy that goes into that field is drawn from the battery, and what the field does not deliver to a tag ends up as heat in the antenna and the circuits around it.&lt;/p&gt;
&lt;p&gt;That is a fine thing to do for the few seconds it takes to hold a phone against a tag. It is not a fine thing to do for the twenty minutes it takes to walk through a building. So the session is shaped like the interaction it exists for: short, started by the user, visible on screen while it runs, and over quickly. The time ceiling, the single active session, and the sheet are not three separate restrictions. They are one decision, that a reader session is a burst and not a mode.&lt;/p&gt;
&lt;blockquote class="callout important"&gt;&lt;p&gt;&lt;strong class="label"&gt;Important:&lt;/strong&gt; Restarting a session the instant it ends does not produce a “mode" either. Trying to use it as a trick will only shorten the time for the next session until the stress on the NFC subsystem will prevent the next session from starting altogether. And for a VoiceOver user, this gets worse as a new modal is announced every time. The app is now fighting the system on the user's screen, and the user can see it.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h2 id="what-one-session-can-do"&gt;What one session can do&lt;/h2&gt;
&lt;p&gt;A single session can do more than one read. If the use is a short guided sequence, a couple of tags within a short time, it fits inside one session without any tricks.&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;Create the NDEF session with &lt;code&gt;invalidateAfterFirstRead: false&lt;/code&gt;. It stays open after a successful read, until the timeout, &lt;code&gt;invalidate()&lt;/code&gt;, or Done.&lt;/li&gt;&lt;li&gt;After handling a tag, call &lt;code&gt;restartPolling()&lt;/code&gt; to discover the next one. Tag objects from before the restart are invalid; drop your references.&lt;/li&gt;&lt;li&gt;Update &lt;code&gt;alertMessage&lt;/code&gt; after each read. The sheet's text can change while the session is valid, so it can say "Tag 2 of 5 read. Hold the phone near the next one."&lt;/li&gt;&lt;/ul&gt;
&lt;p&gt;When the session times out, tell the user it is over and let them start the next one. Do not start it for them.&lt;/p&gt;
&lt;h2 id="what-to-build-instead"&gt;What to build instead&lt;/h2&gt;
&lt;p&gt;Match the tool to the interaction.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;One tag, one tap.&lt;/strong&gt; Start a session on a deliberate user action, a button or a gesture, read the tag, invalidate with a message, done. This is what Core NFC is for. It is also the predictable shape for VoiceOver: one action, one sheet, one result.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No app involvement at all.&lt;/strong&gt; Late model iPhones can read NDEF tags in the background without any reader session. Encode a universal link on the tag; when the user holds the phone near it, the system shows a notification, and tapping it delivers the tag's payload to your app. There are conditions, all &lt;a href="https://developer.apple.com/documentation/corenfc/adding-support-for-background-tag-reading"&gt;documented&lt;/a&gt;: the phone must be in use and unlocked, no reader session may be active, and the camera and Wallet must not be in use. It still requires the user to tap the notification. That is the system's version of "tap a tag while walking around", and it is as close to a live mode as iOS offers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Proximity over minutes, hands-free.&lt;/strong&gt; If the requirement is really "know when the user is near a point of interest" rather than "the user touched this thing", NFC is the wrong radio. It works at a few centimetres and only on request. Beacon region monitoring in Core Location, or a Bluetooth LE scan for a service the beacons advertise, work at room scale and in the background, and are what wayfinding systems use. The tags can stay as the deliberate, precise action layered on top.&lt;/p&gt;
&lt;h2 id="handling-the-endings"&gt;Handling the endings&lt;/h2&gt;
&lt;p&gt;Every session ends in &lt;code&gt;readerSession(_:didInvalidateWithError:)&lt;/code&gt;. Treat the error as the reason, not as a failure.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Error&lt;/th&gt;&lt;th&gt;What happened&lt;/th&gt;&lt;th&gt;What to do&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;readerSessionInvalidationErrorFirstNDEFTagRead&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Read one tag, as configured&lt;/td&gt;&lt;td&gt;Nothing; this is success&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;readerSessionInvalidationErrorUserCanceled&lt;/code&gt;&lt;/td&gt;&lt;td&gt;The user tapped Done&lt;/td&gt;&lt;td&gt;Nothing; the user is in charge&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;readerSessionInvalidationErrorSessionTimeout&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Sixty seconds elapsed&lt;/td&gt;&lt;td&gt;Tell the user; offer to start again&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;readerSessionInvalidationErrorSessionTerminatedUnexpectedly&lt;/code&gt;&lt;/td&gt;&lt;td&gt;The app left the foreground&lt;/td&gt;&lt;td&gt;Start again when the user returns and asks&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;readerSessionInvalidationErrorSystemIsBusy&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Another session was active&lt;/td&gt;&lt;td&gt;Wait for it to end; do not retry in a loop&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The one thing not on the list is "restart automatically". A session that begins without the user asking for it is the sheet appearing without warning, and that is the whole reason the request comes up in the first place.&lt;/p&gt;</content:encoded></item></channel></rss>