●MAX — Rork Max builds native Swift apps instead of React Native, supporting iPhone, iPad, Watch, TV, Vision Pro, and iMessage●NATIVE — It unlocks native capabilities: AR/LiDAR, Metal 3D games, Dynamic Island, Live Activities, HealthKit, and Core ML●CORE — Standard Rork generates iOS/Android apps with React Native (Expo), taking you from plain English to the app stores●FUNDING — Rork raised $2.8M from a16z (Andreessen Horowitz)●GROWTH — The platform now sees 743,000 monthly visits with 85% growth●PRICING — Free to start, with paid plans from $25/month●MAX — Rork Max builds native Swift apps instead of React Native, supporting iPhone, iPad, Watch, TV, Vision Pro, and iMessage●NATIVE — It unlocks native capabilities: AR/LiDAR, Metal 3D games, Dynamic Island, Live Activities, HealthKit, and Core ML●CORE — Standard Rork generates iOS/Android apps with React Native (Expo), taking you from plain English to the app stores●FUNDING — Rork raised $2.8M from a16z (Andreessen Horowitz)●GROWTH — The platform now sees 743,000 monthly visits with 85% growth●PRICING — Free to start, with paid plans from $25/month
The Private Screen That Lingers in the App Switcher — Hiding the Snapshot iOS Takes the Moment You Background Your App
When you send a React Native app generated by Rork to the background, iOS photographs the current screen for the App Switcher and writes it to disk. Journals and personal input screens linger there in plain sight. This walks through the iOS privacy overlay (why inactive, not background), Android's FLAG_SECURE, scoping it to sensitive screens only, and screenshot detection — all in working code.
One morning I opened my own journaling app, then reached to answer a call and double-tapped the home button. There, among the thumbnails in the App Switcher, was the personal line I'd been writing a moment earlier — fully legible. It wasn't anything I'd be mortified for family to see, but it hit me: this was a screen I never designed to be seen by anyone but me, and it had just been left out in the open.
The instant you send an app to the background, iOS photographs the current screen and uses it as the App Switcher preview. That image isn't held only in memory — it's written to disk. So the contents of your app sit on a home screen, before any passcode lock, for a while after you leave. Journals, manifestation notes, health data, the amount on a screen right before payment — the more a screen is meant for the owner's eyes only, the more it hurts to have it linger there.
I run several journaling and intention-tracking apps as an indie developer, and this defense of "the moment of leaving" isn't present by default in the React Native code Rork generates, nor in the Swift that Rork Max emits. This article builds the design that slips a shield in at exactly that moment — including the ordering pitfall that trips most people up first.
What you defend is the moment of leaving, not the time on screen
We tend to think of security as something that happens while a screen is displayed. But what we want to protect here is the split second of the transition as the user leaves. When your app drops out of the foreground, iOS takes a snapshot of the UI for the App Switcher. And it's captured not "after the app has fully gone to the background," but at the inactive state just before that.
React Native's AppState represents this transition with three values: active while running in the foreground, the transitional inactive that accepts no input (iOS only), and background. Pulling down Control Center, receiving a call, opening the App Switcher — all of these pass through inactive first.
Here's the first trap. If you show the shield only after the state becomes background, you miss the snapshot entirely. It was already taken back at inactive. So lock this in from the start: the entry point for your defense is inactive, not background.
iOS: show a privacy overlay on inactive
First, place an overlay at the root of the app that's transparent (effectively hidden) while active, and covers the whole screen otherwise. We keep state management lightweight — no external library, just a small AppState subscription hook.
// hooks/usePrivacyShield.tsimport { useEffect, useRef, useState } from "react";import { AppState, AppStateStatus } from "react-native";// Returns true whenever the state is NOT active (inactive / background).// iOS captures its snapshot during inactive, so we must not wait for background.export function usePrivacyShield(enabled: boolean) { const [shielded, setShielded] = useState(false); const appState = useRef<AppStateStatus>(AppState.currentState); useEffect(() => { if (!enabled) { setShielded(false); return; } const handle = (next: AppStateStatus) => { // Reveal only when we return to active. // Every other transition (inactive / background) falls to the covered side. setShielded(next !== "active"); appState.current = next; }; // Guard against the initial frame (launch can begin in inactive). setShielded(AppState.currentState !== "active"); const sub = AppState.addEventListener("change", handle); return () => sub.remove(); }, [enabled]); return shielded;}
Use this hook in exactly one place at the very top of the app and lay the covering UI over everything. Don't put it per-screen: at the moment of transition there's no room to decide "which screen am I on right now," so it belongs at the root.
// components/PrivacyShield.tsximport { View, StyleSheet, Image } from "react-native";import { usePrivacyShield } from "../hooks/usePrivacyShield";export function PrivacyShield({ enabled }: { enabled: boolean }) { const shielded = usePrivacyShield(enabled); if (!shielded) return null; // A single brand logo, centered. Draw none of the actual content. return ( <View style={styles.cover} pointerEvents="none"> <Image source={require("../assets/shield-logo.png")} style={{ width: 96, height: 96, opacity: 0.9 }} resizeMode="contain" /> </View> );}const styles = StyleSheet.create({ cover: { ...StyleSheet.absoluteFillObject, backgroundColor: "#0E1116", alignItems: "center", justifyContent: "center", zIndex: 9999, },});
At the root, wire it up like this. Keeping enabled driven by "are we on a sensitive screen right now" lets you grow this into per-screen control later.
// app/_layout.tsx (Expo Router example)import { Stack } from "expo-router";import { PrivacyShield } from "../components/PrivacyShield";export default function RootLayout() { return ( <> <Stack /> {/* Start by enabling it app-wide; scope it per-screen later */} <PrivacyShield enabled={true} /> </> );}
With this in place, the instant you return to the home screen or open the App Switcher, the screen flips to the single-logo shield, and the snapshot that gets captured is of the shielded state.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦If you've worried that a personal input screen stays visible in the App Switcher, you can ship an overlay that slips in at the exact moment of backgrounding — today
✦You'll understand why background is too late and inactive is the right trigger, and why iOS and Android defend this in completely different ways, so you never protect one platform and assume you're covered
✦You'll implement a production pattern that scopes FLAG_SECURE to sensitive screens only and uses screenshot detection to quietly inform the user
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
The very thing I tripped on in my first implementation was this ordering. I started by thinking "hide it once we drop to the background," covering only when next === "background" — and the App Switcher thumbnail still showed the contents, plain as day.
// ❌ Doesn't work: waiting for background misses the captureconst handle = (next: AppStateStatus) => { setShielded(next === "background");};
// ✅ Correct: cover on anything other than active (don't miss inactive)const handle = (next: AppStateStatus) => { setShielded(next !== "active");};
The reason, as above, is that the iOS snapshot is taken at inactive, before reaching background. Falling to the shielded side for everything that isn't active catches inactive without leaking. Writing it symmetrically — reveal only when we return to active — means even tiny transitions, like nudging Control Center down and immediately coming back, don't break it.
You may be tempted to add a delay because the shield flickers briefly on every inactive. Don't. A delay opens exactly the gap in which the snapshot is taken. Treat the flicker as proof the defense works, and reduce the friction by designing the overlay to sit naturally within your brand.
Android: FLAG_SECURE is an entirely different mechanism
If you stop here feeling safe, the Android side stays defenseless. Android's Recents screen shows a preview the same way, but the defense is unrelated to iOS.
Android has a window flag, FLAG_SECURE. Raise it and the Recents preview goes blank, and on top of that screenshots and screen recording are blocked at the OS level. In Expo, expo-screen-capture's preventScreenCaptureAsync() sets this flag.
npx expo install expo-screen-capture
// Android: raise FLAG_SECURE to blank Recents + block screenshotsimport * as ScreenCapture from "expo-screen-capture";import { Platform } from "react-native";export async function enableAndroidSecureFlag() { if (Platform.OS !== "android") return; // This alone blanks the Recents thumbnail and stops screenshots/recording await ScreenCapture.preventScreenCaptureAsync("privacy-screen");}export async function disableAndroidSecureFlag() { if (Platform.OS !== "android") return; await ScreenCapture.allowScreenCaptureAsync("privacy-screen");}
The key point: the iOS overlay and Android's FLAG_SECURE protect different things. FLAG_SECURE does nothing for the iOS Recents snapshot (iOS has no equivalent flag). Conversely, the overlay you built for iOS does not blank Android's Recents or block screenshots. Only by using both together do you get "no contents left in the App Switcher / Recents" on both platforms. Implementing one and assuming the whole thing is covered is the easiest mistake to make.
Scope it to sensitive screens, not the whole app
Leaving FLAG_SECURE raised app-wide means users can't screenshot any screen at all. Reporting a bug on the settings screen, showing a favorites list to a friend — taking away those legitimate actions too is going too far. I raise the flag only when entering a sensitive screen and restore it on the way out.
With Expo Router or React Navigation's useFocusEffect, you can control this cleanly in step with the screen appearing and disappearing.
// Inside a sensitive screen component (e.g. the journal editor)import { useFocusEffect } from "expo-router";import { useCallback, useContext } from "react";import { enableAndroidSecureFlag, disableAndroidSecureFlag,} from "../lib/secureFlag";import { PrivacyContext } from "../lib/PrivacyContext";function JournalEditorScreen() { const { setSensitive } = useContext(PrivacyContext); useFocusEffect( useCallback(() => { // On entering: enable the iOS overlay, set FLAG_SECURE on Android setSensitive(true); // flip the iOS overlay's enabled to true enableAndroidSecureFlag(); // blank Android Recents + block screenshots return () => { // On leaving: restore everything setSensitive(false); disableAndroidSecureFlag(); }; }, [setSensitive]) ); // ...screen body}
PrivacyContext is a paper-thin Context whose only job is to toggle the enabled of the PrivacyShield above. setSensitive(true) enables the iOS overlay and, in the same moment, raises the Android flag. That lines up "protect on both platforms only while on a sensitive screen." Forgetting to restore on exit leads to accidents, so always pair the restore in the useFocusEffect cleanup function.
Detect what slips through after the fact
Beyond prevention, being able to notice that something was captured has value on some screens. expo-screen-capture has a listener that fires the moment a screenshot is taken. There's nothing wrong with the owner saving their own record, but for, say, a shared family account where a personal entry gets captured, you can quietly inform the owner.
import * as ScreenCapture from "expo-screen-capture";import { useEffect } from "react";function useScreenshotNotice(onShot: () => void) { useEffect(() => { // Screenshot detection works on both iOS and Android // (pair with isCaptured for recording detection) const sub = ScreenCapture.addScreenshotListener(() => { onShot(); }); return () => sub.remove(); }, [onShot]);}
Detection is a trigger for a notice or a log; it has no power to stop the screenshot itself (that's the job of Android's FLAG_SECURE). So the realistic use is as a supporting line: for the iOS screenshot you can't block, quietly let only the owner know. Over-warning just stokes anxiety, so keep the wording plain and show it once.
A table for where to apply what
You don't need to defend every screen uniformly. The stronger the defense, the more you take away legitimate user actions (sharing via screenshot, reporting bugs). Decide how far to go per the nature of each screen.
Nature of the screen
iOS privacy overlay
Android FLAG_SECURE
Screenshot detection
Journal / personal entry input & view
Apply
Apply
Optional (quiet notice)
Health / mental / body data
Apply
Apply
Optional
Pre-payment / amounts / card info
Apply
Apply
Don't (tends to get in the way)
General lists / settings / help
Don't apply
Don't apply
Don't apply
Wallpaper / image viewing (sharing intended)
Don't apply
Don't apply (don't take away sharing)
Don't apply
There's a single axis for the decision: "Would it cause harm for this screen's contents to be seen, even for an instant, by someone other than the owner?" If yes, defend it; if it's a screen you want shared, leave it open. In apps like wallpaper apps that I want shown and spread, I apply none of this, and reserve it for the relevant screens of apps that handle personal records.
Try it on one screen first
As a first step, pick the single most personal screen in your app — the journal editor, a health-data detail — and wire the iOS overlay enable and Android FLAG_SECURE together there via useFocusEffect. Send a real device to the home screen and visually confirm, in both the App Switcher (iOS) and Recents (Android), that the thumbnail is shielded or blank. Once that's confirmed, you can quietly extend to the rest of your sensitive screens along the table above.
The essence of this defense isn't a flashy feature — it's the single fact that a user can move their hands in peace, trusting that "what I write here is mine alone." I hope this helps anyone building apps that handle personal records the same way.
Share
Thank You for Reading
Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.