Skip to main content

Chapter 13: iOS-Specific Work

Make the app look and feel like it belongs on iOS. Handle the notch, the keyboard, and all the little details.

Your app runs on iOS, but it doesn't quite feel native yet. The status bar overlaps the header. The keyboard covers input fields. There's no app icon. This chapter fixes every one of those issues. These are the details that separate "an app that runs on iOS" from "an iOS app."


What You'll Do

  • Handle safe area insets (the notch, home indicator, status bar)
  • Fix keyboard behavior so inputs aren't covered
  • Set up the app icon for all required sizes
  • Configure the status bar style
  • Ensure touch targets meet Apple's minimum size guidelines
  • Test on the Simulator and a physical device

What You'll Learn

  • CSS env(safe-area-inset-*) variables
  • How the iOS keyboard interacts with web views
  • Apple's app icon requirements (1024×1024 source → many sizes)
  • Xcode's asset catalog system
  • The @capacitor/status-bar plugin
  • Apple's Human Interface Guidelines for touch targets

Step 1: The Safe Area Problem

Modern iPhones have a notch (or Dynamic Island) at the top and a home indicator bar at the bottom. If your app ignores these, content will be hidden:

Without safe areas:        With safe areas:
┌──────────────────┐ ┌──────────────────┐
│▓▓▓▓ NOTCH ▓▓▓▓▓│ │▓▓▓▓ NOTCH ▓▓▓▓▓│
│er (hidden!) │ │ │
│ │ │ Header │
│ Content │ │ │
│ │ │ Content │
│ │ │ │
│ Bottom Nav │ │ Bottom Nav │
│▓▓ HOME BAR ▓▓▓▓│ │ │
└──────────────────┘ │▓▓ HOME BAR ▓▓▓▓│
└──────────────────┘

The CSS solution

iOS exposes safe area dimensions through CSS environment variables:

  • env(safe-area-inset-top) — Space below the notch/Dynamic Island
  • env(safe-area-inset-bottom) — Space above the home indicator
  • env(safe-area-inset-left) — Space on the left (landscape)
  • env(safe-area-inset-right) — Space on the right (landscape)

These values are 0 on devices without a notch (or in the browser) and ~47px on notched iPhones. This means our safe area CSS works everywhere without platform detection.


Step 2: Apply Safe Area Insets

Send this prompt to Claude Code to apply safe area insets to your fixed elements:

>_Claude Code — Step 2
Send this to Claude Code:
> Update the fixed-position elements in our app to respect iOS safe area insets. The header needs padding for the notch, the bottom nav needs padding for the home indicator, and the main content area needs to account for both so nothing is hidden behind the fixed elements.
Why: Without safe area insets, content hides behind the notch and home indicator on modern iPhones.
What to expect: Claude Code will update Header.tsx, BottomNav.tsx, and App.tsx with safe area padding using CSS env() variables.
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 fixed-position elements in our app to respect iOS safe area insets. T..."

Why inline styles instead of Tailwind?

Tailwind doesn't support env() functions in its utility classes. The style prop is the correct way to use CSS environment variables in React. These values are computed by the browser at render time — they're not static.


Step 3: Configure the Status Bar

Install the Status Bar plugin:

npm install @capacitor/status-bar
npx cap sync

Send this prompt to Claude Code to configure the status bar:

>_Claude Code — Step 3
Send this to Claude Code:
> Configure the iOS status bar in main.tsx. We have a light amber theme, so set the status bar to show dark text. Only apply this on native platforms.
Why: The status bar sits at the very top of the iOS screen. We need dark text on our light amber background.
What to expect: Claude Code will update main.tsx with StatusBar plugin imports and a native platform check that configures the style.
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 "Configure the iOS status bar in main.tsx. We have a light amber theme, so set th..."
Style.Light vs Style.Dark

Style.Light means dark text — it refers to the background being light, not the text color. This trips up many developers.

Status bar options

StyleEffect
Style.LightDark text on light background (for light app themes)
Style.DarkWhite text on dark background (for dark app themes)

Our app uses a light amber background, so dark status bar text (Style.Light) is correct.


Step 4: Fix Keyboard Behavior

When a text input is focused on iOS, the keyboard slides up. In a web view, this can push the entire page or cover the input. Send this prompt to Claude Code to fix keyboard behavior:

>_Claude Code — Step 4
Send this to Claude Code:
> Fix iOS keyboard behavior in our Capacitor app. Update the Capacitor config so the web view resizes properly when the keyboard appears. Also add CSS rules so focused inputs scroll into view and inputs don't trigger iOS auto-zoom.
Why: Keyboard handling is one of the trickiest parts of iOS web views — these changes prevent inputs from being hidden.
What to expect: Claude Code will update capacitor.config.ts with iOS keyboard config and add CSS rules to index.css for scroll margins and font sizes.
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 "Fix iOS keyboard behavior in our Capacitor app. Update the Capacitor config so t..."
The 16px Font Size Rule

iOS Safari automatically zooms the page when an input with a font size smaller than 16px is focused. This is meant to help readability but creates a terrible UX — the page zooms in and doesn't zoom back. Setting font-size: 16px on all inputs prevents this.


Step 5: Touch Target Sizes

Apple's Human Interface Guidelines require minimum touch targets of 44×44 points. Some of our buttons (especially icon-only ones) might be too small. Send this prompt to Claude Code to fix touch targets:

>_Claude Code — Step 5
Send this to Claude Code:
> Audit and fix touch target sizes across our app to meet Apple's minimum guidelines. Add a utility CSS class for touch targets and review icon-only buttons to make sure they're large enough to tap comfortably.
Why: Apple's Human Interface Guidelines require 44x44pt minimum touch targets. Failing this can lead to App Store rejection.
What to expect: Claude Code will add a global CSS rule for minimum touch target sizes and review icon buttons for compliance.
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 "Audit and fix touch target sizes across our app to meet Apple's minimum guidelin..."

