Skip to content

fix(ios): re-apply loop animations on window re-attach#43

Merged
janicduplessis merged 1 commit into
mainfrom
fix/ios-loop-tab-detach
May 9, 2026
Merged

fix(ios): re-apply loop animations on window re-attach#43
janicduplessis merged 1 commit into
mainfrom
fix/ios-loop-tab-detach

Conversation

@janicduplessis
Copy link
Copy Markdown
Collaborator

@janicduplessis janicduplessis commented May 9, 2026

Summary

Resolves #42. iOS removes CAAnimations from a layer when the view leaves the window hierarchy. With react-navigation bottom tabs (which detach inactive screens by default via react-native-screens), this dropped any in-flight loop animations and they never restarted when the user returned to the tab — the loop appeared frozen until some unrelated re-render eventually triggered a new prop diff.

EaseView now keeps a per-view dictionary of in-flight loop animations with an explicit beginTime, and re-adds them in didMoveToWindow whenever the view re-attaches to a window. iOS preserves beginTime through addAnimation's internal copy, so the loop's phase continues seamlessly via (currentMediaTime - beginTime) mod period — visually it looks as if the animation kept running the whole time the view was off-screen, rather than restarting from frame 0.

I considered literally snapshotting presentationLayer at detach and resuming mid-cycle, but it requires chained CAAnimations or a CAKeyframeAnimation per property and per direction (for reverse), and the user-visible result is indistinguishable from beginTime preservation for the typical sub-second tab switch.

Out of scope: the same root cause applies on app foreground (after backgrounding). That uses a different hook (UIApplicationDidBecomeActiveNotification) and can be fixed in a follow-up.

Example app changes

  • New Issues section pattern: each fixed bug gets its own self-contained reproducer at example/app/issues/<n>/ so we can verify the fix and catch regressions later. The first one — app/issues/42/ — is a Tabs layout with a Loop tab (spin + pulse) and an Other tab (scrollview), matching the repro steps in react navigation stops loop animations #42.
  • The home screen list is split into top-level API / Demos / Issues tabs.
  • AGENTS.md now documents the reproducer pattern so future bug fixes ship with one.

Test Plan

iOS simulator (iPhone 16 Pro, iOS 26 toolchain), Debug build. Same scripted navigation in both clips: open Issue #42 reproducer, switch Loop → Other → Loop, twice.

Before:

before.mp4

After:

after.mp4

The mount #N indicator stays the same across the round-trips — confirms the EaseView is still mounted; the bug is purely the dropped CAAnimations, not a remount.

CI:

  • yarn format:write && yarn lint && yarn test — clean (82/82 passing)

iOS removes CAAnimations from a layer when the view leaves the window
hierarchy. With react-navigation tabs (which detach inactive screens by
default), this dropped any in-flight loop animations and they never
restarted on tab return — resolves #42.

Track in-flight loop animations in a per-view dictionary with an
explicit beginTime, and re-add them in didMoveToWindow when the view
re-attaches. Phase is preserved through the addAnimation copy, so the
animation looks as if it kept running while the view was off-screen.

Adds an Issues section to the example app with a tabs reproducer at
example/app/issues/42/, splits the home list into API / Demos / Issues
top-level tabs, and documents the reproducer pattern in AGENTS.md so
future bug fixes ship with a regression test.
@janicduplessis janicduplessis marked this pull request as ready for review May 9, 2026 17:55
@janicduplessis janicduplessis merged commit 4ec8f6b into main May 9, 2026
5 checks passed
@janicduplessis janicduplessis deleted the fix/ios-loop-tab-detach branch May 9, 2026 17:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

react navigation stops loop animations

1 participant