●FUNDING — Rork raises a $15M seed led by Left Lane Capital●RORK MAX — Rork Max generates native Swift apps instead of React Native●PLATFORM — It targets iPhone, iPad, Watch, and Vision Pro, reaching Live Activities and Core ML●GROWTH — Traffic keeps climbing at 743K monthly visits and 85% growth●TEST — The Companion app lets you test on a real device without a paid Apple Developer account●STACK — Built on React Native and Expo for true native experiences, not web wrappers●FUNDING — Rork raises a $15M seed led by Left Lane Capital●RORK MAX — Rork Max generates native Swift apps instead of React Native●PLATFORM — It targets iPhone, iPad, Watch, and Vision Pro, reaching Live Activities and Core ML●GROWTH — Traffic keeps climbing at 743K monthly visits and 85% growth●TEST — The Companion app lets you test on a real device without a paid Apple Developer account●STACK — Built on React Native and Expo for true native experiences, not web wrappers
Previewing Files In-App in Rork — calling Quick Look safely from Expo
How to preview PDFs, images, and Office documents in place without sending users out of your app, using Quick Look (QLPreviewController) from Rork (Expo). Covers pre-downloading remote files, the local-URL requirement, the Android FileProvider alternative, and handling the share button.
A user taps an invoice PDF they received inside the app, gets bounced out to Safari, and can't find their way back — building apps as an indie developer, I've felt the pain of this "left and never returned" experience. Sending someone away just to view a file breaks the context they were in.
iOS has a mechanism built for exactly this: Quick Look (QLPreviewController). It opens PDFs, images, text, CSV, and Office documents like Word and Excel as a full-screen preview inside your app. A standard Rork app is generated as React Native (Expo), so you call this native Quick Look through a thin bridge module. The crux of the design was knowing precisely what you can and can't hand it.
What Quick Look can and can't open
First, the premise. Quick Look is a local file preview mechanism. Getting this wrong is how the first implementation stumbles.
It opens local file URLs (paths starting with file://)
Passing an https:// remote URL directly won't work. You have to download it locally first
Supported formats are broad: PDF, images (JPEG/PNG/HEIC), text, CSV, RTF, iWork, Microsoft Office, USDZ (3D), and more
A correct extension is the hint for type detection. With an extensionless name like document.tmp, even genuine PDF content sometimes won't open
What I tripped on first was handing it a signed URL fetched from the server as-is. Quick Look just shows a blank, with no error. Locking in the rule "always cache remote files before opening" from the start is the shortcut.
Cache the remote file first
In Expo, download with expo-file-system and hand the saved local URI to Quick Look. If it's already downloaded, reusing it instead of re-fetching keeps the wait short even for a large PDF.
// downloadForPreview.tsimport * as FileSystem from 'expo-file-system';// Cache a remote URL and return the file:// local pathexport async function ensureLocalCopy(remoteUrl: string, filename: string) { const target = FileSystem.cacheDirectory + filename; const info = await FileSystem.getInfoAsync(target); if (info.exists) return target; // don't re-download if present const { uri, status } = await FileSystem.downloadAsync(remoteUrl, target); if (status !== 200) { throw new Error(`download failed: ${status}`); } return uri;}
Always include the correct extension in the filename. If the server doesn't return one, deriving .pdf or .xlsx from the Content-Type yourself keeps Quick Look's type detection stable.
✦
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
✦You'll be able to let users view PDFs, images, and Office docs inside the app instead of handing them to an external app
✦You'll see, with working code, why passing a remote URL directly fails and the correct flow of caching first, then opening the local URL
✦You'll learn the design that folds iOS Quick Look and Android FileProvider+ACTION_VIEW into one call site, plus when to suppress the share button
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.
QLPreviewController is a UIViewController, so present it from your Expo native module on top of the current top screen. The data source is a thin thing that just returns the item to preview.
import QuickLookfinal class PreviewBridge: NSObject, QLPreviewControllerDataSource { private var url: URL! func present(path: String, from vc: UIViewController) { self.url = URL(fileURLWithPath: path) let preview = QLPreviewController() preview.dataSource = self vc.present(preview, animated: true) } func numberOfPreviewItems(in controller: QLPreviewController) -> Int { 1 } func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem { return url as QLPreviewItem }}
From JavaScript, make it a call site that just passes the downloaded local path.
import { ensureLocalCopy } from './downloadForPreview';import QuickLookPreview from './QuickLookBridge'; // native moduleexport async function openPreview(remoteUrl: string, filename: string) { const localPath = await ensureLocalCopy(remoteUrl, filename); // The implementation passes a path with file:// stripped on iOS await QuickLookPreview.present(localPath.replace('file://', ''));}
On Android, substitute with FileProvider and ACTION_VIEW
Android has no Quick Look. Instead, build a safe content URI with FileProvider and hand it off via Intent.ACTION_VIEW. If you insist on a full-screen in-app view, using a built-in viewer for PDFs only and delegating the rest to external apps turned out to be the practical compromise.
fun openWithViewer(context: Context, file: File, mime: String) { val uri = FileProvider.getUriForFile( context, "${context.packageName}.fileprovider", file ) val intent = Intent(Intent.ACTION_VIEW).apply { setDataAndType(uri, mime) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } // Guard against crashing when no app can handle it if (intent.resolveActivity(context.packageManager) != null) { context.startActivity(intent) } else { // Fallback: a chooser or a built-in PDF viewer }}
Keep the JS openPreview under the same name and branch so iOS calls Quick Look and Android calls this Intent. Folding it into one call site means your screen code never has to think about platform differences.
How to handle the share and print buttons
Quick Look shows share and print buttons in the top right by default. This needs a design decision.
Nature of the file
Share button handling
The user's own data (receipts, their own creations)
Fine to allow as-is
Paid or limited-distribution content
You'll want to suppress share/print (prevent exfiltration)
A temporary confirmation preview
Suppress and keep it inside the app
When you want to suppress sharing, handle it through the QLPreviewController delegate that controls edit/share capability rather than the QLPreviewItem. Complete exfiltration prevention is hard given how the OS works, so it's safest to design on the assumption that "screenshots can still be taken" and not over-promise. In my case the line is: free confirmation previews allow share; paid content switches to a built-in viewer and skips Quick Look entirely.
Large files and cleanup after closing
Download a tens-of-MB PDF and then open it, and you get a silent wait. Don't open the preview while downloading — show progress, and users feel more at ease. expo-file-system's createDownloadResumable gives you a progress callback.
Cleanup after closing is easy to forget. Files dropped into the cache pile up if you leave them. My policy is "keep the most recently opened ones, and sweep caches untouched for a while at startup." Anything under cacheDirectory the OS will clear when it has to, but setting your own ceiling avoids storage pressure.
When a file can be viewed to completion inside the app, the user keeps working without losing context. Quick Look itself is a few lines to call, but it only becomes a viewing path you can rely on once you include remote pre-downloading, the Android substitute, share gating, and cleanup. I hope it helps anyone working on the same implementation.
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.