Check these components for touch target compliance:

  • EntryCard — edit and delete buttons
  • BottomNav — tab buttons (already good with py-2 + icon + label)
  • Header — shake button
  • ShakeReveal — dismiss button
Don't Apply 44px Minimums Globally

Avoid setting min-height: 44px and min-width: 44px on all button and a elements globally — this can break layouts with inline links, compact toggles, or small icon buttons. Instead, apply the .touch-target class selectively to interactive elements that need it, or use adequate padding (e.g., p-2.5 with a w-6 h-6 icon = 44px total).


Step 6: Set Up the App Icon

Apple requires a 1024×1024 pixel source image for your app icon. From this, Xcode generates all required sizes.

Create your icon

You can use any image editor, or ask Claude Code for help:

>_Claude Code
Try asking:
> I need a 1024x1024 app icon for a journal app called GratitudeTree. The app uses an amber/warm color palette. Can you suggest a simple icon design that would work well at small sizes (like on the iPhone home screen)?
Tip: App icons need to be recognizable at 60×60 pixels. Simple shapes and bold colors work best. Avoid text — it's unreadable at small sizes.
$ claude "I need a 1024x1024 app icon for a journal app called GratitudeTree. The app uses..."

Add the icon to Xcode

  1. Open the Xcode project: npx cap open ios
  2. In the left sidebar, navigate to App → Assets.xcassets → AppIcon
  3. Drag your 1024×1024 image into the App Store slot (1024pt)
  4. Xcode will automatically generate all required sizes from this source

If you're using Xcode 15+, you only need the single 1024×1024 image — Xcode generates the rest automatically.

Verify the icon

After adding the icon:

  1. Build and run in the Simulator (Cmd + R)
  2. Press Cmd + Shift + H to go to the home screen
  3. You should see your custom app icon with "GratitudeTree" underneath

Step 7: Deploy to a Physical Device

Testing on a real device is the only way to verify:

  • Safe area insets with the actual notch
  • Haptic feedback
  • Camera access
  • Shake detection
  • Performance on real hardware

Requirements

  1. An iPhone connected via USB (or on the same Wi-Fi for wireless debugging)
  2. A free Apple Developer account (or paid for App Store submission)
  3. Xcode's automatic signing set up

Steps

  1. Connect your iPhone to your Mac via USB
  2. In Xcode, select your iPhone from the device dropdown (instead of a Simulator)
  3. Go to Signing & Capabilities → select your Apple ID team
  4. Click Play (Cmd + R)
  5. On your iPhone, go to Settings → General → VPN & Device Management → trust your developer certificate
  6. The app installs and launches on your phone

Using the dev server on a physical device

Update capacitor.config.ts to use your Mac's local IP instead of localhost. Find your Mac's IP: System Settings → Network → Wi-Fi → Details → IP Address

Your phone and Mac must be on the same Wi-Fi network.

Paste Errors into Claude Code

If you run into issues deploying to a physical device, paste the full Xcode error output directly into Claude Code. It can diagnose signing issues, provisioning profile problems, and device trust errors quickly.


Step 8: Final Polish Checklist

Before moving to Part 6 (TestFlight), walk through this checklist:

CheckStatus
Header text doesn't overlap the notch/Dynamic Island
Bottom nav doesn't overlap the home indicator
Status bar text is readable (dark text on light bg)
Keyboard doesn't cover text inputs
Input fields don't trigger page zoom (font-size ≥ 16px)
All buttons are at least 44×44 touch targets
App icon appears on the home screen
Haptics work on the physical device
Camera opens the actual viewfinder (not just file picker)
Shake detection works with a real shake

Step 9: Production Build

Before moving to TestFlight, let's make a production build. Send this prompt to Claude Code to prepare the production config:

>_Claude Code — Step 9
Send this to Claude Code:
> Update capacitor.config.ts for production by removing the development server block. The app should load from the bundled files instead of connecting to localhost.
Why: This is the critical switch from development to production — the app must load from bundled files for TestFlight.
What to expect: Claude Code will update capacitor.config.ts to remove the server block while keeping all other configuration.
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 capacitor.config.ts for production by removing the development server blo..."

Then build and sync:

npm run build
npx cap sync

Now the iOS app uses the bundled files from dist/ instead of connecting to a dev server. This is what you'll submit to TestFlight.

Don't Forget to Remove the Server Block

Forgetting to remove the server block is the most common TestFlight mistake — the app tries to connect to localhost and shows a blank screen for your testers.


Step 10: Commit Your Progress

git add .
git commit -m "Add iOS polish: safe areas, status bar, keyboard handling, app icon, touch targets"
git push

⚠️Common Issues

Checkpoint

Checkpoint — End of Chapter 13

Your app should now:

  • Safe area insets are applied — content doesn't overlap the notch or home indicator
  • Status bar has the correct style (dark text on light background)
  • Keyboard doesn't cover input fields when focused
  • All interactive elements meet the 44×44 minimum touch target
  • The app icon appears on the home screen
  • The app runs correctly on a physical iOS device
  • A production build works without the dev server

Part 5 Complete!

Your web app is now a fully functional iOS app. Let's recap what Part 5 added:

FeatureChapter
Capacitor setup and Xcode projectChapter 10
Native camera and haptic feedbackChapter 11
Shake-to-discover with motion detectionChapter 12
Safe areas, icons, keyboard, and polishChapter 13

In Part 6, we'll take this app to the finish line — setting up an Apple Developer account, configuring App Store Connect, and shipping to TestFlight so other people can install and test your app on their iPhones.

𝕏

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

Next: Chapter 14 — Apple Developer Account →