ReviewOS

craft-native/craft

Interactive tab swipe places the incoming tab on the wrong edge, so it never appears

#359
Closed glennmichael123 opened this 8 days ago ·
8 days ago

What happens

During an interactive tab swipe, the outgoing screen follows the finger correctly, but the incoming tab never appears. The drag reveals the app's background instead, and the tab bar's selection has already moved — so it reads as the screen peeling away to nothing.

Verified on iPhone 17 Pro (iOS 26), craft-native@0.0.187, an all-native app (renderer: 'native') with five tabs. Captured mid-drag at ~150pt of travel: the outgoing screen is offset, the right third of the window is empty.

The back swipe is unaffected and looks correct, which is the giveaway — that one is UIKit's own interactivePopGestureRecognizer, while the tab swipe is hand-rolled in CraftHybrid.swift.

Why

beginInteractiveTabSwipe(to:direction:):

target.view.frame = view.bounds.offsetBy(dx: direction * width, dy: 0)

direction is the direction the finger is travelling. The tab being pulled in has to start on the opposite edge. As written it is placed on the same side — directly behind the screen that is leaving — so it is never on screen at any point in the gesture.

Symmetric: a finger moving left selects index + 1 and places it at -width; a finger moving right selects index - 1 and places it at +width. Both wrong.

There is a second, latent one three lines down in updateInteractiveTabSwipe(translation:):

let start = target.view.frame.minX >= width * 0.5 ? width : -width

This re-derives the starting edge from the frame it has just moved. Once the drag passes half the screen width the comparison flips and the incoming tab jumps across. It is hidden today by the first bug, but it bites as soon as that one is fixed alone.

Patch

Remember the edge for the gesture instead of re-deriving it, and start on the opposite side:

+    /// Which edge the tab being pulled in started from, kept for the length of
+    /// one gesture: re-derived from the live frame, it drifts as the frame moves.
+    private var tabSwipeFrom: CGFloat = 0
+
     /// Prepare a mounted target beside the current stack. Native roots render
@@
         target.view.isHidden = false
         let width = view.bounds.width
-        target.view.frame = view.bounds.offsetBy(dx: direction * width, dy: 0)
+        tabSwipeFrom = -direction * width
+        target.view.frame = view.bounds.offsetBy(dx: tabSwipeFrom, dy: 0)
@@
         let width = max(1, view.bounds.width)
-        let start = target.view.frame.minX >= width * 0.5 ? width : -width
         let offset = min(width, max(-width, translation))
-        target.view.frame.origin.x = start + offset
+        target.view.frame.origin.x = tabSwipeFrom + offset

finishInteractiveTabSwipe(completed:completion:) needs no change — its direction is derived from minX's sign, which is correct once the target starts on the right edge, in both the completed and the sprung-back cases.

Applied and rebuilt, the incoming tab follows the finger in from the correct edge and the two screens move together like a pager. Happy to open a PR if that is useful.

8 days ago

Fixed on main and published in v0.0.202. HQ consumes that published SDK in 4cf192e5, alongside the app performance improvements in 831eb299.

The native incoming tab starts on the opposite edge and retains that edge throughout the gesture. Cold first-tab mounting, movement beyond halfway, reversal, full-width completion and cancellation work correctly. A newer tab tap interrupts the old animation and invalidates its completion, so stale callbacks cannot override the new selection. These fixes are in 32d59a95 and a3dfe7f5.

Transitions stay in UIKit/Core Animation, retain mounted native tab stacks and respect Reduce Motion. HQ coalesces overlapping screen refreshes by account and caches expensive derived screen calculations.

Verified:

  • The published tab-fix source passes 11 native XCTest cases, 0 failed, 0 skipped, independently checked from the downloaded XCTest result bundle. Coverage includes real animation interpolation, swipe/cancel/retry, carousel gesture priority, retained tab state and rapid-tap interruption.
  • Negative controls fail on the old source: four of five original geometry cases fail before the edge fix, and the rapid-tap test fails against v0.0.200 locally and in baseline CI.
  • Latest HQ main with published craft-native 0.0.202 and stx 0.2.544: 3,825 passed, 19 skipped, 0 failed; lint 0 errors, 0 warnings; typecheck passed; iOS Release build, Android APK build and unsigned physical-iPhone archive passed.
  • The SDK v0.0.202 release workflow passed its publication gates. Installed Swift templates match the published package; generated Swift and installed dependencies were not patched.

