Hello devs π
Today I'm gonna tell you about a hacky way to detect screen lock in react native applications (iOS only).
As you probably already know, React Native gives you the AppState API, which helps to monitor the app state changes. But it's a bit restrictive, meaning that it has only three different states to look after (for Android, it's only two): active,
inactive,
and background.
Problem
I worked on the app where I needed to track when a user locks the screen. I hadn't used AppState in react native applications before, so I was hoping that background
meant "the screen is locked". So I checked the basic flow of these states on my iPhone using Expo. I put a console.log
in the render method and started looking at the states in the terminal, turning the screen on and off and switching between the apps.
Unfortunately, it turned out that the app always goes through the background
state no matter if you lock the screen or swipe to the Home screen or switch between the apps. And it doesn't only go directly to the background
state but also goes through inactive
on its way to background.
The only case when it doesn't go directly to the background is when you swipe up to the App Switcher and stay there for a while before swiping right or left to another app. This is the inactive
state.
App states
So basically, we can divide the state changes into three different scenarios:
- Whenever a user goes to the Home Screen, swipes to another app, or turns off the screen:
active -> inactive -> background.
- If a user goes to the App Switcher without swiping to another app:
active -> inactive.
- When a user brings the app back to the foreground:
background -> active.
In the search for solutions
Obviously, none of these cases fitted my need to track the screen lock. So I searched for some answers on the internet that could help me solve it. It turned out that the only way to do so is to exploit the magic of Native Modules. That means that I should either write a native module myself or use a third-party library to fill this gap.
None of the options seemed very compelling to me. First of all, I'm not a swift or a kotlin programmer, and I don't have much time to look into it. Of course, it's fun to learn new stuff, but only when it's systematic, planned and balanced approach. Not when you have to learn something new ad hoc to solve a little problem that you have right here right now.
That's why we usually use someone else's wheels instead of inventing our own. And this is when the third-party libraries and modules are at our service. So I looked for some React Native libraries on GitHub and found only this package.
But it is three years old, and it didn't work for me, unfortunately. And since I don't know how to debug native modules and I didn't want to spend more time on this, I continued searching, but everything else that I found was only some Objective C pieces of code like this one.
Another reason why I didn't want to use or create a native module for it was that I didn't want to eject from Expo because with it, React Native development is easier and much more fun. And of course, eventually, I would also have to write two different native modules: one for android and one for iOS.
Workaround
So I thought that maybe there is a way to bypass that limitation somehow, and I started looking closely at the behavior of that state changes. I noticed that when I minimize the app, i.e., go to the Home screen, the app goes from the inactive
state to background
a bit slower than when I lock the screen.
Using the code from the App State React Native tutorial, I added two Date objects to check the time difference, and it turned out that it was drastic.
export default class App extends React.Component {
state = {
appState: AppState.currentState,
};
a: any = 0;
b: any = 0;
componentDidMount() {
AppState.addEventListener('change', this._handleAppStateChange);
}
componentWillUnmount() {
AppState.removeEventListener('change', this._handleAppStateChange);
}
_handleAppStateChange = (nextAppState: any) => {
if (nextAppState == 'inactive') {
this.a = new Date();
}
if (nextAppState == 'background') {
this.b = new Date();
console.log(this.b - this.a);
}
if (nextAppState == 'active') {
this.a = 0;
this.b = 0;
}
};
render() {
return (
<View style={styles.container}>
<Text>Current state is: {this.state.appState}</Text>
</View>
);
}
}
The first scenario finished in ~800ms
, while the latter one finished within 5-8ms
. This is about 100 times faster to lock the screen than to minimize the app.
Thus, we can write an if
statement to check a transition from inactive
to background.
If it is done in less than 10ms
, we can assume that it is the screen lock, and if more than 100ms
we can assume that it is all other cases.
Conclusion
I understand that it is a completely unstable and very hacky workaround to make it work without any native modules and keeping Expo in the game. Of course, the numbers might be different from one version of iOS or Expo to another. Also, they might vary in the final build.
And this only works for iPhones because Android, unfortunately, doesn't have the inactive
state. But it has focus
and blur
events, which might fix this issue.
If you have any other solutions to this problem or some stable native modules, please share them with me. I would be glad to hear how you tackled this problem if you ever faced it. Also, any Android solution would be appreciated as well.
Thank you! And happy coding!
Top comments (0)