Skip to main content

Chapter 11: Native Plugins — Camera & Haptics

Access the real camera and haptic engine on iOS through JavaScript.

This is where Capacitor earns its keep. In the browser, you get a basic file picker for photos. On native iOS, you get the full camera experience — viewfinder, photo library access, and permission prompts. We'll also add haptic feedback — the subtle vibrations that make iOS apps feel tactile and responsive.


What You'll Build

  • Camera integration — take photos directly or choose from the photo library
  • iOS permission handling with Info.plist configuration
  • A platform-aware photo picker (native camera on iOS, file input on web)
  • Haptic feedback on key interactions (creating entries, deleting, tab switching)
  • A useHaptics hook for reusable haptic triggers

What You'll Learn

  • How Capacitor plugins bridge JavaScript to native Swift code
  • iOS permission model and Info.plist usage strings
  • Platform detection (Capacitor.isNativePlatform())
  • The Haptics API and its feedback types
  • Graceful degradation — features that work on native but don't break on web

Step 1: Install the Plugins

npm install @capacitor/camera @capacitor/haptics
npx cap sync

cap sync is important here — it copies the new native plugin code into the Xcode project and runs pod install to link the Swift libraries.

Paste Terminal Errors into Claude Code

If you get any terminal errors during this step, paste the full error output directly into Claude Code. It can read error messages and fix them faster than you can search Stack Overflow.


Step 2: Configure iOS Permissions

iOS requires you to explain why your app needs camera and photo library access. These strings appear in the system permission dialog.

Open ios/App/App/Info.plist in Xcode (or a text editor) and add three permission keys inside the top-level <dict>:

  • NSCameraUsageDescription — Set this to a string like "GratitudeTree needs camera access to take photos for your journal entries."
  • NSPhotoLibraryUsageDescription — Set this to a string like "GratitudeTree needs photo library access to add photos to your journal entries."
  • NSPhotoLibraryAddUsageDescription — Set this to a string like "GratitudeTree needs to save photos you take to your photo library."

What each permission does

KeyWhen it triggers
NSCameraUsageDescriptionFirst time the app tries to open the camera
NSPhotoLibraryUsageDescriptionFirst time the app tries to read from the photo library
NSPhotoLibraryAddUsageDescriptionFirst time the app tries to save a photo to the library
Missing Permission Strings = App Crash

If you forget these strings and try to use the camera, iOS will crash your app immediately. Apple is strict about this — every permission must have a human-readable reason. The App Store will also reject apps with missing usage descriptions.

You can also ask Claude Code to handle this for you:

>_Claude Code
Try asking:
> Add the required iOS permissions for camera and photo library access to our Capacitor iOS project. We need permissions for the camera, reading photos, and saving photos.
Tip: If you get any terminal errors during this step, paste the full error output directly into Claude Code. It can read error messages and fix them faster than you can search Stack Overflow.
$ claude "Add the required iOS permissions for camera and photo library access to our Capa..."

Step 3: Build the Camera Service

Send this prompt to Claude Code to create the camera service:

>_Claude Code — Step 3
Send this to Claude Code:
> Create a camera service at src/lib/camera.ts that wraps Capacitor's Camera plugin. Export takePhoto() and pickFromLibrary() functions that return a data URL and blob. On native platforms, use the real camera and photo library. On web, fall back to a file input. Include helpers for converting between data URLs and blobs.
Why: This service abstracts platform differences — native iOS gets the real camera, web gets a file picker, and the rest of the app doesn't need to care.
What to expect: Claude Code will create src/lib/camera.ts with takePhoto, pickFromLibrary, and web fallback functions.
Tip: If you get any terminal errors during this step, paste the full error output directly into Claude Code. It can read error messages and fix them faster than you can search Stack Overflow.
$ claude "Create a camera service at src/lib/camera.ts that wraps Capacitor's Camera plugi..."

The platform detection pattern

The most important pattern in Capacitor development is checking Capacitor.isNativePlatform() before calling native APIs. Your app works on both web and iOS:

  • On iOS: Uses the real camera, shows the native photo picker
  • On web: Falls back to a standard file input dialog

This means your app never breaks — it just has fewer features on the web.

Camera options explained

OptionValueWhy
quality8080% JPEG quality — good balance of size and clarity
resultTypeDataUrlReturns a base64 data URL we can preview and upload
sourceCamera or PhotosCamera opens the viewfinder; Photos opens the library
width/height1200Max dimensions — Capacitor resizes automatically

Step 4: Build the Haptics Service

Haptic feedback makes interactions feel physical. Send this prompt to Claude Code to create the haptics service:

>_Claude Code — Step 4
Send this to Claude Code:
> Create a haptics service at src/lib/haptics.ts that wraps Capacitor's Haptics plugin. Export functions for light, medium, and heavy impacts, plus success, warning, and error notifications. Each function should silently do nothing on web — never throw errors.
Why: Haptics make iOS interactions feel physical and tactile. The native guard ensures these calls are silent on web.
What to expect: Claude Code will create src/lib/haptics.ts with six exported functions, each with a native platform guard.
Tip: If you get any terminal errors during this step, paste the full error output directly into Claude Code. It can read error messages and fix them faster than you can search Stack Overflow.
$ claude "Create a haptics service at src/lib/haptics.ts that wraps Capacitor's Haptics pl..."