Additional native-link/input/scroll improvements are upstream for a separate v0.0.203 release. Their combined-source 14-case native acceptance and the full native UI job passed. That broader release still has a separate hybrid-web check under investigation; HQ remains on the verified published v0.0.202. It does not block closing this native tab-swipe defect, whose fix is shipped and consumed.

All task commits were pushed to origin/main as Chris Breuer, without coauthor trailers. Other simulator sessions were left running.

8 days ago

The hybrid-web follow-up is now finished and consumed by HQ on main.

  • Published craft-native v0.0.203 includes 77fc6833: a slow hybrid push retains visible content in the incoming native slot until the real page is ready, then reveals the page and removes its snapshot together.
  • The exact release revision, 2f2f6c47, passed the complete device E2E run. All six jobs passed, including the real hybrid HTTP/router journey, hosted UIKit handoff regression, native UI/navigation, Android emulator and iOS Zig runtime checks. The hybrid test server served two real requests. The source checks snapshot ownership and release, rejects observed blank frames, and requires the destination page to appear.
  • The v0.0.203 release workflow passed all 16 jobs. HQ's installed Swift templates were compared byte-for-byte with that release tag.
  • HQ consumes the published package in 2dd2e055. b140222d refreshes the shared native bundles with the current published stx 0.2.545 compiler.
  • Fresh HQ verification: 3,858 passed, 19 skipped, 0 failed; lint 0 errors, 0 warnings; typecheck passed; iOS Release, Android APK and unsigned physical-iPhone archive all built successfully.

The final HQ commit was pushed to origin/main as Chris Breuer, without coauthor trailers. No generated Swift or installed dependency was patched, and other simulator sessions were left running. The earlier v0.0.203 publication caveat is resolved.

8 days ago

Actual HQ iOS follow-up is complete: the full navigation journey now passes through the previously stalled Settings step.

The shared driver fix is published in craft-native v0.0.213, from 06f58de1. Test-ID commands now use an exact identifier predicate and firstMatch, avoiding unique-element ambiguity resolution and accidental label matching on the scrolled Settings row. The release workflow passed.

Verified with the normal HQ XCTest runner against the actual HQ Release app built with the published SDK, on an iPhone 17 Pro / iOS 27 Simulator:

  • Cold full journey: 1 passed, 0 failed, 0 skipped.
  • Warm full journey with 1.5-second API delay: 1 passed, 0 failed, 0 skipped.
  • Both cover all tab swipes, Calendar push/edge-back, Discover/profile push/pop, scrolling to Settings, exact test-ID resolution, Settings push/edge-back, preserved Me selection and scroll position, and the final return to Today. Assertions and official xcresult summaries were checked; animation behavior was not disabled.

HQ main consumes published 0.0.213. df0c7b46 strengthens the journey, waits for simulator boot, preserves separate result bundles/logs, rejects empty or skipped test selections, and makes full simulator diagnostics opt-in. 7d1d6a03 freezes a workout test's elapsed-time clock so machine load cannot change its expected 2:05 label.

The full HQ suite passed 3,916 tests, 36 skipped, 0 failed, with clean lint and typecheck. A concurrent main update to Stacks 0.75.188 was preserved on rebase; its mobile implementation and the stx compiler were unchanged. After installing that release, 86 relevant navigation/Settings/workout tests, lint, typecheck, and production iOS generation passed again. Main's production assets and URL were preserved.

All task commits were pushed as Chris Breuer without coauthor trailers. Other QA sessions and simulators were left running. This issue remains closed.

8 days ago

Follow-up verification in the actual HQ.training native iOS app is complete with the normally published craft-native@0.0.220 (no source override or app-owned Swift).

The original incoming-edge/halfway-jump fix and hybrid-web support are retained. Subsequent fixes make the live native stack authoritative over stale page selections, preserve the owning tab during an interrupted preview, restore tab chrome/accessories after cancellation, and make the full accessory surface tappable. The shared XCTest driver also resolves exact Settings identifiers without the previous broad accessibility scan.

