How to Inject a SwiftUI View Inside a Capacitor App
A plugin plus UIHostingController puts real SwiftUI over the WebView. Here is the bridge contract and the three integration shapes.
TL;DR
Injecting SwiftUI into a Capacitor app means a plugin method called from JavaScript reaches Swift, wraps a SwiftUI view in a UIHostingController, and presents it over the WebView, with the result returning as JSON. Default to the full-screen native modal, it has the clean lifecycle, use persistent native chrome when branding demands it, and accept an embedded island only when a modal genuinely cannot serve. Treat the boundary like an API: request in, events out, the promise resolved exactly once on every exit path including swipe dismissal. Start the native screens from a free VP0 design an agent like Claude Code or Cursor extends, and treat a growing native-screen list as a framework conversation, not a plugin backlog.
What injecting SwiftUI into Capacitor means
A Capacitor app is a web app inside a native shell: your screens are HTML in a WebView, and Capacitor’s plugin system is the bridge to everything native. Injecting SwiftUI means using that bridge to put real native views in front of the user, a screen, a sheet, or a component rendered by SwiftUI rather than by the web layer. The mechanism is a short chain: a plugin method called from JavaScript reaches Swift, the Swift side wraps a SwiftUI view in a UIHostingController, and that controller is presented over or embedded into the view hierarchy the WebView lives in.
The reason teams want this is always the same: some experience refuses to feel right in the WebView, a camera flow, a payment sheet, a gesture-heavy interaction, a screen where scrolling physics betray the web. The platform is genuinely popular for the shell pattern, @capacitor/core pulls roughly 2,721,240 weekly npm downloads, so the question is rarely whether Capacitor was a wrong choice; it is how to give specific moments native quality without rebuilding the app.
How does the bridge actually work?
Three pieces, each small. On the web side you define a plugin interface and call it like any async function. On the native side a Capacitor plugin class receives the call with a JSON payload, builds the SwiftUI view with that data, wraps it in a UIHostingController, and presents it from the bridge’s view controller, the standard UIViewController presentation machinery. When the native experience finishes, the plugin resolves the call with a JSON result, and the web app continues with whatever the user did.
The discipline that keeps it sane is treating the boundary like a network API. Everything crossing it is serializable data, in and out, with the SwiftUI side owning its own internal state while it is on screen and reporting a result, never reaching back into the web app’s state directly. Teams that let the two layers share state ad hoc end up debugging two sources of truth; teams that pass requests in and results out keep both sides simple enough to reason about.
The three integration shapes
| Shape | What the user sees | The work |
|---|---|---|
| Full-screen native modal | A SwiftUI screen presented over the web app | Cleanest; present, do the job, resolve with a result |
| Persistent native chrome | A native header or bar framing the WebView | Layout coordination; the web content must respect it |
| Embedded native island | A SwiftUI component inside a web layout | Hardest; position sync, z-order, scroll coordination |
The full-screen modal is the shape to default to, because it has a clean lifecycle: the web app asks, the native screen owns the user until it resolves, and nothing fights over layout. Persistent chrome is well-trodden, the custom native header pattern is its worked example. The embedded island is where budgets go to die, since a native view positioned inside a scrolling web page must track scroll, resize, and stacking continuously; choose it only when the component truly cannot be a modal. Whichever shape, the SwiftUI screens themselves are ordinary iOS design work, and a free VP0 design gives an agent like Claude Code or Cursor a real screen to extend from its machine-readable source page, so the native moments look native, not improvised.
Passing data and events across the bridge
Requests in, results and events out, all JSON. The call that opens the native screen carries everything that screen needs to render, ids, titles, amounts, configuration, because fetching state back across the bridge mid-flight is where complexity multiplies. While the native screen is up, anything the web side must know about gets emitted as plugin events, progress, selection changed, and the final outcome arrives as the resolved result of the original call: completed with data, cancelled, or failed with a reason.
Version the contract the way you would an API, because that is what it is. A short markdown file in the repo naming each plugin method, its request fields, its events, and its result shape keeps the web and Swift sides honest with each other, and it doubles as the prompt context an agent needs to extend either side without inventing fields the other never sends.
Two native-side details earn attention early. Dismissal must be unified, the user swiping the sheet away, tapping cancel, or completing the flow should all resolve the JavaScript promise exactly once, since a swipe-dismissed sheet that never resolves leaves the web app awaiting forever. And the hosting controller needs its sizing told honestly, sheets with detents, full-screen covers, and keyboard avoidance are decisions the SwiftUI layer owns, not things the WebView can negotiate.
When this is the wrong move
Injection is a scalpel, and reaching for it weekly means the architecture is arguing with you. One or two native moments, camera, payments, a signature flow, are exactly what the plugin system is for. When the list of screens that “need to be native” keeps growing, the honest conversation is the framework one, because a Capacitor app that is half native plugins carries both platforms’ complexity with neither’s coherence, and the comparison that decides it is covered in React Native versus Capacitor.
The migration direction matters too. If the product’s center of gravity is shifting native, porting the web UI a screen at a time into the shell rarely beats a deliberate rebuild, the path explored in moving from Ionic to React Native, where the existing app serves as the spec. Injection buys quality for moments; it does not convert an architecture. Knowing which problem you have before writing the first plugin is the cheapest decision in the whole project.
Common mistakes when vibe coding the bridge
The classic generated mistake is the hostage promise: the agent wires the happy path, present the controller, resolve on the done button, and forgets every other exit, so the first swipe-dismiss strands the web app awaiting a promise that never settles. State the rule in the prompt: every presentation path resolves exactly once, including dismissal, cancellation, and failure.
Three more recur. The agent passes a live object across the bridge in spirit, designing the native screen to call back for state mid-flight, instead of sending everything needed up front; insist on request-in, result-out. It forgets the WebView keeps running underneath, so timers, audio, or navigation continue behind the native modal and the app returns to a surprise; pause what should pause, and resume it deliberately when the promise resolves rather than trusting the page to notice. And it hardcodes presentation from the key window’s root controller instead of the bridge’s view controller, which works until a sheet is already up and then presents nothing, with no error. Each fix is one sentence in a prompt and an afternoon in a debugger, which is the usual exchange rate.
Key takeaways: SwiftUI inside a Capacitor app
- The bridge is a plugin, the wrapper is UIHostingController. JavaScript asks; Swift presents; the result comes back as JSON.
- Default to the full-screen modal. Clean lifecycle, no layout fights; embedded islands are the expensive exception.
- Treat the boundary like an API. Requests in, results and events out, serializable, resolved exactly once.
- Injection is for moments, not migration. A growing native-screen list is a framework conversation.
- Start the native screens from a free VP0 design. The agent extends them from the source page; you own the bridge contract.
The practical shape
Build the first native moment as a full-screen modal plugin: one method in, one SwiftUI screen wrapped in a hosting controller, one promise resolved on every exit path. Start the screen itself from a free VP0 design extended by your agent, keep the bridge contract written down, request fields, events, result shape, and pause the web layer’s activity while native owns the user. Add the persistent-chrome shape when branding demands it, and accept an embedded island only after the modal version has genuinely failed the product. If you find yourself planning the fourth or fifth injected screen, stop and have the architecture conversation instead, because at that point you are not adding native moments to a web app, you are building a native app inside a web app’s plumbing, and there are better ways to build a native app.
Frequently asked questions
How do I inject a SwiftUI view inside a Capacitor app? Write a Capacitor plugin: a method callable from JavaScript that, on the Swift side, builds your SwiftUI view, wraps it in a UIHostingController, and presents it from the bridge’s view controller. Pass everything the screen needs as the call’s JSON payload, emit plugin events for anything the web side must know mid-flight, and resolve the call exactly once with the outcome, completed, cancelled, or failed, on every exit path including swipe dismissal. A free VP0 design supplies the SwiftUI screen an agent extends.
Can SwiftUI views be embedded inside the web page layout? Yes, but it is the expensive shape. A native view positioned within scrolling web content has to track scroll position, resizing, and z-order continuously, and the coordination code dwarfs the component it hosts. Reach for the embedded island only when the experience truly cannot be a modal or persistent chrome, for example a live camera preview inline in a feed. For most products the full-screen native modal delivers the native quality with a fraction of the maintenance.
When should I use native SwiftUI screens in Capacitor instead of web UI? For the moments where the WebView betrays the experience: camera and scanning flows, payment sheets, gesture-heavy interactions, and anything where scroll physics or keyboard behavior must feel exactly native. One to three such moments is the pattern working as designed. If the list keeps growing, treat it as an architecture signal rather than a plugin backlog, and have the framework conversation, because a shell app that is half native carries both complexities with neither coherence.
How do data and events flow between the WebView and SwiftUI? Like an API: the opening call carries a JSON request with everything the native screen needs, plugin events stream anything the web side must observe while native is up, and the original promise resolves once with the result. The SwiftUI layer owns its internal state while on screen and never reaches into web state directly. That discipline, request in, result out, exactly-once resolution, is what keeps two UI worlds from becoming two sources of truth.
Is there a free template for the native screens? The native moments deserve real iOS design, and VP0 provides it free: actual SwiftUI-ready screen designs with a machine-readable source page that Claude Code, Cursor, or another agent reads from a pasted link and extends into the plugin’s view. The bridge contract, the plugin method, the payload shape, the exactly-once resolution, is the part you define, and it is small once written down. That split holds because screens transfer between products while bridge contracts are specific to yours.
What the VP0 community is asking
How do I inject a SwiftUI view inside a Capacitor app?
Write a Capacitor plugin: a method callable from JavaScript that, on the Swift side, builds your SwiftUI view, wraps it in a UIHostingController, and presents it from the bridge's view controller. Pass everything the screen needs as the call's JSON payload, emit plugin events for anything the web side must know mid-flight, and resolve the call exactly once with the outcome, completed, cancelled, or failed, on every exit path including swipe dismissal. A free VP0 design supplies the SwiftUI screen an agent extends.
Can SwiftUI views be embedded inside the web page layout?
Yes, but it is the expensive shape. A native view positioned within scrolling web content has to track scroll position, resizing, and z-order continuously, and the coordination code dwarfs the component it hosts. Reach for the embedded island only when the experience truly cannot be a modal or persistent chrome, for example a live camera preview inline in a feed. For most products the full-screen native modal delivers the native quality with a fraction of the maintenance.
When should I use native SwiftUI screens in Capacitor instead of web UI?
For the moments where the WebView betrays the experience: camera and scanning flows, payment sheets, gesture-heavy interactions, and anything where scroll physics or keyboard behavior must feel exactly native. One to three such moments is the pattern working as designed. If the list keeps growing, treat it as an architecture signal rather than a plugin backlog, and have the framework conversation, because a shell app that is half native carries both complexities with neither coherence.
How do data and events flow between the WebView and SwiftUI?
Like an API: the opening call carries a JSON request with everything the native screen needs, plugin events stream anything the web side must observe while native is up, and the original promise resolves once with the result. The SwiftUI layer owns its internal state while on screen and never reaches into web state directly. That discipline, request in, result out, exactly-once resolution, is what keeps two UI worlds from becoming two sources of truth.
Is there a free template for the native screens?
The native moments deserve real iOS design, and VP0 provides it free: actual SwiftUI-ready screen designs with a machine-readable source page that Claude Code, Cursor, or another agent reads from a pasted link and extends into the plugin's view. The bridge contract, the plugin method, the payload shape, the exactly-once resolution, is the part you define, and it is small once written down. That split holds because screens transfer between products while bridge contracts are specific to yours.
Part of the React Native & Expo: Mobile Frontend Architecture hub. Browse all VP0 topics →
Keep reading
v0 by Vercel mobile app export: from web to a real iOS app
v0 by Vercel generates web React, so there is no direct mobile export. Here are the three real paths from a v0 design to an iOS app, and when each fits.
An Offline-First Folder Architecture for Expo Apps
An offline-first Expo app needs a clear data, sync, and UI split. Here is a folder architecture that keeps offline working and an AI agent on track while it builds.
Lovable AI Source Limits: Structure Layouts Before Export
Lovable AI caps how much source it writes per request, so big mobile screens come back incomplete. Here is how to structure layouts so the output stays clean.
Untangle a FlutterFlow Export and Run the Bare Source
A FlutterFlow export is a complete Flutter project, not React Native. Here is how to open it in an IDE, add your own Firebase, and run the bare source cleanly.
Circadian Rhythm Light Exposure Tracker UI, Free
Build a circadian rhythm and light exposure tracker for iOS from a free template. Log light, chart your day, and get timing nudges with Claude Code or Cursor.
Blank White Screen on Launch in AI-Built Expo Apps? Fix
AI-generated Expo app launching to a blank white screen? It is a JS-load, render, or splash-timing issue. Here are the causes and the fixes.