Haptic types on iOS

TypeFeels likeUse for
Impact LightGentle tapTab switches, toggles
Impact MediumFirm pressSaving, confirming
Impact HeavyStrong thudShake interaction reveal
Notification SuccessDouble tap patternEntry saved, login success
Notification WarningTriple tap patternDelete confirmation
Notification ErrorBuzz patternError, failed action

The if (!isNative) return guard is critical — calling haptics on the web would throw an error. This way, all haptic calls are safe on any platform.


Step 5: Update the Create Page for Native Camera

Send this prompt to Claude Code to update the Create page with native camera support:

>_Claude Code — Step 5
Send this to Claude Code:
> Update the Create page to use our native camera service instead of the basic file input. Replace the photo picker with two buttons — one for taking a photo and one for choosing from the library. Store the photo blob for uploading. Also add an upload function to storage.ts that handles blobs directly since the camera plugin already compresses. Add haptic feedback before camera actions and after saving.
Why: Replacing the basic file input with native camera buttons that match how real iOS apps handle photo input.
What to expect: Claude Code will update Create.tsx with two camera action buttons, photoBlob state, and haptic feedback on interactions.
Tip: If you get any terminal errors during this step, paste the full error output directly into Claude Code. It can read error messages and fix them faster than you can search Stack Overflow.
$ claude "Update the Create page to use our native camera service instead of the basic fil..."

The two-button pattern (Take Photo + Library) matches how native iOS apps handle photo input. Claude Code will also add an uploadPhotoBlob function to storage.ts that uploads a pre-compressed blob, since the Capacitor camera plugin already compresses at 80% quality and 1200x1200.


Step 6: Add Haptics Throughout the App

Send this prompt to Claude Code to add haptic feedback across the app:

>_Claude Code — Step 6
Send this to Claude Code:
> Add haptic feedback to key interactions throughout the app — a light tap on tab switches, a warning vibration when showing delete confirmation, and a success feel after saving an entry.
Why: Strategic haptic placement on meaningful actions — not every tap, just the ones that benefit from physical feedback.
What to expect: Claude Code will update BottomNav.tsx, EntryCard.tsx, and Create.tsx with haptic calls at key interaction points.
Tip: If you get any terminal errors during this step, paste the full error output directly into Claude Code. It can read error messages and fix them faster than you can search Stack Overflow.
$ claude "Add haptic feedback to key interactions throughout the app — a light tap on tab ..."

The haptic philosophy

Don't overdo haptics. Apple's Human Interface Guidelines recommend:

  • DO: Use haptics for meaningful state changes (save, delete, navigate)
  • DON'T: Use haptics for every tap or scroll
  • DO: Match haptic intensity to action importance (light for tabs, heavy for shake reveal)
  • DON'T: Use haptics as a substitute for visual feedback
>_Claude Code
Try asking:
> I'm adding haptic feedback to my Capacitor iOS app. I have light taps on tab switches, success feedback on saving entries, and warning vibrations on delete confirmation. Is there anything else that should have haptics in a journal app?
Tip: If you get any terminal errors during this step, paste the full error output directly into Claude Code. It can read error messages and fix them faster than you can search Stack Overflow.
$ claude "I'm adding haptic feedback to my Capacitor iOS app. I have light taps on tab swi..."

Step 7: Test on the Simulator

npx cap sync

Then build and run from Xcode. Test:

  1. Camera button — on the Simulator, it opens the photo library (the Simulator has no real camera). On a physical device, it opens the actual camera viewfinder.
  2. Library button — opens the photo picker with sample photos
  3. Tab switching — you should feel a subtle haptic tap (physical device only — the Simulator doesn't simulate haptics)
  4. Save entry — success haptic after saving
  5. Delete confirmation — warning haptic when the delete prompt appears
Testing Haptics Requires a Physical Device

The iOS Simulator doesn't support haptic feedback. To feel the haptics, you'll need to run the app on a real iPhone. We'll cover deploying to a physical device in a later chapter.


Step 8: Commit Your Progress

git add .
git commit -m "Add native camera, photo library, and haptic feedback via Capacitor plugins"
git push

⚠️Common Issues

Checkpoint

Checkpoint — End of Chapter 11

Your app should now:

  • The camera plugin lets users take photos on native iOS
  • The photo library picker works for choosing existing photos
  • Permission prompts appear with clear usage descriptions
  • The app falls back to file input on the web (no crashes)
  • Haptic feedback fires on tab switches, entry creation, and delete
  • Haptics are silent on web — no errors, no broken behavior

What's Next

You have camera and haptics working. In the next chapter, we build the hero feature — shake the phone to discover a random past memory. It's the interaction that makes GratitudeTree feel magical.

𝕏

Follow @parvsondhi for build threads, tips, and updates on this tutorial.

Next: Chapter 12 — The Shake Interaction →