Validation:

  • Actual HQ Release app: 52 requested stress tab taps, including triple taps; swipe then third-tab/outgoing-tab taps; repeated with 1500 ms API responses and refresh activity. The settled selected tab and content agree.
  • Actual HQ XCTest: all tabs in both directions, controlled cancellation on all four tabs with and without API delay, Calendar/Discover/Profile/Settings pushes and native pops, retained Settings/Calendar stacks, and Messages accessory center/padding taps. Five accepted journeys, 98 assertions, zero failures or skips. One attempt interrupted by the iOS Home/Search screen is preserved separately; the same retention journey passed on retry with desktop keyboard forwarding disabled.
  • Focused native CI: 14 hosted UIKit tests and 15 UI tests pass, no skips. The pre-existing kitchen-sink offscreen countdown/checkbox case is explicitly excluded.
  • Rebuilt the integrated HQ main source as Release build 69: the navigation journey passed 26 assertions, followed by a combined cancellation/retention/accessory journey with 50 assertions, zero failures/skips. Generic unsigned iOS archive also passed.
  • HQ checks: 3,983 passed, 36 pre-existing conditional skips, 0 failures; lint 0 errors/0 warnings and typecheck passed. HQ changes pushed to main. All outgoing commits use Chris Breuer’s identity without coauthor trailers.
  • SDK tests: 688 pass; iOS package tests: 9 pass; native renderer contracts: 245 pass; typecheck and package build/verification pass.
  • Published SDK release passed all platform archive and package-publication checks.

This is simulator acceptance, not a physical-device or numeric FPS benchmark. Generic Craft CI also has a failure in unchanged Android registered-natives contract code; it is not included in the focused iOS pass above. The issue remains closed.

8 days ago

Android follow-up is now shipped in craft-native 0.0.226 and verified in the installed HQ app.

The native host cancels stale tab-animation completions, normalizes retained tab frames before display, and routes Android Back through its lifecycle callback. The JNI/Kotlin contract checks the complete names and descriptors without a stale hard-coded method count.

Validation:

The actual-app journey covers all four tabs in both directions, incoming content during held swipes, two 24-tap bursts, interrupted settling, cancelled drags, 1.5-second API delays, Calendar/profile/Settings push and pop, stack and scroll retention, and Messages center/padding taps. The bursts delivered 20/23 and 23/23 intervals below 180 ms; interruption taps arrived 52/39/50/40 ms after swipe release. Post-Back verification waits for the real Me root and its Settings button, and the retained scroll position was checked in the saved screenshot.

Installed/build APK SHA-256: 17c500890a9b08272999a18b68699ed8ae508d80866850f9b05ec2c678636e85. Recording, native trees, screenshots, frame diagnostics and package provenance are preserved with the consumer's Android journey. The issue remains closed.

8 days ago

Actual shipped-release acceptance update:

  • Android 1.0.0 (72): PASS. Tested the immutable published APK (2b75046bde28b76f13fade086143c7d3649679b0455d35efb18e8c3c3d849003) against production using a disposable QA athlete. The complete native journey passed 88 assertions / 85 native taps / zero failures or skips, with all three animation scales at 1. Both 24-tap bursts had all 23 intervals below 180 ms. Held incoming panels, interrupted swipes, Calendar/Discover/profile/Settings/Messages push/pop, retained stacks, and actual Settings return were verified. This is emulator functional evidence; no physical-device FPS claim.
  • Physical TestFlight 1.0.0 (71): not fully passing. The unchanged TestFlight installation passed both held swipes and all four rapid-tap burst final-selection/content checks. It then failed selection of Me after a tap scheduled 40 ms after swipe release. The original XCTest result, recording, input schedule, and before/after installation identities are preserved. The existing signed-in account and phone radios were preserved/restored.
  • Published Craft 0.0.228, commit 780543a7, adds .allowUserInteraction to that settling animation. That is a plausible correction for the observed 71 behavior, but it still needs acceptance in the next installed TestFlight release; source inspection is not a shipped-release pass.
  • The first-party timed-input driver and absent-font resource correction have passed full SDK CI, including an independent native receiver that verifies delivery of all 24 taps. Source is now on main at 8eb65484.

