Repository navigation
🐛 Keep OnboardingFlowView's page list stable for the whole flow - #6
Merged
Merged
Conversation
The pending step list was computed in init, and init runs on every parent re-render. Once OnboardingFlow became observable, every recorded answer re-rendered the parent and rebuilt the list without the step just answered, so the TabView's pages shrank mid-flow: index 1 became a different screen (a skipped step), a page changed identity during the slide (the crossed-over transition), and with one step left the dots disappeared and there was nothing to swipe back to. Hold the snapshot in @State so the first one sticks, read storage directly when taking it so the parent doesn't subscribe to the flow, and drop the speculative .id() added while chasing the symptom. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- OnboardingFlow.reconcile(isSatisfied:) records true for unanswered steps the app reports as already satisfied outside the flow (a permission iOS already granted). OnboardingFlowView takes an isAlreadySatisfied closure, reconciles before snapshotting its pages and again when the scene becomes active. Resetting the kit's own records, or replaying onboarding, no longer shows a page for something the system already allows. - OnboardingFooter is the one button area the step and welcome views share. Both slots always hold their space, visible or not, so the main button and Skip sit in the same spot on every screen. Tests: 23 (20 flow/storage, 3 theme environment), all passing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Each page drew its own footer inside the TabView, so the page control sat on top of the button area. Step and welcome views now publish their actions upward through a preference (keyed by step ID from the environment) and OnboardingFlowView renders one OnboardingFooter under the TabView for the page on screen. A step view used outside a flow still draws its own footer. Public API of the step and welcome views is unchanged; Dolly needed no source changes. Tests: 30 (20 flow/storage, 10 UI), all passing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Development/testing counterpart to reset(): a tool can put a single step back to unrecorded to set up "this one was never seen" (or keep one in the middle answered) and replay the flow against that mix. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A bare TabView index change jumps; wrapping it in withAnimation slides the page the way a swipe does. The welcome records inside withAnimation too so the flow's welcome-to-pages swap crossfades instead of cutting. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The welcome was a separate screen swapped out ahead of the TabView, so leaving it could only crossfade. It's now the first page of the same pager when the flow still needs it: Continue slides to the first step exactly like a swipe, swiping back to it works, and swiping past it records it as seen. OnboardingFlowPage holds the page list logic so it can be tested on its own. Tests: 34 (21 flow/storage, 13 UI), all passing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Moving from a page with Skip to one without swapped the Skip Button for a placeholder inside the animated page change, so for the length of the fade both existed and the footer was briefly 44pt taller: the page content and the dots sat higher, then settled down. The secondary slot is now one stable Button that is hidden instead of swapped, the primary slot gets an identity transition, and the flow renders a hidden footer of the same size while a page's actions haven't arrived yet. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The footer was still two conditional branches (real footer vs hidden placeholder) swapped while a page's actions arrived, and its content changes animated inside the page-change transaction. Both nudge the TabView's height mid-slide, which is the residual drift in the content and the dots. OnboardingFooter now takes optional actions and hides its buttons instead of being replaced, and the flow renders it with animation disabled. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Measured on the iOS 27 simulator, TabView(.page) rendered a programmatic page change as a cut (not a slide) under both withAnimation and .animation(value:), settled each new page's centered content ~10pt late (its layout box changes after the page is added), and drew no dots at all. All three are its internals. The pager is now a horizontal ScrollView with .paging and a scrollPosition binding: advance() animates the position so Next slides exactly like a swipe, pages are created up front so nothing lays out mid-slide, and OnboardingPageIndicator draws themed dots between the pages and the footer. Verified with a screen recording: every transition is a 7 to 8 frame slide at 20fps and content drift after landing is zero. Tests: 35, all passing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A horizontal ScrollView takes its height from its content, and containerRelativeFrame(.vertical) measured the pages against the whole screen, so the pager grew to full height and pushed the footer (and its Skip line) below the bottom edge. Pages are now sized from a GeometryReader that owns the pager's actual slot above the dots and footer. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The README is the API reference; this is what an agent (or a person) needs to wire the kit into an app without re-learning tonight's lessons: dependency shape, one flow app-wide, needsOnboarding as the only root switch, step views own state and actions, theme from the environment, reconcile OS-granted permissions, analytics and replay stay in the app, and the simulator recording harness. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Three things a faithful CrumbDB port needs: - OnboardingTheme.accentColor (defaults to primaryColor) drives the header icon, the footer buttons, and the current dot, so an app with orange highlights and Moss text keeps both. - cardBackground is optional and, when set, OnboardingStepView draws the header and content inside a rounded card filled with it. - OnboardingFlowView.onPageShown fires once per page shown (first page on appear, then every change), for page-view analytics; onAppear on page content can't do it since every page is created up front. Tests: 39, all passing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
dryan
approved these changes
Oct 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The pending step list was computed in init, and init runs on every
parent re-render. Once OnboardingFlow became observable, every recorded
answer re-rendered the parent and rebuilt the list without the step
just answered, so the TabView's pages shrank mid-flow: index 1 became
a different screen (a skipped step), a page changed identity during
the slide (the crossed-over transition), and with one step left the
dots disappeared and there was nothing to swipe back to.
Hold the snapshot in @State so the first one sticks, read storage
directly when taking it so the parent doesn't subscribe to the flow,
and drop the speculative .id() added while chasing the symptom.
Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com