The tutorial was not the app
Until recently, the first thing a new ICIDO user saw was TutorialView. The commit that replaced it is blunt about what that view was: it simulates the app — a fake grid, a fake hold-to-locate (24dfd97). It was a well-made imitation of scanning, shown to somebody who had not yet scanned anything.
An earlier change had already cut that tutorial from seven steps to three, to match what the app actually asks of a person (eb52db4). Cutting it further was not the fix. The problem was not length. The problem was that nothing in it was real.
Prompt, wait, prompt again
The instruction behind the replacement was short: prompt the user to tap a thing, wait for them to do it, then prompt the next.
That is now a state machine in a new file, OnboardingCoach.swift. Its steps run tapNewScan → sweepRoom → tapSave → openPlaces → openPlace → getLocated → pickDestination → done, and each one advances only on a real event from the app. In the commit's words, the coach can never get ahead of the user or claim something happened that didn't (24dfd97).
That constraint is the whole design. The events are wired into screens that already knew the answer: the scan view reports readiness when the scan can be saved, the viewer reports that you have been located. Both are conditions the app was already computing for itself. Saving is gated on ARKit's world-mapping status reaching good, and ARKit publishes no explicit "you are relocalized" event at all — success is inferred when tracking returns to normal after the saved map is loaded. So when the tour says you are located, it is reading the same signal navigation reads. There is no separate, more optimistic version of the truth for the benefit of a new user.
A hole in the dimming
The overlay dims the screen and punches a rounded hole around the control the step is about. The hit-testing shape matches the fill rule used to draw the hole, so a tap inside it reaches the real button while taps outside are swallowed. The person is operating the actual app; the rest of the screen is only quiet.
The position of that hole is not hard-coded. Anchors are reported by the views that draw the controls, so the hole tracks the real frame — and in a list, only one row is allowed to claim an anchor. The overlay is also hosted per screen rather than once at the app root, for a concrete reason: the scanner is presented as a full-screen cover, which sits above the root, so a root-level overlay would have been hidden behind it.
One step deliberately does nothing at all. The camera steps — sweeping the room, waiting to be located — have no anchor and no dimming, because you have to be able to see what you are scanning.
The build that fixed the placement
The first version put cards in the wrong place on some screens, and the follow-up commit names two real defects (a5408d7).
The instruction card was positioned by its centre, at a fixed 130-point offset from the hole. Card height varies with the length of the message and with whether the step carries a button, so a tall card slid over the very control it was pointing at. The card is now pinned by one edge to the hole — top edge below it, or bottom edge above it — which is height-independent by construction. The side is chosen by whichever has more room, rather than by a midpoint test.
The second defect was subtler. The overlay had been attached at a different level on each screen: on Home to an inner padded stack, in the viewer to the root container. The geometry reader measuring the anchors therefore did not share an origin with what was being drawn. All four screens now host the overlay on their root container, and the modifier no longer ignores the safe area, which had been moving the origin away from the content it overlays.
Both defects share a shape: code that produced a correct result on the screen it was written against and a wrong one elsewhere. Alongside them, an anchor that resolves off-screen or to a zero-size frame — a scrolled-away list row, or a frame not laid out yet — is now ignored rather than drawn, since an out-of-bounds hole threw the card somewhere arbitrary.
Not every device can be walked through a scan
First-run routing now splits by device. A LiDAR iPhone gets the guided tour, starting after the permission primer. A device that cannot scan cannot be coached through scanning, so it still gets the old simulated tutorial — which also stops the app looking dead on such a device. Settings gained a "Guided tour" entry to replay it, shown only where scanning is possible.
Scanning needs the LiDAR sensor, which means an iPhone 12 Pro or a later Pro model. That constraint is why the fallback exists at all rather than being deleted with the rest.
Where this is
These changes are the release notes for builds 116 and 117, and they are in TestFlight. ICIDO is coming to the App Store.
None of it changes what leaves the device, which is nothing. ICIDO has no accounts and no sign-in, no analytics, and makes no network connections. Scanned maps, tagged spots and settings stay on the iPhone.