Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions example/app/issues/66/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Stack } from 'expo-router';
import { EaseView } from 'react-native-ease';

// Issue #66 — iOS loops stop after the app returns from the background.
// https://github.com/appandflow/react-native-ease/issues/66
//
// Steps to reproduce:
// 1. Both loops below run.
// 2. Press Home, then reopen the app.
// 3. Expected: both keep running from where they were, and the mount id is
// unchanged. Without the fix they stay frozen at their resting value.

export default function BackgroundLoopsScreen() {
const [mountId] = useState(() => Math.random().toString(36).slice(2, 8));

return (
<View style={styles.root}>
<Stack.Screen options={{ title: 'Issue #66 — Background loops' }} />
<Text style={styles.body}>
Background the app (Home), then reopen it. Both loops must keep running.
Mount {mountId} must not change.
</Text>
<View style={styles.row}>
<EaseView
initialAnimate={{ rotate: 0 }}
animate={{ rotate: 360 }}
transition={{
type: 'timing',
duration: 2000,
easing: 'linear',
loop: 'repeat',
}}
style={styles.spinner}
/>
<EaseView
initialAnimate={{ scale: 0.6, opacity: 0.4 }}
animate={{ scale: 1, opacity: 1 }}
transition={{
type: 'timing',
duration: 900,
easing: 'easeInOut',
loop: 'reverse',
}}
style={styles.pulse}
/>
</View>
</View>
);
}

const styles = StyleSheet.create({
root: {
flex: 1,
padding: 20,
backgroundColor: '#1a1a2e',
},
body: {
color: '#a0a0c0',
fontSize: 14,
marginBottom: 32,
},
row: {
flexDirection: 'row',
gap: 40,
alignItems: 'center',
},
spinner: {
width: 64,
height: 64,
borderRadius: 12,
backgroundColor: '#4a90d9',
},
pulse: {
width: 64,
height: 64,
borderRadius: 32,
backgroundColor: '#e94560',
},
});
5 changes: 5 additions & 0 deletions example/src/demos/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,11 @@ export const demos: Record<string, DemoEntry> = {
title: 'Issue #54 — Stale shimmer loop',
section: 'Issues',
},
'issue-66': {
route: '/issues/66',
title: 'Issue #66 — Background loops',
section: 'Issues',
},
'issue-loop-cancel': {
route: '/issues/loop-cancel',
title: 'Audit — Cancelled loop resurrects',
Expand Down
22 changes: 22 additions & 0 deletions ios/EaseView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,11 @@ - (instancetype)initWithFrame:(CGRect)frame {
_loopAnimations = [NSMutableDictionary dictionary];
_delegateProxy = [EaseAnimationDelegateProxy new];
_delegateProxy.target = self;
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(applicationDidBecomeActive)
name:UIApplicationDidBecomeActiveNotification
object:nil];
}
return self;
}
Expand Down Expand Up @@ -517,6 +522,9 @@ - (void)reapplyLoopAnimations {
[CATransaction begin];
[CATransaction setDisableActions:YES];
for (NSString *key in _loopAnimations) {
if ([self.layer animationForKey:key] != nil) {
continue;
}
// Increment to balance the eventual animationDidStop callback when the
// view detaches again (or the loop is replaced).
_pendingAnimationCount++;
Expand Down Expand Up @@ -1391,6 +1399,20 @@ - (void)didMoveToWindow {
}
}

// Core Animation drops a layer's animations while the app is in the
// background, and only accepts them again once the app is active. The view
// never leaves its window, so didMoveToWindow doesn't replay the loops.
- (void)applicationDidBecomeActive {
if (self.window == nil || _isFirstMount || _loopAnimations.count == 0) {
return;
}
if (_props) {
[self removeStaleLoopAnimationsForProps:*std::static_pointer_cast<
const EaseViewProps>(_props)];
}
[self reapplyLoopAnimations];
}

- (void)invalidateLayer {
[super invalidateLayer];

Expand Down
Loading