Chapter 10: Capacitor Setup
This is the chapter you've been building toward. Your web app is about to become a real iOS app.
Everything you built in Part 4 was a web app running in the browser. That changes now. In this chapter, we install Capacitor --- the bridge that wraps your web app in a native iOS shell --- and run it in the iOS Simulator. Same code you already wrote. Real native app.
What You'll Do
- Install Capacitor and the iOS platform
- Configure your project for iOS builds
- Learn the build, sync, run workflow
- Open your project in Xcode for the first time
- Run the app in the iOS Simulator
What You'll Learn
- How Capacitor works under the hood (it's a WKWebView, not a compiler)
- The relationship between your web build and the native project
- The Capacitor sync workflow
- Xcode project structure basics
Before You Start: Install Xcode
This is the chapter where you need the full Xcode app (not just Command Line Tools). If you haven't installed it yet, open the Mac App Store, search for Xcode, and install it. It's roughly 8 GB, so start the download now if you haven't already.
After the install finishes, open Xcode once. It will ask you to accept the license agreement and install additional components --- say yes to both. This one-time setup takes a few minutes. You cannot proceed with this chapter until Xcode is fully set up.
Once Xcode is ready, verify it from your terminal:
xcode-select -p
You should see something like /Applications/Xcode.app/Contents/Developer. If you see a path pointing to CommandLineTools instead, run:
sudo xcode-select -s /Applications/Xcode.app
Step 1: How Capacitor Works
Before installing anything, let's understand what Capacitor actually does. This is important because it affects how you think about the entire iOS workflow.
Your React App (HTML/CSS/JS)
|
v
+--------------------+
| Capacitor |
| |
| +---------------+ |
| | WKWebView | | <-- Safari's rendering engine
| | | |
| | Your app | |
| | runs here | |
| +---------------+ |
| |
| Native Bridge | <-- Connects JS to native APIs
| (Camera, etc.) |
| |
+--------------------+
|
v
iOS App (.ipa)
Capacitor is not a compiler. It does not convert your React code into Swift or Objective-C. Here's what actually happens:
- Your app's built files (the
dist/folder fromnpm run build) are copied into an iOS project - The iOS app loads those files inside a WKWebView --- essentially an embedded Safari browser without the address bar
- Capacitor provides a JavaScript bridge to native device APIs (camera, haptics, filesystem, and more)
- The result is a genuine iOS app that you can submit to the App Store
This is the same approach used by many production apps. Your web skills transfer directly --- no Swift required.
Why not just use a PWA? Progressive Web Apps can't reliably access the camera, can't trigger haptic feedback, can't appear in the App Store, and can't use TestFlight for beta testing. Capacitor gives you all of that without changing a single line of your existing web code.
Step 2: Send the Prompt to Claude Code
Let Claude Code handle the installation and setup:
$ claude "I want to bundle this app for iOS using capacitor. Please help with this."What Claude Code will do
When you send that prompt, Claude Code will run several commands. Here's what to expect:
Install the packages:
npm install @capacitor/core @capacitor/cli
npm install @capacitor/ios
Initialize Capacitor --- this creates the config file:
npx cap init
During initialization, you'll be asked for:
- App name:
GratitudeTree(or whatever you named your app) - App Package ID: something like
com.yourname.gratitudetree--- this is the unique identifier for your app on the App Store, using reverse domain notation
Add the iOS platform --- this creates the Xcode project:
npx cap add ios
This command creates an entire ios/ folder in your project containing a full Xcode workspace. You won't need to manually edit most of these files --- Capacitor manages them for you.
Throughout this chapter, if any terminal command fails or produces unexpected output, copy the full error message and paste it to Claude Code. It's particularly good at diagnosing Capacitor setup issues, CocoaPods errors, and Xcode configuration problems. This applies to every step from here on.
Step 3: Understand What Capacitor Created
After Claude Code finishes, take a look at what changed in your project.
The config file
Claude Code created a capacitor.config.ts file in your project root. This file tells Capacitor everything it needs to know about your app. The key settings are:
| Setting | What it does |
|---|---|
appId | Your app's unique identifier on the App Store (reverse domain notation, e.g., com.yourname.gratitudetree) |
appName | The name displayed under the app icon on the home screen |
webDir | The folder containing your built web files --- this should be dist since Vite outputs there |
server.url | (Optional) During development, points to Vite's dev server for live reload |
server.cleartext | (Optional) Allows HTTP connections --- needed for local development since localhost isn't HTTPS |
The server block is for development only. It tells the iOS app to load your UI from http://localhost:5173 instead of from the bundled files. This means changes you make in your React code will appear instantly in the Simulator without rebuilding. You'll remove this block later when building for TestFlight.
The server block with the url property is a development convenience. When you build for TestFlight or the App Store, you must remove it so the app uses the bundled dist/ files. We'll handle that in Part 6.
The ios/ folder
The ios/ directory is a complete Xcode project. Here's what's inside:
ios/
└── App/
├── App/
│ ├── AppDelegate.swift <-- Native app entry point
│ ├── Info.plist <-- iOS app configuration
│ ├── Assets.xcassets <-- App icon and images
│ └── public/ <-- Your built web files land here
├── Podfile <-- CocoaPods dependencies
└── App.xcworkspace <-- Open this in Xcode
You don't need to understand all of these files. The important thing to know is that Capacitor generated and manages this project for you.
Step 4: The Build-Sync-Run Workflow
Capacitor has a three-step workflow. You'll use this every time you want to test changes in the Simulator:
npm run build
npx cap sync
npx cap open ios
Why three steps?
- Build --- Vite compiles your TypeScript and React into plain HTML, CSS, and JavaScript in the
dist/folder - Sync --- Capacitor copies that
dist/folder into the iOS project and installs or updates any native plugins (via CocoaPods) - Open --- Launches Xcode with your project so you can run it in the Simulator
The dev server shortcut
During development, you can skip the build step because the server.url setting in your Capacitor config points to Vite's dev server. This means the Simulator loads your app directly from localhost:5173. The workflow becomes:
- Start Vite in one terminal:
npm run dev - Sync and open in another terminal:
npx cap syncthennpx cap open ios - Run in the Simulator from Xcode
- Edit your React code --- changes hot-reload in the Simulator automatically
This is the workflow you'll use most during development. The full build-sync-run workflow is for production builds and when you need to test the bundled app.
If npx cap sync fails with pod install errors, or if npx cap open ios can't find Xcode, paste the full error into Claude Code. These are common first-time setup issues that Claude Code knows how to resolve.
Step 5: Run It in the Simulator
This is the big moment.
Everything you've built across Parts 1 through 4 --- the idea, the scaffolding, the UI shell, the entries, authentication, polish, streaks --- all of it is about to come to life as a real iOS app.
Start the dev server
In your first terminal:
npm run dev
Sync and open Xcode
In a second terminal:
npx cap sync
npx cap open ios
Xcode will open with your project loaded.
Run the app
- In the Xcode toolbar at the top, find the device selector dropdown. Click it and choose a Simulator --- for example, iPhone 15 or iPhone 16
- Click the Play button (the triangle icon in the top-left, or press
Cmd + R) - Wait for the Simulator to boot up and the app to install. The first build takes longer because Xcode needs to compile native dependencies and set up the Simulator. Subsequent builds are much faster.
- Your GratitudeTree app appears --- running inside an iOS app, not a browser
Your app is running on iOS.
Take a second to appreciate what just happened. You started this tutorial with an idea. You built a complete web application with a UI shell, journal entries, authentication, streaks, and polish. And now that same application is running as a native iOS app in the Simulator.
There's no browser address bar. No navigation controls. It looks and feels like every other app on your phone. Because it is one.
Take a screenshot of the Simulator. You earned this moment.
What to verify in the Simulator
- The app loads and shows your login screen (or the main app if you have an active session)
- Tab navigation works --- tap Feed, New, and Profile
- Creating and viewing entries works (as long as your Vite dev server is running)
- The app fills the screen naturally --- no browser chrome, no address bar
- The status bar (time, battery, signal) appears above your app like a real iOS app
The first Xcode build downloads and compiles CocoaPods dependencies, which can take a minute or two. Don't worry if it seems slow. Subsequent builds reuse the cached artifacts and finish much faster.
If you see an infinite loading spinner
If your app loads in the Simulator but gets stuck on an infinite loading spinner, this is a known compatibility issue between Firebase's persistentMultipleTabManager and Capacitor's WKWebView. The multi-tab manager depends on BroadcastChannel and SharedWorker, which WKWebView does not support --- so Firebase silently fails to initialize.
Send this prompt to Claude Code to fix it:
$ claude "My app uses Firebase Firestore with persistentLocalCache. I'm about to build it ..."If sign-in hangs (button spins forever)
If tapping "Sign In" causes the button to spin forever --- the network request succeeds (you can verify in Safari Web Inspector) but the signInWithEmailAndPassword promise never resolves or rejects --- this is a different WKWebView compatibility issue.
Firebase Auth's default initialization (getAuth) auto-detects which storage mechanism to use for session persistence. In a normal browser this is instant. In Capacitor's WKWebView, this detection can deadlock: Firebase receives the auth token but gets stuck trying to persist it, and the promise hangs silently. No error, no timeout, just nothing.
The fix is to replace getAuth with initializeAuth and explicitly tell Firebase to use IndexedDB. Send this prompt to Claude Code:
$ claude "In our Firebase config file, replace getAuth(app) with initializeAuth(app, { per..."Step 6: Understand the Xcode Project
You don't need to become an Xcode expert. But knowing the basics will save you time when configuring native features in the next chapters.
Files you'll touch during this tutorial
| File | When you'll need it |
|---|---|
ios/App/App/Info.plist | Adding camera and photo library permission descriptions |
ios/App/App/Assets.xcassets | Setting the app icon |
capacitor.config.ts | Changing Capacitor configuration (lives in your project root, not in ios/) |
Files Capacitor manages (don't edit manually)
| File | What it is |
|---|---|
AppDelegate.swift | The iOS app lifecycle entry point |
Podfile | CocoaPods dependency list (updated by cap sync) |
ios/App/App/public/ | Your built web files (overwritten every sync) |
The golden rule
Never manually edit files inside ios/App/App/public/. That folder gets completely overwritten every time you run npx cap sync. All your code changes happen in your React project. Capacitor copies them into the iOS project during sync.
Xcode Signing (quick setup)
When you first open the project, Xcode may show a signing error. To fix it:
- Click on App in the Xcode project navigator (the left sidebar)
- Select the Signing & Capabilities tab
- Check Automatically manage signing
- Select your Team from the dropdown --- if you don't have an Apple Developer account, select your personal team (your Apple ID works for Simulator testing)
This is only needed for running on physical devices and for TestFlight. The Simulator usually works without signing configuration, but it's good to set it up now.
Step 7: Add Convenience Scripts
Ask Claude Code to add shortcuts to your package.json:
$ claude "Add convenience scripts to package.json for our Capacitor iOS workflow. I want a..."After Claude Code adds the scripts, your development workflow becomes:
npm run dev
In a second terminal:
npm run ios:dev
That single command syncs the project and opens Xcode. From there, press Cmd + R to run in the Simulator. Two terminals, two commands, and you're developing an iOS app.
Step 8: Commit Your Progress
Your project now has a complete iOS setup. Save it:
git add -A
git commit -m "Add Capacitor: iOS project setup and dev workflow"
git push
The ios/ folder should be committed --- it contains your Xcode project configuration, Info.plist, and other settings you'll modify later. However, the ios/App/Pods/ directory should be in .gitignore (Capacitor sets this up by default). If Claude Code created or updated your .gitignore, verify that Pods/ is listed.
gratitude-tree/
├── ios/ <-- NEW (entire Xcode project)
│ └── App/
│ ├── App/
│ │ ├── AppDelegate.swift
│ │ ├── Info.plist
│ │ ├── Assets.xcassets
│ │ └── public/ <-- Built web files (auto-generated)
│ ├── Podfile
│ └── App.xcworkspace
├── src/
│ └── ... <-- Unchanged
├── capacitor.config.ts <-- NEW
├── package.json <-- UPDATED (new scripts)
└── ...
Checkpoint
Your app should now:
- Capacitor is installed and initialized with your app ID
- The
ios/folder contains a full Xcode project - Your Capacitor config is set up with the dev server URL for live reload
- Running
npx cap synccopies web files to the iOS project - The app runs in the iOS Simulator and looks like a native app
- Hot reload works --- React code changes appear in the Simulator
- You understand the build, sync, run workflow
- Convenience scripts are added to
package.json
What's Next
Your app runs on iOS --- but it's still using web APIs for everything. In the next chapter, we'll add Capacitor plugins for the camera and haptic feedback. These are your first real native features --- things that a browser simply can't do as well. The app will start to feel like it belongs on a phone.
Follow @parvsondhi for build threads, tips, and updates on this tutorial.