Capacitor:WebアプリをiOS/Androidネイティブアプリに変換
Capacitor:WebアプリをiOS/Androidネイティブアプリに変換
モバイルアプリを開発する際、Swift/Kotlin/React Nativeを学ぶのは時間がかかります。Capacitorを使えば、既存のWeb技術(React、Vue、Angularなど)で作ったアプリを、そのままiOSとAndroidのネイティブアプリに変換できます。
この記事では、Capacitorの基本からアプリストア公開まで、実践的に解説します。
Capacitorとは
Capacitorは、Ionic Teamが開発したクロスプラットフォームのネイティブランタイムです。Webアプリケーションをラップし、ネイティブアプリとして動作させます。
主な特徴
- Web標準ベース: 既存のWeb技術をそのまま活用
- ネイティブAPI: カメラ、位置情報、プッシュ通知などにアクセス
- フレームワーク非依存: React、Vue、Svelte、Angularなど何でも対応
- 完全なネイティブアクセス: Xcode/Android Studioで直接編集可能
- プラグインエコシステム: 公式・コミュニティプラグイン多数
- PWA対応: 同じコードベースでWebアプリとしても動作
- ライブリロード: ネイティブアプリ開発中もホットリロード可能
競合との比較
Capacitor vs React Native
- React Nativeは完全なネイティブUI
- Capacitorは既存のWebアプリをそのまま利用可能
- React Nativeはパフォーマンスがやや有利
- Capacitorは学習コストが低い
Capacitor vs Cordova
- 両方ともIonic Teamが開発
- Capacitorはより現代的なアーキテクチャ
- Capacitorはネイティブプロジェクトへの直接アクセスが容易
- Capacitorはプラグイン管理がシンプル
Capacitor vs Electron(デスクトップ向け)
- Capacitorはモバイル専用
- Electronはデスクトップアプリ向け
- 技術スタックは似ている
セットアップ
前提条件
開発環境:
- Node.js 16以降
- npm/pnpm/yarn
- iOS開発: macOS + Xcode
- Android開発: Android Studio
既存のWebアプリに追加
React、Vue、その他のWebアプリケーションにCapacitorを追加します。
# Capacitorのインストール
npm install @capacitor/core @capacitor/cli
# Capacitorの初期化
npx cap init
対話式のセットアップ:
? App name: My Awesome App
? App Package ID (in Java package format): com.example.myapp
? (Optional) Web asset directory: dist (or build, public, etc.)
Package IDは、逆ドメイン形式で一意にする必要があります。
プラットフォームの追加
# iOSとAndroidを追加
npm install @capacitor/ios @capacitor/android
npx cap add ios
npx cap add android
これにより、ios/とandroid/ディレクトリが作成されます。
プロジェクト構造
my-app/
├── src/ # Webアプリのソース
├── dist/ # ビルド済みWebアプリ
├── ios/ # iOSネイティブプロジェクト
├── android/ # Androidネイティブプロジェクト
├── capacitor.config.ts
└── package.json
Capacitor設定
// capacitor.config.ts
import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.example.myapp',
appName: 'My Awesome App',
webDir: 'dist',
server: {
androidScheme: 'https',
},
plugins: {
SplashScreen: {
launchShowDuration: 2000,
backgroundColor: '#ffffff',
showSpinner: false,
},
},
};
export default config;
開発ワークフロー
1. Webアプリのビルド
まず、通常通りWebアプリをビルドします。
# React
npm run build
# Next.js(静的エクスポート)
npm run build && npm run export
# Vite
npm run build
2. ネイティブプロジェクトに同期
ビルド済みのWebアプリをネイティブプロジェクトにコピーします。
npx cap sync
これは以下を実行します:
dist/の内容を各プラットフォームにコピー- ネイティブ依存関係を更新
- Capacitorプラグインを同期
3. ネイティブアプリの実行
# iOS(macOSのみ)
npx cap open ios
# Android
npx cap open android
Xcode/Android Studioが開くので、エミュレーターや実機で実行できます。
ライブリロード開発
開発中は、ライブリロードを使うと効率的です。
# 開発サーバーを起動
npm run dev
// capacitor.config.ts(開発時のみ)
import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.example.myapp',
appName: 'My Awesome App',
webDir: 'dist',
server: {
url: 'http://192.168.1.100:5173', // 開発サーバーのURL
cleartext: true,
},
};
export default config;
これで、Webアプリを編集すると、ネイティブアプリも即座に更新されます。
コアプラグイン
Capacitorには、よく使われる機能のための公式プラグインがあります。
App Plugin
アプリのライフサイクルとステート管理。
import { App } from '@capacitor/app';
// アプリがフォアグラウンドに戻ったとき
App.addListener('appStateChange', ({ isActive }) => {
console.log('App state changed. Is active?', isActive);
});
// バックボタン(Android)
App.addListener('backButton', ({ canGoBack }) => {
if (!canGoBack) {
App.exitApp();
} else {
window.history.back();
}
});
// アプリ情報の取得
const info = await App.getInfo();
console.log('App version:', info.version);
Camera Plugin
カメラとフォトギャラリーへのアクセス。
npm install @capacitor/camera
npx cap sync
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
export async function takePicture() {
try {
const image = await Camera.getPhoto({
quality: 90,
allowEditing: false,
resultType: CameraResultType.Uri,
source: CameraSource.Camera,
});
// image.webPathはファイルパス
return image.webPath;
} catch (error) {
console.error('Camera error:', error);
}
}
export async function pickFromGallery() {
const image = await Camera.getPhoto({
quality: 90,
allowEditing: true,
resultType: CameraResultType.DataUrl,
source: CameraSource.Photos,
});
// image.dataUrlはbase64エンコードされた画像
return image.dataUrl;
}
Reactコンポーネント例:
import { useState } from 'react';
import { Camera, CameraResultType } from '@capacitor/camera';
export function CameraComponent() {
const [photo, setPhoto] = useState<string>();
const takePhoto = async () => {
const image = await Camera.getPhoto({
quality: 90,
resultType: CameraResultType.Uri,
});
setPhoto(image.webPath);
};
return (
<div>
<button onClick={takePhoto}>Take Photo</button>
{photo && <img src={photo} alt="Captured" />}
</div>
);
}
Geolocation Plugin
位置情報の取得。
npm install @capacitor/geolocation
npx cap sync
import { Geolocation } from '@capacitor/geolocation';
export async function getCurrentPosition() {
const coordinates = await Geolocation.getCurrentPosition();
console.log('Current position:', coordinates);
// {
// coords: {
// latitude: 35.6762,
// longitude: 139.6503,
// accuracy: 10,
// altitude: null,
// ...
// }
// }
return coordinates.coords;
}
export function watchPosition(callback: (position: any) => void) {
const id = Geolocation.watchPosition({}, (position, err) => {
if (position) {
callback(position);
}
});
// クリーンアップ関数を返す
return () => Geolocation.clearWatch({ id });
}
Reactフック:
import { useState, useEffect } from 'react';
import { Geolocation } from '@capacitor/geolocation';
export function useGeolocation() {
const [position, setPosition] = useState<any>(null);
const [error, setError] = useState<string>();
useEffect(() => {
let watchId: string;
const startWatch = async () => {
try {
watchId = await Geolocation.watchPosition({}, (position, err) => {
if (err) {
setError(err.message);
} else if (position) {
setPosition(position.coords);
}
});
} catch (err: any) {
setError(err.message);
}
};
startWatch();
return () => {
if (watchId) {
Geolocation.clearWatch({ id: watchId });
}
};
}, []);
return { position, error };
}
Push Notifications
プッシュ通知の実装。
npm install @capacitor/push-notifications
npx cap sync
import {
PushNotifications,
Token,
PushNotificationSchema,
ActionPerformed,
} from '@capacitor/push-notifications';
export async function initializePushNotifications() {
// パーミッションリクエスト
let permStatus = await PushNotifications.checkPermissions();
if (permStatus.receive === 'prompt') {
permStatus = await PushNotifications.requestPermissions();
}
if (permStatus.receive !== 'granted') {
throw new Error('Push notification permission denied');
}
// 登録
await PushNotifications.register();
// リスナー設定
PushNotifications.addListener('registration', (token: Token) => {
console.log('Push registration success, token:', token.value);
// トークンをバックエンドに送信
sendTokenToServer(token.value);
});
PushNotifications.addListener('registrationError', (error: any) => {
console.error('Push registration error:', error);
});
PushNotifications.addListener(
'pushNotificationReceived',
(notification: PushNotificationSchema) => {
console.log('Push notification received:', notification);
}
);
PushNotifications.addListener(
'pushNotificationActionPerformed',
(notification: ActionPerformed) => {
console.log('Push notification action performed', notification);
// 通知タップ時の処理
}
);
}
async function sendTokenToServer(token: string) {
await fetch('https://api.example.com/push-tokens', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token }),
});
}
Local Notifications
ローカル通知(デバイス内で生成)。
npm install @capacitor/local-notifications
npx cap sync
import { LocalNotifications } from '@capacitor/local-notifications';
export async function scheduleNotification() {
await LocalNotifications.requestPermissions();
await LocalNotifications.schedule({
notifications: [
{
title: 'Reminder',
body: 'Don\'t forget to check your tasks!',
id: 1,
schedule: {
at: new Date(Date.now() + 1000 * 60 * 60), // 1時間後
},
sound: 'beep.wav',
attachments: undefined,
actionTypeId: '',
extra: null,
},
],
});
}
export async function scheduleRepeatingNotification() {
await LocalNotifications.schedule({
notifications: [
{
title: 'Daily Reminder',
body: 'Time for your daily task!',
id: 2,
schedule: {
on: {
hour: 9,
minute: 0,
},
every: 'day',
},
},
],
});
}
Storage Plugin
永続的なキーバリューストレージ。
npm install @capacitor/preferences
npx cap sync
import { Preferences } from '@capacitor/preferences';
// データの保存
export async function saveData(key: string, value: any) {
await Preferences.set({
key,
value: JSON.stringify(value),
});
}
// データの取得
export async function getData(key: string) {
const { value } = await Preferences.get({ key });
return value ? JSON.parse(value) : null;
}
// データの削除
export async function removeData(key: string) {
await Preferences.remove({ key });
}
// すべてのデータをクリア
export async function clearAll() {
await Preferences.clear();
}
Reactフック:
import { useState, useEffect } from 'react';
import { Preferences } from '@capacitor/preferences';
export function useStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(initialValue);
useEffect(() => {
Preferences.get({ key }).then(({ value: stored }) => {
if (stored) {
setValue(JSON.parse(stored));
}
});
}, [key]);
const updateValue = async (newValue: T) => {
setValue(newValue);
await Preferences.set({
key,
value: JSON.stringify(newValue),
});
};
return [value, updateValue] as const;
}
// 使用例
function MyComponent() {
const [username, setUsername] = useStorage('username', '');
return (
<input
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
);
}
カスタムプラグインの作成
独自のネイティブ機能を追加できます。
npm install @capacitor/cli
npx cap plugin:generate
? Plugin npm name: my-custom-plugin
? Plugin id: com.example.mycustomplugin
? Plugin class name: MyCustomPlugin
? Description: My awesome custom plugin
? Git repository:
? Author:
? License: MIT
これで、プラグインのテンプレートが生成されます。
App Store / Play Store公開
iOS(App Store)
1. Xcodeでプロジェクトを開く
npx cap open ios
2. 署名設定
Xcode → Signing & Capabilities → Team(Apple Developer Account)
3. アプリアイコンとスプラッシュスクリーン
ios/App/App/Assets.xcassets/AppIcon.appiconset/
1024x1024pxのアイコンを配置します。
4. ビルド番号とバージョン
Xcode → General → Version & Build
5. アーカイブとアップロード
Xcode → Product → Archive → Distribute App
App Store Connectにアップロードされます。
6. App Store Connectで申請
- スクリーンショット追加
- アプリ説明記入
- プライバシーポリシー設定
- 審査提出
Android(Play Store)
1. Android Studioでプロジェクトを開く
npx cap open android
2. アプリアイコン
android/app/src/main/res/mipmap-*/
各解像度のアイコンを配置します。
3. 署名鍵の作成
keytool -genkey -v -keystore my-release-key.keystore -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000
4. 署名設定
android/app/build.gradle
android {
...
signingConfigs {
release {
storeFile file("my-release-key.keystore")
storePassword "password"
keyAlias "my-key-alias"
keyPassword "password"
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
5. AABのビルド
Build → Generate Signed Bundle / APK → Android App Bundle
6. Play Consoleにアップロード
- Google Play Consoleでアプリ作成
- AABファイルをアップロード
- ストア掲載情報入力
- 審査提出
ベストプラクティス
1. プラットフォーム検出
import { Capacitor } from '@capacitor/core';
const isNative = Capacitor.isNativePlatform();
const platform = Capacitor.getPlatform(); // 'ios', 'android', 'web'
if (platform === 'ios') {
// iOS固有の処理
} else if (platform === 'android') {
// Android固有の処理
}
2. エラーハンドリング
import { Camera } from '@capacitor/camera';
try {
const photo = await Camera.getPhoto({...});
} catch (error: any) {
if (error.message.includes('cancelled')) {
// ユーザーがキャンセル
} else if (error.message.includes('permission')) {
// パーミッション拒否
alert('Camera permission is required');
}
}
3. パーミッション管理
import { Camera } from '@capacitor/camera';
const checkPermissions = async () => {
const status = await Camera.checkPermissions();
if (status.camera !== 'granted') {
const newStatus = await Camera.requestPermissions();
return newStatus.camera === 'granted';
}
return true;
};
まとめ
Capacitorは、Web技術でネイティブアプリを構築できる強力なツールです。主な利点は以下の通りです。
- 既存のスキル活用: Web開発スキルをそのまま利用
- コード共有: Web、iOS、Androidで同じコードベース
- ネイティブアクセス: フルネイティブAPIへのアクセス
- 柔軟性: 必要に応じてネイティブコードを追加可能
React、Vue、その他のWebフレームワークでアプリを構築し、Capacitorでネイティブアプリとして公開できます。学習コストが低く、開発効率が高いため、多くの企業やスタートアップで採用されています。