Reopening while the physical iOS interruption acceptance remains unresolved. Android 72 evidence is complete; it is not being substituted for the iOS check.

8 days ago

The separately scoped ordinary navigation run on the same physical TestFlight 71 installation also found an edge-pop failure after Discover → own profile: the profile remained visible and Discover did not return. All root tab/cancellation checks and Calendar push/edge-pop passed before it. Settings/Messages were not reached in that run. This result is preserved separately from the timed interruption failure; neither run is being reported as a full iOS pass. The phone and account are intact, and the next actual TestFlight 73 is available for the same regression checks.

8 days ago

Shipped-release navigation acceptance is complete.

Physical TestFlight 1.0.0 (73): PASS, installed through native TestFlight Update → Open, with the same installed bundle URL verified before and after the run. The phone's existing account and radios were preserved. The independent QA helper was removed afterward and its absence verified.

The full unchanged acceptance journey passed one native XCTest with zero failures/skips, all eight native input records, four 24-tap bursts ending on every tab, two held incoming-panel captures, third/outgoing tab taps scheduled 40 ms after swipe release, cancelled drags, Calendar and Discover/profile push/pop, profile edge Back, Settings return and retained scroll position, tab-stack retention, and Messages open/Back. Both regressions preserved from actual TestFlight 71 now pass on actual 73 (normal Craft 0.0.228). TestFlight 71 remains documented as the historical failing build.

Published Android 1.0.0 (72): PASS for the complete navigation journey: 88 assertions / 85 native taps / zero failures/skips, exact immutable signed APK hash verified, animation scales all 1, both 24-tap bursts with all 23 intervals below 180 ms, interrupted swipes and actual Settings return. This is emulator functional evidence, not a physical Android FPS measurement.

The reproducible first-party timed-input tooling and absent-font resource correction are normally published in Craft 0.0.231; the full release workflow passed. HQ installs the published iOS test package and carries the release-specific runners on main: 0fccb0a5 and 3c82fe9c. Full HQ lint passed 2,053 files with zero errors/warnings, typecheck passed, and the current shared full app gate passed 4,075 tests (49 existing skips, zero failures). Native release acceptance itself had no skipped tests. Commits are authored/committed as Chris without coauthor trailers.

Actual 71/72/73 installation identities, original failure recordings, native event schedules, held screenshots, end-state screenshots and XCTest/JUnit reports are retained in the local acceptance archive. No quantitative iOS FPS or input-latency claim is made.

Closing #359 with actual installed-release evidence. A separate Android active-screen/system-inset background defect found during workout QA is being corrected upstream and adopted normally; it does not change the completed navigation results above.

8 days ago

Final follow-up: the remaining native/runtime, hybrid-web and CI work is published and accepted.

  • Craft 0.0.234 is normally published, including craft-native and @craft-native/ios. The complete release workflow passed; public tarball integrity and shipped Android/iOS templates were verified against the release source. This includes the hybrid-web transition fixes, native switch accessibility correction, active-screen Android system-bar backgrounds, and validated cloud build-number restoration.
  • The full upstream Mobile E2E workflow is green at b71d2e84: run 39029521848. Native iOS navigation passed 31 tests / 0 failures / 0 skips, UIKit passed 131 unit + 2 UI tests, both Android API 34/35 native contracts passed, and generated iOS/Android, hybrid transitions, media and Zig checks passed. The long navigation suite now has its own bounded CI job; the 45-minute limits and acceptance assertions remain intact. The slider boundary and reference-window teardown races were corrected in test fixtures; earlier failures/cancellation are retained separately.
  • Actual HQ with public Craft 0.0.234 passed the complete Android navigation journey: 89 assertions / 85 native taps / 0 failures / 0 skips, covering rapid tab bursts, interrupted and held swipes, push/pop and retained scroll state with normal animation scales. The production native app code was tested under a disposable QA application alias/local API. Running-player black focus bars, restoration to light controls and native Back to Workout/Today also passed. The Health viewport assertion follows the current visible landmark and separately verifies retained Heart Rate scroll.
  • The earlier immutable-release acceptance remains valid: physical TestFlight 73 full native journey passed, and signed Android 72 full journey passed. TestFlight 71 retains its original two failures. The newer actual TestFlight 74 read-only smoke and exact signed Android 73 smoke (15 checks) also passed; these are recorded separately from the complete journeys.
  • HQ main at 427f1da9 installs both Craft packages at 0.0.234, Stacks 0.75.200 and aligned STX/components 0.2.554. Integrated lint/typecheck passed and the full HQ suite passed 4,132 tests / 0 failures with 53 existing optional skips. Changes are pushed to origin/main as Chris, without coauthor trailers.

