Conversation
|
|
Yes, thanks, that makes sense. Without clearing loadingAnimationView, an in-flight async load can still complete and override the currently set animation. Resetting it in removeCurrentAnimationView() would prevent stale completions from mutating state and let ARC clean up the loader. I’ve made the changes accordingly. |
|
Hey, just wanted to add that we're hitting the bug this PR fixes in production. |
|
We're also blocked on a release here. We need to update lottie to work with recent expo versions, which is mandatory to migrate to 16kb page sizes on android. But upgrading lottie breaks our app on iOS. |
|
@matinzd have you had a chance to take a look at this? |
|
any update on this? |
|
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
|
not an issue, not stale |
|
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
Summary
This PR fixes five interconnected bugs affecting
LottieViewon iOS with the New Architecture (Fabric) when usingautoPlay={false}and/orloop={false}. The bugs cause animations to start automatically despiteautoPlay={false}, preventonAnimationLoadedfrom firing, and break subsequent visits to screens containing aLottieView.Bug 1 —
setSpeedunconditionally starts playback regardless ofautoPlayFile:
ios/LottieReactNative/ContainerView.swiftRoot cause
setSpeedcalledanimationView?.play()whenever speed was non-zero and the animation was not already playing, ignoring theautoPlayflag entirely:On every mount, LottieView's JS
defaultProps.speed = 1causessetSpeed(1)to be called. This started the animation immediately, even withautoPlay={false}.Fix
Remove the
play()call fromsetSpeed. Playback responsibility belongs exclusively tosetAutoPlayandreplaceAnimationView→playIfNeeded().Bug 2 — Fabric view recycling:
autoPlaynot yet updated whensetSpeedfiresFile:
ios/Fabric/LottieAnimationViewComponentView.mmRoot cause
In
updateProps:oldProps:,setSpeedwas called on line 64, whilesetAutoPlaywas called on line 96. When Fabric recycles a native view previously used by a component withautoPlay=true(e.g. an animation that played once), the recycledContainerView._autoPlayis still true at the momentsetSpeedis called with the new component's speed.Even with Bug 1 fixed, any future code in
setSpeedthat checksautoPlaywould see the stale value.Additionally, loop had the same ordering problem.
Fix
Move
setAutoPlayandsetLoopto the top ofupdateProps:oldProps:, beforesetSpeedand all source setters:Bug 3 —
setSourceDotLottieURIloses the loader view via ARCFile:
ios/LottieReactNative/ContainerView.swiftRoot cause
Assigning the temporary
LottieAnimationViewto_discards the only strong reference. ARC can deallocate the view before the async load completes, silently dropping the completion callback. As a result,replaceAnimationViewis never called andonAnimationLoadednever fires.Fix
Retain the loader in a stored property and release it inside the completion:
Bug 4 —
animationLoadedclosure assigned after the event already firedFile:
ios/LottieReactNative/ContainerView.swiftRoot cause
In
replaceAnimationView,animationView.animationLoadedwas assigned afteraddSubview. For animations loaded viadotLottieUrl, theLottieAnimationViewpassed to the completion callback already has the composition loaded. Lottie'sanimationLoadedproperty uses a SwiftdidSetobserver that immediately calls the closure ifanimation != nil. By assigning afteraddSubview, this immediate call was missed.Fix
Assign
animationLoadedfirst, before any other setup:Bug 5 —
onAnimationLoadeddropped when Fabric attaches event emitter after load completes (race condition + view pool recycling)Files:
ios/Fabric/LottieAnimationViewComponentView.mm,ios/Fabric/LottieContainerView.h,ios/LottieReactNative/ContainerView.swiftRoot cause — race condition
In Fabric,
updateProps:oldProps:is called beforeupdateEventEmitter:. For localfile:// .lottieassets, the async load insetSourceDotLottieURIcompletes very quickly (synchronously in some cases), meaningonAnimationLoadedfires while_eventEmitteris stillnil. The existingguard if (!_eventEmitter) return;silently drops the event.Root cause — view pool recycling
When a user navigates away from a screen (unmounting it) and then navigates back, Fabric recycles native views. The recycled
LottieContainerViewalready has the animation loaded from the previous mount. SincesourceDotLottieURIhasn't changed, Fabric's prop diffing skipssetSourceDotLottieURI,replaceAnimationViewis never called, andonAnimationLoadednever fires for the new component instance.Fix
Track whether the animation has been loaded in a new
isAnimationLoaded: Boolproperty on ContainerView. OverrideupdateEventEmitter:in the Fabric component view to emitonAnimationLoadedwhen_view.isAnimationLoadedistrue. This method is called on mount (including recycled views) but not on re-renders, making it the correct place for this check.Affected platforms
iOS only (New Architecture / Fabric). Android and Old Architecture are not affected.
Test scenario
LottieViewwithautoPlay={true}.LottieViewwithautoPlay={false}andloop={false}.onAnimationLoaded.onAnimationLoadedon return.