Native XCTest/JUnit reports, exact installed artifact hashes, held-state screenshots and original failure evidence are retained in the acceptance archive. This is functional native-transition evidence; no numerical FPS/input-latency or absolute system-glyph contrast claim is made. All temporary QA drivers/services are off and the loaned simulator has been returned and removed by its owner. Other sessions were preserved.

#359 remains closed with the published follow-up and full CI acceptance complete.

8 days ago

Full released-app navigation follow-up passed on the actual HQ TestFlight 74 and the immutable published Android 73 APK (SHA-256 76c0150e646766bf2845763f99755b9f2b1679a9b63e31cc0f82742a30173996).

  • iPhone: 1 complete XCTest, 0 failures/skips; 8 timed native-input records, 96 rapid taps plus 2 interruption taps, incoming native screens verified in both held swipe directions, cancelled drags, all tabs, Calendar/public-profile push/pop, Messages, and retained Settings navigation.
  • Settings returned to the exact original native row frame: [16, 613.3, 370, 49] before and after; no corrective scrolling after return.
  • Android: 95 assertions, 87 native taps, 0 failures/skips, including rapid taps, interrupted swipes, held incoming views, push/pop, and Settings return. Original animation settings and package inventory were restored; the disposable emulator was removed.

Repeatable release acceptance

The acceptance fixtures now check the visible Settings row instead of an offscreen profile card, and select the actual public-directory button while keeping the QA owner's profile private. Earlier strict failures and the human-confirmed call-interrupted iPhone run remain recorded; the unchanged clean replay passed. The app install and signed-in iPhone session were preserved.

Validation: full Bun suite 4,148 passed, 53 skipped, 0 failed, then 102 integration tests after rebasing the latest activity-share changes; final lint and typecheck passed. Functional native transition acceptance is complete; this does not claim a measured frame rate. Issue remains closed.

8 days ago

Build 75 follow-up:

  • Both release tags share source ea0430e7396b07d7997bf76c94854fda6abf0b49 and the same dependency lock: Craft/iOS 0.0.236, Stacks 0.75.215 and STX 0.2.557. TestFlight 75 is valid and available to internal testers.
  • Actual signed Android 75: 95 assertions, 87 native taps, zero failures/skips, using the public APK (f30b0dbfdd5dc8ea1c698fcabe527a0b3cab4c8ac65967bf12e4f3fa92941134) on stock Google Play API 35, with all animation scales at 1. This includes rapid tab bursts, interrupted/cancelled swipes, all tabs, Calendar/public-profile push/pop and Messages.
  • All six held-swipe screenshots show incoming native content. A supplementary Settings test returned the row to the exact same pixel bounds [44, 1217, 1036, 1351], with no corrective scrolling after Back.
  • The signed 73→75 data-preserving upgrade also passed first-cold offline profile/photo/account continuity, followed by independently verified HTTPS handoffs, native sharing and Discover/cache checks. Original settings and package inventory were restored, helpers removed and the owned emulator retired.
  • Physical TestFlight 75 full navigation remains pending the existing device-verification/cache-upgrade handoff. The earlier physical 74 acceptance remains historical evidence. Android results here are emulator functional acceptance, with no hardware FPS claim.

The exact published Craft 0.0.236 source also passed the complete Mobile E2E workflow, including iOS native navigation and Android API 34/35 contracts.

Versioned HQ acceptance fixtures preserve build 75’s signed test-host inputs byte-for-byte while separately covering the next navigation layout. This issue retains its existing closed status; the actual iPhone 75 result remains outstanding.

Sign in to comment on this issue.