ionic-skills について
ionic-skillsは、Angular、React、VueでIonic Capacitorのモバイルアプリを開発するためのガイドです。ナビゲーション、必須ページ、プラグイン、ストレージの規約と、各フレームワーク向けの実装パターンを定めています。
主な機能
- 01Angular、React、Vueに対応
- 02オンボーディング、ペイウォール、設定ページを規定
- 03ion-tabsによるタブナビゲーション
- 04Capacitorプラグインとストレージの規約
- 05ルーティングと設定の例を掲載
活用シーン
- 01Ionic Capacitorアプリの開発開始
- 02フレームワーク別のアプリ構成設計
- 03ストレージとネイティブプラグインの実装
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ erkamyaman/ ionic-capacitor-skills --skill ionic-skillsインストール後、Claude Code で/ionic-skillsを入力して読み込まれたか確認します。
同梱ファイルmetadata.json
詳細
Ionic Capacitor アプリ開発ガイド
IMPORTANT: これはプロジェクトではなく、SKILL ファイルです。このフォルダーで npm install を絶対に実行しないでください。この場所にコードファイルを絶対に作成しないでください。新しいプロジェクトを作成する際は、必ず最初にユーザーにプロジェクトのパスを確認するか、別のディレクトリ(例:
~/Projects/app-name)に作成してください。
このガイドでは、Angular、React、または Vue を使用した Ionic Capacitor による本番環境対応のモバイルアプリの構築方法を説明します。
必須要件
新しい Ionic プロジェクトを作成する際は、以下のすべてを必ず含めてください。
必須ページ(必ず作成)
- オンボーディングページ - 全画面の背景動画とグラデーションオーバーレイを備えた、スワイプ式のオンボーディング
- ペイウォールページ - RevenueCat のペイウォールページ(オンボーディングの後に表示)
- 設定ページ - 言語、テーマ、通知、オンボーディングのリセットオプションを備えた設定ページ
必須ナビゲーション(必ず使用)
- タブナビゲーションには
ion-tabsとion-tab-barを使用してください。カスタムタブの実装やサードパーティ製タブライブラリは絶対に使用しないでください。
必須ライブラリ(必ずインストール)
Angular
npm install @capacitor/preferences @capacitor/push-notifications @capacitor/splash-screen @capacitor/status-bar @revenuecat/purchases-capacitor @capacitor-community/admob @ngx-translate/core @ngx-translate/http-loader swiper
React
npm install @capacitor/preferences @capacitor/push-notifications @capacitor/splash-screen @capacitor/status-bar @revenuecat/purchases-capacitor @capacitor-community/admob react-i18next i18next i18next-http-backend swiper
Vue
npm install @capacitor/preferences @capacitor/push-notifications @capacitor/splash-screen @capacitor/status-bar @revenuecat/purchases-capacitor @capacitor-community/admob vue-i18n swiper
共通ライブラリ(すべてのフレームワーク)
@revenuecat/purchases-capacitor(RevenueCat)@capacitor-community/admob(AdMob)@capacitor/push-notifications(プッシュ通知)@capacitor/preferences(キーと値のストレージ)@capacitor/splash-screen(スプラッシュ画面の制御)@capacitor/status-bar(ステータスバーのスタイル設定)swiper(オンボーディングのスライド)
フレームワーク別の i18n ライブラリ
| フレームワーク | ライブラリ | 用途 |
|---|---|---|
| Angular | @ngx-translate/core + @ngx-translate/http-loader | translate パイプ |
| React | react-i18next + i18next | useTranslation() フック |
| Vue | vue-i18n | useI18n() コンポーザブル / $t() |
禁止事項(絶対に使用しない)
すべてのフレームワーク
- ❌
localStorageを直接使用しないでください。代わりに@capacitor/preferencesを使用してください。 - ❌
@ionic/storageを使用しないでください。代わりに@capacitor/preferencesを使用してください。 - ❌ カスタムタブバーを使用しないでください。代わりに
ion-tabs+ion-tab-barを使用してください。 - ❌
cordova-plugin-*プラグインを使用しないでください。代わりに Capacitor プラグインを使用してください。 - ❌
any型を使用しないでください。必ず適切な TypeScript 型を使用してください。 - ❌
ngx-admob-freeやその他の非推奨の広告ライブラリを使用しないでください。@capacitor-community/admobのみを使用してください。 - ❌ Capacitor の同期呼び出しを使用しないでください。Capacitor プラグインのメソッドは必ず
awaitしてください。
Angular 固有
- ❌ 新しいページやコンポーネントに NgModules を使用しないでください。スタンドアロンコンポーネントを使用してください。
- ❌ スタンドアロンコンポーネントで
IonicModuleを使用しないでください。個々のコンポーネント(IonButton、IonContentなど)をインポートしてください。 - ❌
@Component内にtemplateやstylesをインラインで記述しないでください。templateUrlとstyleUrlsを使用して、別々の.html、.ts、.scssファイルを使用してください。 - ❌
@angular/http(非推奨)を使用しないでください。@angular/common/httpを使用してください。
React 固有
- ❌ クラスコンポーネントを使用しないでください。フックを使った関数コンポーネントを使用してください。
- ❌ DOM を直接操作しないでください。React の参照と状態を使用してください。
- ❌
@ionic/angularのインポートを使用しないでください。@ionic/reactを使用してください。
Vue 固有
- ❌ 新しいコードに Options API を使用しないでください。
<script setup>を使った Composition API を使用してください。 - ❌ DOM を直接操作しないでください。Vue の参照とリアクティビティを使用してください。
- ❌
@ionic/angularのインポートを使用しないでください。@ionic/vueを使用してください。
技術スタック
| 懸念事項 | Angular | React | Vue |
|---|---|---|---|
| フレームワーク | Ionic 8 + Angular 19 | Ionic 8 + React 19 | Ionic 8 + Vue 3.5 |
| ネイティブランタイム | Capacitor 7 | Capacitor 7 | Capacitor 7 |
| ナビゲーション | Angular Router(遅延読み込み) | @ionic/react-router | @ionic/vue-router |
| タブナビゲーション | ion-tabs + ion-tab-bar | ion-tabs + ion-tab-bar | ion-tabs + ion-tab-bar |
| 状態管理 | Angular Services(Signals/RxJS) | カスタムフック / Context | Composables / Pinia |
| 翻訳 | @ngx-translate/core | react-i18next | vue-i18n |
| 購入 | @revenuecat/purchases-capacitor | @revenuecat/purchases-capacitor | @revenuecat/purchases-capacitor |
| 広告 | @capacitor-community/admob | @capacitor-community/admob | @capacitor-community/admob |
| 通知 | @capacitor/push-notifications | @capacitor/push-notifications | @capacitor/push-notifications |
| ストレージ | @capacitor/preferences | @capacitor/preferences | @capacitor/preferences |
WARNING:
localStorageを直接使用しないでください!クロスプラットフォームの永続ストレージには、代わりに@capacitor/preferencesを使用してください。
プロジェクトの作成
ユーザーからアプリの作成を依頼された場合、必ず次の手順を実行してください。
- 最初にバンドル ID を確認します(例:「バンドル ID は何ですか?例:com.company.appname」)
- 現在のディレクトリにプロジェクトを作成します
- その後、必要なすべてのページを実装します
プロジェクトの作成(Angular)
npm install -g @ionic/cli
ionic start app-name blank --type=angular --capacitor
プロジェクトの作成(React)
npm install -g @ionic/cli
ionic start app-name blank --type=react --capacitor
プロジェクトの作成(Vue)
npm install -g @ionic/cli
ionic start app-name blank --type=vue --capacitor
Capacitor の設定(すべてのフレームワーク)
// capacitor.config.ts
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.company.appname',
appName: 'App Name',
webDir: 'www',
server: {
androidScheme: 'https',
},
};
export default config;
Note:React/Vue プロジェクトでは、
webDirは'www'ではなく'dist'の場合があります。プロジェクトのビルド出力ディレクトリを確認してください。
ネイティブプラットフォームの追加
npx cap add ios
npx cap add android
プロジェクト構成
Angular プロジェクトの構成
project-root/
├── src/
│ ├── app/
│ │ ├── app.component.ts
│ │ ├── app.component.html
│ │ ├── app.config.ts
│ │ ├── app.routes.ts
│ │ ├── tabs/
│ │ │ ├── tabs.page.ts
│ │ │ ├── tabs.page.html
│ │ │ ├── tabs.page.scss
│ │ │ └── tabs.routes.ts
│ │ ├── home/
│ │ │ ├── home.page.ts
│ │ │ ├── home.page.html
│ │ │ └── home.page.scss
│ │ ├── explore/
│ │ │ ├── explore.page.ts
│ │ │ ├── explore.page.html
│ │ │ └── explore.page.scss
│ │ ├── settings/
│ │ │ ├── settings.page.ts
│ │ │ ├── settings.page.html
│ │ │ └── settings.page.scss
│ │ ├── paywall/
│ │ │ ├── paywall.page.ts
│ │ │ ├── paywall.page.html
│ │ │ └── paywall.page.scss
│ │ ├── onboarding/
│ │ │ ├── onboarding.page.ts
│ │ │ ├── onboarding.page.html
│ │ │ └── onboarding.page.scss
│ │ ├── services/
│ │ │ ├── theme.service.ts
│ │ │ ├── onboarding.service.ts
│ │ │ ├── ads.service.ts
│ │ │ ├── purchases.service.ts
│ │ │ └── notifications.service.ts
│ │ ├── guards/
│ │ │ └── onboarding.guard.ts
│ │ └── utils/
│ │ ├── admob.ts
│ │ ├── purchases.ts
│ │ ├── onboarding.ts
│ │ ├── theme.ts
│ │ └── notifications.ts
│ ├── assets/
│ │ └── i18n/
│ │ ├── en.json
│ │ └── tr.json
│ ├── theme/
│ │ └── variables.scss
│ ├── global.scss
│ ├── index.html
│ └── main.ts
├── ios/
├── android/
├── capacitor.config.ts
├── angular.json
├── package.json
└── tsconfig.json
React プロジェクトの構成
project-root/
├── src/
│ ├── App.tsx
│ ├── main.tsx
│ ├── pages/
│ │ ├── OnboardingPage.tsx
│ │ ├── PaywallPage.tsx
│ │ ├── SettingsPage.tsx
│ │ ├── HomePage.tsx
│ │ └── ExplorePage.tsx
│ ├── components/
│ │ ├── TabsLayout.tsx
│ │ └── OnboardingGuard.tsx
│ ├── hooks/
│ │ ├── useTheme.ts
│ │ ├── useOnboarding.ts
│ │ ├── useAds.ts
│ │ ├── usePurchases.ts
│ │ └── useNotifications.ts
│ ├── utils/
│ │ ├── admob.ts
│ │ ├── purchases.ts
│ │ ├── onboarding.ts
│ │ ├── theme.ts
│ │ └── notifications.ts
│ ├── theme/
│ │ └── variables.css
│ └── i18n/
│ ├── index.ts
│ ├── en.json
│ └── tr.json
├── public/
├── ios/
├── android/
├── capacitor.config.ts
├── package.json
└── tsconfig.json
Vue プロジェクトの構成
project-root/
├── src/
│ ├── App.vue
│ ├── main.ts
│ ├── router/
│ │ └── index.ts
│ ├── views/
│ │ ├── OnboardingPage.vue
│ │ ├── PaywallPage.vue
│ │ ├── SettingsPage.vue
│ │ ├── HomePage.vue
│ │ ├── ExplorePage.vue
│ │ └── TabsLayout.vue
│ ├── composables/
│ │ ├── useTheme.ts
│ │ ├── useOnboarding.ts
│ │ ├── useAds.ts
│ │ ├── usePurchases.ts
│ │ └── useNotifications.ts
│ ├── utils/
│ │ ├── admob.ts
│ │ ├── purchases.ts
│ │ ├── onboarding.ts
│ │ ├── theme.ts
│ │ └── notifications.ts
│ ├── theme/
│ │ └── variables.css
│ └── assets/
│ └── i18n/
│ ├── en.json
│ └── tr.json
├── ios/
├── android/
├── capacitor.config.ts
├── package.json
└── tsconfig.json
アプリの設定
アプリの設定(Angular)
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter, RouteReuseStrategy } from '@angular/router';
import { provideIonicAngular, IonicRouteStrategy } from '@ionic/angular/standalone';
import { provideHttpClient } from '@angular/common/http';
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { HttpClient } from '@angular/common/http';
import { importProvidersFrom } from '@angular/core';
import { routes } from './app.routes';
export function createTranslateLoader(http: HttpClient) {
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
}
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideIonicAngular({ mode: 'md' }),
provideHttpClient(),
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
importProvidersFrom(
TranslateModule.forRoot({
defaultLanguage: 'en',
loader: {
provide: TranslateLoader,
useFactory: createTranslateLoader,
deps: [HttpClient],
},
})
),
],
};
<ion-app>
<ion-router-outlet></ion-router-outlet>
</ion-app>
// app.component.ts
import { Component, OnInit } from '@angular/core';
import { IonApp, IonRouterOutlet } from '@ionic/angular/standalone';
import { AdsService } from './services/ads.service';
@Component({
selector: 'app-root',
standalone: true,
imports: [IonApp, IonRouterOutlet],
templateUrl: './app.component.html',
})
export class AppComponent implements OnInit {
constructor(private adsService: AdsService) {}
async ngOnInit() {
await this.adsService.initialize();
}
}
アプリの設定(React)
// main.tsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
/* Core CSS required for Ionic components */
import '@ionic/react/css/core.css';
import '@ionic/react/css/normalize.css';
import '@ionic/react/css/structure.css';
import '@ionic/react/css/typography.css';
import '@ionic/react/css/padding.css';
import '@ionic/react/css/float-elements.css';
import '@ionic/react/css/text-alignment.css';
import '@ionic/react/css/text-transformation.css';
import '@ionic/react/css/flex-utils.css';
import '@ionic/react/css/display.css';
import './theme/variables.css';
import './i18n';
const root = createRoot(document.getElementById('root')!);
root.render(<App />);
// App.tsx
import { useEffect } from 'react';
import { IonApp, IonRouterOutlet, setupIonicReact } from '@ionic/react';
import { IonReactRouter } from '@ionic/react-router';
import { Route, Redirect } from 'react-router-dom';
import OnboardingPage from './pages/OnboardingPage';
import PaywallPage from './pages/PaywallPage';
import TabsLayout from './components/TabsLayout';
import { OnboardingGuard } from './components/OnboardingGuard';
import { initializeAdMob } from './utils/admob';
setupIonicReact({ mode: 'md' });
const App: React.FC = () => {
useEffect(() => {
initializeAdMob();
}, []);
return (
<IonApp>
<IonReactRouter>
<IonRouterOutlet>
<Route exact path="/onboarding" component={OnboardingPage} />
<Route exact path="/paywall" component={PaywallPage} />
<Route path="/tabs">
<OnboardingGuard>
<TabsLayout />
</OnboardingGuard>
</Route>
<Route exact path="/">
<Redirect to="/tabs" />
</Route>
</IonRouterOutlet>
</IonReactRouter>
</IonApp>
);
};
export default App;
// i18n/index.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import en from './en.json';
import tr from './tr.json';
const browserLang = navigator.language.split('-')[0];
i18n.use(initReactI18next).init({
resources: {
en: { translation: en },
tr: { translation: tr },
},
lng: ['en', 'tr'].includes(browserLang) ? browserLang : 'en',
fallbackLng: 'en',
interpolation: { escapeValue: false },
});
export default i18n;
アプリの設定(Vue)
// main.ts
import { createApp } from 'vue';
import { IonicVue } from '@ionic/vue';
import App from './App.vue';
import router from './router';
import { createI18n } from 'vue-i18n';
import en from './assets/i18n/en.json';
import tr from './assets/i18n/tr.json';
/* Core CSS required for Ionic components */
import '@ionic/vue/css/core.css';
import '@ionic/vue/css/normalize.css';
import '@ionic/vue/css/structure.css';
import '@ionic/vue/css/typography.css';
import '@ionic/vue/css/padding.css';
import '@ionic/vue/css/float-elements.css';
import '@ionic/vue/css/text-alignment.css';
import '@ionic/vue/css/text-transformation.css';
import '@ionic/vue/css/flex-utils.css';
import '@ionic/vue/css/display.css';
import './theme/variables.css';
const browserLang = navigator.language.split('-')[0];
const i18n = createI18n({
legacy: false,
locale: ['en', 'tr'].includes(browserLang) ? browserLang : 'en',
fallbackLocale: 'en',
messages: { en, tr },
});
const app = createApp(App);
app.use(IonicVue, { mode: 'md' });
app.use(router);
app.use(i18n);
router.isReady().then(() => app.mount('#app'));
<template>
<ion-app>
<ion-router-outlet />
</ion-app>
</template>
<script setup lang="ts">
import { IonApp, IonRouterOutlet } from '@ionic/vue';
import { onMounted } from 'vue';
import { initializeAdMob } from './utils/admob';
onMounted(async () => {
await initializeAdMob();
});
</script>
ルーティング
ルーティング(Angular)
// app.routes.ts
import { Routes } from '@angular/router';
import { onboardingGuard } from './guards/onboarding.guard';
export const routes: Routes = [
{
path: 'onboarding',
loadComponent: () => import('./onboarding/onboarding.page').then(m => m.OnboardingPage),
},
{
path: 'paywall',
loadComponent: () => import('./paywall/paywall.page').then(m => m.PaywallPage),
},
{
path: 'tabs',
loadChildren: () => import('./tabs/tabs.routes').then(m => m.tabsRoutes),
canActivate: [onboardingGuard],
},
{
path: '',
redirectTo: 'tabs',
pathMatch: 'full',
},
];
// tabs/tabs.routes.ts
import { Routes } from '@angular/router';
import { TabsPage } from './tabs.page';
export const tabsRoutes: Routes = [
{
path: '',
component: TabsPage,
children: [
{
path: 'home',
loadComponent: () => import('../home/home.page').then(m => m.HomePage),
},
{
path: 'explore',
loadComponent: () => import('../explore/explore.page').then(m => m.ExplorePage),
},
{
path: 'settings',
loadComponent: () => import('../settings/settings.page').then(m => m.SettingsPage),
},
{
path: '',
redirectTo: 'home',
pathMatch: 'full',
},
],
},
];
ルーティング(React)
ルートはApp.tsxで定義します(上記の「アプリの設定」を参照)。タブルートはTabsLayoutコンポーネントで定義します。
// components/TabsLayout.tsx
import { IonTabs, IonRouterOutlet, IonTabBar, IonTabButton, IonIcon, IonLabel } from '@ionic/react';
import { Route, Redirect } from 'react-router-dom';
import { home, compass, settings } from 'ionicons/icons';
import { useTranslation } from 'react-i18next';
import { useEffect } from 'react';
import HomePage from '../pages/HomePage';
import ExplorePage from '../pages/ExplorePage';
import SettingsPage from '../pages/SettingsPage';
import { showBannerAd, hideBannerAd } from '../utils/admob';
import { isPremiumUser } from '../utils/purchases';
const TabsLayout: React.FC = () => {
const { t } = useTranslation();
useEffect(() => {
isPremiumUser().then((premium) => {
if (!premium) showBannerAd();
});
return () => { hideBannerAd(); };
}, []);
return (
<IonTabs>
<IonRouterOutlet>
<Route exact path="/tabs/home" component={HomePage} />
<Route exact path="/tabs/explore" component={ExplorePage} />
<Route exact path="/tabs/settings" component={SettingsPage} />
<Route exact path="/tabs">
<Redirect to="/tabs/home" />
</Route>
</IonRouterOutlet>
<IonTabBar slot="bottom">
<IonTabButton tab="home" href="/tabs/home">
<IonIcon icon={home} />
<IonLabel>{t('tabs.home')}</IonLabel>
</IonTabButton>
<IonTabButton tab="explore" href="/tabs/explore">
<IonIcon icon={compass} />
<IonLabel>{t('tabs.explore')}</IonLabel>
</IonTabButton>
<IonTabButton tab="settings" href="/tabs/settings">
<IonIcon icon={settings} />
<IonLabel>{t('tabs.settings')}</IonLabel>
</IonTabButton>
</IonTabBar>
</IonTabs>
);
};
export default TabsLayout;
ルーティング(Vue)
// router/index.ts
import { createRouter, createWebHistory } from '@ionic/vue-router';
import { RouteRecordRaw } from 'vue-router';
import { isOnboardingCompleted } from '../utils/onboarding';
import TabsLayout from '../views/TabsLayout.vue';
const routes: RouteRecordRaw[] = [
{
path: '/onboarding',
component: () => import('../views/OnboardingPage.vue'),
},
{
path: '/paywall',
component: () => import('../views/PaywallPage.vue'),
},
{
path: '/tabs/',
component: TabsLayout,
children: [
{
path: '',
redirect: '/tabs/home',
},
{
path: 'home',
component: () => import('../views/HomePage.vue'),
},
{
path: 'explore',
component: () => import('../views/ExplorePage.vue'),
},
{
path: 'settings',
component: () => import('../views/SettingsPage.vue'),
},
],
},
{
path: '/',
redirect: '/tabs/',
},
];
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
});
router.beforeEach(async (to, _from, next) => {
if (to.path.startsWith('/tabs') || to.path === '/') {
const completed = await isOnboardingCompleted();
if (!completed) {
return next('/onboarding');
}
}
next();
});
export default router;
<template>
<ion-page>
<ion-tabs>
<ion-router-outlet />
<ion-tab-bar slot="bottom">
<ion-tab-button tab="home" href="/tabs/home">
<ion-icon :icon="home" />
<ion-label>{{ t('tabs.home') }}</ion-label>
</ion-tab-button>
<ion-tab-button tab="explore" href="/tabs/explore">
<ion-icon :icon="compass" />
<ion-label>{{ t('tabs.explore') }}</ion-label>
</ion-tab-button>
<ion-tab-button tab="settings" href="/tabs/settings">
<ion-icon :icon="settings" />
<ion-label>{{ t('tabs.settings') }}</ion-label>
</ion-tab-button>
</ion-tab-bar>
</ion-tabs>
</ion-page>
</template>
<script setup lang="ts">
import {
IonPage, IonTabs, IonRouterOutlet, IonTabBar, IonTabButton, IonIcon, IonLabel,
} from '@ionic/vue';
import { home, compass, settings } from 'ionicons/icons';
import { useI18n } from 'vue-i18n';
import { onMounted, onUnmounted } from 'vue';
import { showBannerAd, hideBannerAd } from '../utils/admob';
import { isPremiumUser } from '../utils/purchases';
const { t } = useI18n();
onMounted(async () => {
const premium = await isPremiumUser();
if (!premium) await showBannerAd();
});
onUnmounted(async () => {
await hideBannerAd();
});
</script>
共通ユーティリティ関数(フレームワーク非依存)
これらのユーティリティファイルには、Capacitor プラグインの呼び出しを含む純粋な TypeScript が記述されています。すべてのフレームワークで使用されます。各フレームワークでは、それぞれのパターン(Angular サービス、React フック、Vue コンポーザブル)でラップします。
ストレージ(すべてのフレームワーク)
import { Preferences } from '@capacitor/preferences';
// Set a value
await Preferences.set({ key: 'onboardingCompleted', value: 'true' });
// Get a value
const { value } = await Preferences.get({ key: 'onboardingCompleted' });
console.log(value); // 'true'
// Remove a value
await Preferences.remove({ key: 'onboardingCompleted' });
オンボーディング状態ユーティリティ
// utils/onboarding.ts
import { Preferences } from '@capacitor/preferences';
const KEY = 'onboardingCompleted';
export async function isOnboardingCompleted(): Promise<boolean> {
const { value } = await Preferences.get({ key: KEY });
return value === 'true';
}
export async function setOnboardingCompleted(completed: boolean): Promise<void> {
await Preferences.set({ key: KEY, value: String(completed) });
}
export async function resetOnboarding(): Promise<void> {
await Preferences.remove({ key: KEY });
}
テーマユーティリティ
// utils/theme.ts
import { Preferences } from '@capacitor/preferences';
export type ThemeMode = 'light' | 'dark' | 'system';
const KEY = 'themeMode';
export async function getTheme(): Promise<ThemeMode> {
const { value } = await Preferences.get({ key: KEY });
return (value as ThemeMode) || 'system';
}
export async function setTheme(mode: ThemeMode): Promise<void> {
await Preferences.set({ key: KEY, value: mode });
applyTheme(mode);
}
export function applyTheme(mode: ThemeMode): void {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const isDark = mode === 'dark' || (mode === 'system' && prefersDark);
document.documentElement.classList.toggle('ion-palette-dark', isDark);
}
AdMob ユーティリティ
// utils/admob.ts
import { AdMob, BannerAdOptions, BannerAdSize, BannerAdPosition } from '@capacitor-community/admob';
import { Capacitor } from '@capacitor/core';
let initialized = false;
export async function initializeAdMob(): Promise<void> {
if (!Capacitor.isNativePlatform()) return;
await AdMob.initialize({
initializeForTesting: true, // Set to false in production
});
initialized = true;
}
export async function showBannerAd(): Promise<void> {
if (!initialized) return;
const options: BannerAdOptions = {
adId: 'ca-app-pub-3940256099942544/6300978111', // Test ID - replace in production
adSize: BannerAdSize.ADAPTIVE_BANNER,
position: BannerAdPosition.BOTTOM_CENTER,
isTesting: true, // Set to false in production
};
await AdMob.showBanner(options);
}
export async function hideBannerAd(): Promise<void> {
if (!initialized) return;
await AdMob.hideBanner();
}
開発やテストには、テスト用広告 ID を使用してください。
- バナー:
ca-app-pub-3940256099942544/6300978111
AdMob の初期化を省略しないでください。省略すると、プラグインが正しく動作しません。
RevenueCat ユーティリティ
// utils/purchases.ts
import { Capacitor } from '@capacitor/core';
import { Purchases, LOG_LEVEL, PURCHASES_ERROR_CODE, PurchasesPackage } from '@revenuecat/purchases-capacitor';
let purchasesInitialized = false;
export async function initializePurchases(): Promise<void> {
if (!Capacitor.isNativePlatform()) return;
await Purchases.setLogLevel({ level: LOG_LEVEL.DEBUG }); // Use WARN in production
const apiKey = Capacitor.getPlatform() === 'ios'
? 'appl_YOUR_IOS_API_KEY'
: 'goog_YOUR_ANDROID_API_KEY';
await Purchases.configure({ apiKey });
purchasesInitialized = true;
}
export async function isPremiumUser(): Promise<boolean> {
if (!purchasesInitialized) return false;
try {
const { customerInfo } = await Purchases.getCustomerInfo();
return Object.keys(customerInfo.entitlements.active).length > 0;
} catch {
return false;
}
}
export async function getOfferings(): Promise<PurchasesPackage[]> {
if (!purchasesInitialized) return [];
try {
const { offerings } = await Purchases.getOfferings();
return offerings?.current?.availablePackages ?? [];
} catch {
return [];
}
}
export async function purchasePackage(pkg: PurchasesPackage): Promise<boolean> {
try {
const { customerInfo } = await Purchases.purchasePackage({ aPackage: pkg });
return Object.keys(customerInfo.entitlements.active).length > 0;
} catch (error: unknown) {
if ((error as { code?: string })?.code === PURCHASES_ERROR_CODE.PURCHASE_CANCELLED_ERROR) {
return false;
}
throw error;
}
}
export async function restorePurchases(): Promise<boolean> {
try {
const { customerInfo } = await Purchases.restorePurchases();
return Object.keys(customerInfo.entitlements.active).length > 0;
} catch {
return false;
}
}
通知ユーティリティ
// utils/notifications.ts
import { Capacitor } from '@capacitor/core';
import { PushNotifications } from '@capacitor/push-notifications';
export async function requestNotificationPermission(): Promise<boolean> {
if (!Capacitor.isNativePlatform()) return false;
const result = await PushNotifications.requestPermissions();
if (result.receive === 'granted') {
await PushNotifications.register();
return true;
}
return false;
}
export async function addNotificationListeners(): Promise<void> {
await PushNotifications.addListener('registration', (token) => {
console.log('Push registration success, token:', token.value);
});
await PushNotifications.addListener('registrationError', (error) => {
console.error('Push registration error:', error);
});
await PushNotifications.addListener('pushNotificationReceived', (notification) => {
console.log('Push notification received:', notification);
});
await PushNotifications.addListener('pushNotificationActionPerformed', (action) => {
console.log('Push notification action:', action);
});
}
フレームワーク固有のサービスラッパー
Angular サービス
// services/onboarding.service.ts
import { Injectable } from '@angular/core';
import { isOnboardingCompleted, setOnboardingCompleted, resetOnboarding } from '../utils/onboarding';
@Injectable({ providedIn: 'root' })
export class OnboardingService {
isCompleted = isOnboardingCompleted;
setCompleted = setOnboardingCompleted;
reset = resetOnboarding;
}
// services/theme.service.ts
import { Injectable } from '@angular/core';
import { getTheme, setTheme, applyTheme, ThemeMode } from '../utils/theme';
@Injectable({ providedIn: 'root' })
export class ThemeService {
async initialize(): Promise<void> {
const theme = await getTheme();
applyTheme(theme);
}
getTheme = getTheme;
setTheme = setTheme;
}
// services/ads.service.ts
import { Injectable } from '@angular/core';
import { initializeAdMob, showBannerAd, hideBannerAd } from '../utils/admob';
import { isPremiumUser } from '../utils/purchases';
@Injectable({ providedIn: 'root' })
export class AdsService {
async initialize(): Promise<void> {
await initializeAdMob();
}
async showBanner(): Promise<void> {
if (await isPremiumUser()) return;
await showBannerAd();
}
async hideBanner(): Promise<void> {
await hideBannerAd();
}
}
// services/purchases.service.ts
import { Injectable } from '@angular/core';
import {
initializePurchases,
isPremiumUser,
getOfferings,
purchasePackage,
restorePurchases,
} from '../utils/purchases';
@Injectable({ providedIn: 'root' })
export class PurchasesService {
initialize = initializePurchases;
isPremium = isPremiumUser;
getOfferings = getOfferings;
purchase = purchasePackage;
restorePurchases = restorePurchases;
}
// services/notifications.service.ts
import { Injectable } from '@angular/core';
import { requestNotificationPermission, addNotificationListeners } from '../utils/notifications';
@Injectable({ providedIn: 'root' })
export class NotificationsService {
requestPermission = requestNotificationPermission;
addListeners = addNotificationListeners;
}
React フック
// hooks/useOnboarding.ts
import { useCallback } from 'react';
import { isOnboardingCompleted, setOnboardingCompleted, resetOnboarding } from '../utils/onboarding';
export function useOnboarding() {
const isCompleted = useCallback(() => isOnboardingCompleted(), []);
const setCompleted = useCallback((v: boolean) => setOnboardingCompleted(v), []);
const reset = useCallback(() => resetOnboarding(), []);
return { isCompleted, setCompleted, reset };
}
// hooks/useTheme.ts
import { useCallback } from 'react';
import { getTheme, setTheme, applyTheme, ThemeMode } from '../utils/theme';
export function useTheme() {
const initialize = useCallback(async () => {
const theme = await getTheme();
applyTheme(theme);
}, []);
return {
initialize,
getTheme: useCallback(() => getTheme(), []),
setTheme: useCallback((mode: ThemeMode) => setTheme(mode), []),
};
}
// hooks/useAds.ts
import { useCallback } from 'react';
import { initializeAdMob, showBannerAd, hideBannerAd } from '../utils/admob';
import { isPremiumUser } from '../utils/purchases';
export function useAds() {
const initialize = useCallback(() => initializeAdMob(), []);
const showBanner = useCallback(async () => {
if (await isPremiumUser()) return;
await showBannerAd();
}, []);
const hideBanner = useCallback(() => hideBannerAd(), []);
return { initialize, showBanner, hideBanner };
}
// hooks/usePurchases.ts
import { useCallback } from 'react';
import {
initializePurchases,
isPremiumUser,
getOfferings,
purchasePackage,
restorePurchases,
} from '../utils/purchases';
import { PurchasesPackage } from '@revenuecat/purchases-capacitor';
export function usePurchases() {
return {
initialize: useCallback(() => initializePurchases(), []),
isPremium: useCallback(() => isPremiumUser(), []),
getOfferings: useCallback(() => getOfferings(), []),
purchase: useCallback((pkg: PurchasesPackage) => purchasePackage(pkg), []),
restorePurchases: useCallback(() => restorePurchases(), []),
};
}
// hooks/useNotifications.ts
import { useCallback } from 'react';
import { requestNotificationPermission, addNotificationListeners } from '../utils/notifications';
export function useNotifications() {
return {
requestPermission: useCallback(() => requestNotificationPermission(), []),
addListeners: useCallback(() => addNotificationListeners(), []),
};
}
Vue コンポーザブル
// composables/useOnboarding.ts
import { isOnboardingCompleted, setOnboardingCompleted, resetOnboarding } from '../utils/onboarding';
export function useOnboarding() {
return {
isCompleted: isOnboardingCompleted,
setCompleted: setOnboardingCompleted,
reset: resetOnboarding,
};
}
// composables/useTheme.ts
import { getTheme, setTheme, applyTheme, ThemeMode } from '../utils/theme';
export function useTheme() {
const initialize = async () => {
const theme = await getTheme();
applyTheme(theme);
};
return { initialize, getTheme, setTheme };
}
// composables/useAds.ts
import { initializeAdMob, showBannerAd, hideBannerAd } from '../utils/admob';
import { isPremiumUser } from '../utils/purchases';
export function useAds() {
const initialize = () => initializeAdMob();
const showBanner = async () => {
if (await isPremiumUser()) return;
await showBannerAd();
};
const hideBanner = () => hideBannerAd();
return { initialize, showBanner, hideBanner };
}
// composables/usePurchases.ts
import {
initializePurchases,
isPremiumUser,
getOfferings,
purchasePackage,
restorePurchases,
} from '../utils/purchases';
export function usePurchases() {
return {
initialize: initializePurchases,
isPremium: isPremiumUser,
getOfferings,
purchase: purchasePackage,
restorePurchases,
};
}
// composables/useNotifications.ts
import { requestNotificationPermission, addNotificationListeners } from '../utils/notifications';
export function useNotifications() {
return {
requestPermission: requestNotificationPermission,
addListeners: addNotificationListeners,
};
}
オンボーディングガード
オンボーディングガード(Angular)
// guards/onboarding.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { isOnboardingCompleted } from '../utils/onboarding';
export const onboardingGuard: CanActivateFn = async () => {
const router = inject(Router);
const completed = await isOnboardingCompleted();
if (!completed) {
router.navigateByUrl('/onboarding', { replaceUrl: true });
return false;
}
return true;
};
オンボーディングガード(React)
// components/OnboardingGuard.tsx
import { useEffect, useState } from 'react';
import { useIonRouter } from '@ionic/react';
import { isOnboardingCompleted } from '../utils/onboarding';
export const OnboardingGuard: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const router = useIonRouter();
const [checked, setChecked] = useState(false);
useEffect(() => {
isOnboardingCompleted().then((completed) => {
if (!completed) {
router.push('/onboarding', 'forward', 'replace');
} else {
setChecked(true);
}
});
}, []);
return checked ? <>{children}</> : null;
};
オンボーディングガード(Vue)
Vue のオンボーディングガードはrouter.beforeEachフックとして実装されています。上記の「ルーティング(Vue)」セクションを参照してください。
オンボーディングページ
共通動画 CSS(すべてのフレームワーク)
.background-video {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 0;
}
.gradient-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(to bottom, rgba(0,0,0,0.3), rgba(0,0,0,0.7));
z-index: 1;
}
.onboarding-slides {
position: relative;
z-index: 2;
height: 100%;
}
動画を実際に描画せず、参照するだけにしないでください。ネイティブ HTML5 の<video>を使用してください。canvas、アニメーション GIF、外部プレーヤーは使用しないでください。
オンボーディングページ(Angular)
<ion-content [fullscreen]="true" class="onboarding-content">
<video
#bgVideo
[src]="videoUrl"
autoplay
loop
muted
playsinline
class="background-video"
></video>
<div class="gradient-overlay"></div>
<div class="onboarding-slides">
</div>
</ion-content>
// onboarding/onboarding.page.scss
.background-video {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 0;
}
.gradient-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.7));
z-index: 1;
}
.onboarding-slides {
position: relative;
z-index: 2;
height: 100%;
}
// onboarding/onboarding.page.ts
import { Component } from '@angular/core';
import { IonContent, IonButton, IonIcon } from '@ionic/angular/standalone';
import { Router } from '@angular/router';
import { OnboardingService } from '../services/onboarding.service';
const VIDEO_URL =
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4';
@Component({
selector: 'app-onboarding',
standalone: true,
imports: [IonContent, IonButton, IonIcon],
templateUrl: './onboarding.page.html',
styleUrls: ['./onboarding.page.scss'],
})
export class OnboardingPage {
videoUrl = VIDEO_URL;
constructor(
private router: Router,
private onboardingService: OnboardingService
) {}
async completeOnboarding() {
await this.onboardingService.setCompleted(true);
this.router.navigateByUrl('/paywall', { replaceUrl: true });
}
}
オンボーディングページ(React)
// pages/OnboardingPage.tsx
import { IonContent, IonButton, IonIcon, useIonRouter } from '@ionic/react';
import { useOnboarding } from '../hooks/useOnboarding';
import './OnboardingPage.css';
const VIDEO_URL =
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4';
const OnboardingPage: React.FC = () => {
const router = useIonRouter();
const { setCompleted } = useOnboarding();
const completeOnboarding = async () => {
await setCompleted(true);
router.push('/paywall', 'forward', 'replace');
};
return (
<IonContent fullscreen className="onboarding-content">
<video
src={VIDEO_URL}
autoPlay
loop
muted
playsInline
className="background-video"
/>
<div className="gradient-overlay" />
<div className="onboarding-slides">
{/* Swiper slides content here */}
</div>
</IonContent>
);
};
export default OnboardingPage;
オンボーディングページ(Vue)
<template>
<ion-content :fullscreen="true" class="onboarding-content">
<video
:src="videoUrl"
autoplay
loop
muted
playsinline
class="background-video"
/>
<div class="gradient-overlay" />
<div class="onboarding-slides">
</div>
</ion-content>
</template>
<script setup lang="ts">
import { IonContent, IonButton, IonIcon } from '@ionic/vue';
import { useRouter } from 'vue-router';
import { useOnboarding } from '../composables/useOnboarding';
const VIDEO_URL =
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4';
const videoUrl = VIDEO_URL;
const router = useRouter();
const { setCompleted } = useOnboarding();
async function completeOnboarding() {
await setCompleted(true);
router.replace('/paywall');
}
</script>
<style scoped>
.background-video {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
object-fit: cover;
z-index: 0;
}
.gradient-overlay {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
background: linear-gradient(to bottom, rgba(0,0,0,0.3), rgba(0,0,0,0.7));
z-index: 1;
}
.onboarding-slides {
position: relative;
z-index: 2;
height: 100%;
}
</style>
ペイウォールページ
重要:オンボーディングの完了直後に、必ずペイウォールを表示してください。
ペイウォールには、必ず次の2つのサブスクリプションオプションを用意してください。
- 週払い - デフォルトのオプション
- 年払い - 「50% OFF」バッジ付き(おすすめとして、強調表示してください)
ボタンは3つです。「購読する」、「広告を見て続ける」、「購入を復元」。
フロー:Onboarding -> Paywall -> Main App (tabs)
ペイウォールページ(Angular)
<ion-content [fullscreen]="true">
<div class="paywall-container">
<h1>{{ 'paywall.title' | translate }}</h1>
<div class="subscription-options">
<div
*ngFor="let option of subscriptionOptions"
class="option-card"
[class.selected]="selectedPlan === option.id"
(click)="selectedPlan = option.id"
>
<ion-badge *ngIf="option.badge" color="danger">{{ option.badge }}</ion-badge>
<h3>{{ option.title | translate }}</h3>
<p>{{ option.price }}</p>
</div>
</div>
<ion-button expand="block" (click)="subscribe()">
{{ 'paywall.subscribe' | translate }}
</ion-button>
<ion-button fill="clear" (click)="skip()">
{{ 'paywall.skip' | translate }}
</ion-button>
<ion-button fill="clear" size="small" (click)="restore()">
{{ 'paywall.restore' | translate }}
</ion-button>
</div>
</ion-content>
// paywall/paywall.page.scss
.paywall-container {
// Add your paywall styles here
}
.subscription-options {
// Add your subscription options styles here
}
.option-card {
// Add your option card styles here
&.selected {
// Selected state styles
}
}
// paywall/paywall.page.ts
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import {
IonContent, IonButton, IonIcon, IonBadge,
} from '@ionic/angular/standalone';
import { NgFor, NgIf, NgClass } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { PurchasesService } from '../services/purchases.service';
@Component({
selector: 'app-paywall',
standalone: true,
imports: [
IonContent, IonButton, IonIcon, IonBadge,
NgFor, NgIf, NgClass, TranslateModule,
],
templateUrl: './paywall.page.html',
styleUrls: ['./paywall.page.scss'],
})
export class PaywallPage {
selectedPlan = 'weekly';
subscriptionOptions = [
{ id: 'weekly', title: 'paywall.weekly', price: '$4.99/week', badge: null },
{ id: 'yearly', title: 'paywall.yearly', price: '$129.99/year', badge: '50% OFF' },
];
constructor(
private router: Router,
private purchasesService: PurchasesService,
) {}
async subscribe() {
// Use RevenueCat to process purchase
this.router.navigateByUrl('/tabs', { replaceUrl: true });
}
skip() {
this.router.navigateByUrl('/tabs', { replaceUrl: true });
}
async restore() {
const restored = await this.purchasesService.restorePurchases();
if (restored) {
this.router.navigateByUrl('/tabs', { replaceUrl: true });
}
}
}
ペイウォールページ(React)
// pages/PaywallPage.tsx
import { useState } from 'react';
import {
IonContent, IonButton, IonBadge, useIonRouter,
} from '@ionic/react';
import { useTranslation } from 'react-i18next';
import { usePurchases } from '../hooks/usePurchases';
const subscriptionOptions = [
{ id: 'weekly', title: 'paywall.weekly', price: '$4.99/week', badge: null },
{ id: 'yearly', title: 'paywall.yearly', price: '$129.99/year', badge: '50% OFF' },
];
const PaywallPage: React.FC = () => {
const { t } = useTranslation();
const router = useIonRouter();
const { restorePurchases } = usePurchases();
const [selectedPlan, setSelectedPlan] = useState('weekly');
const subscribe = async () => {
// Use RevenueCat to process purchase
router.push('/tabs', 'forward', 'replace');
};
const skip = () => {
router.push('/tabs', 'forward', 'replace');
};
const restore = async () => {
const restored = await restorePurchases();
if (restored) {
router.push('/tabs', 'forward', 'replace');
}
};
return (
<IonContent fullscreen>
<div className="paywall-container">
<h1>{t('paywall.title')}</h1>
<div className="subscription-options">
{subscriptionOptions.map((option) => (
<div
key={option.id}
className={`option-card ${selectedPlan === option.id ? 'selected' : ''}`}
onClick={() => setSelectedPlan(option.id)}
>
{option.badge && <IonBadge color="danger">{option.badge}</IonBadge>}
<h3>{t(option.title)}</h3>
<p>{option.price}</p>
</div>
))}
</div>
<IonButton expand="block" onClick={subscribe}>
{t('paywall.subscribe')}
</IonButton>
<IonButton fill="clear" onClick={skip}>
{t('paywall.skip')}
</IonButton>
<IonButton fill="clear" size="small" onClick={restore}>
{t('paywall.restore')}
</IonButton>
</div>
</IonContent>
);
};
export default PaywallPage;
ペイウォールページ(Vue)
<template>
<ion-content :fullscreen="true">
<div class="paywall-container">
<h1>{{ t('paywall.title') }}</h1>
<div class="subscription-options">
<div
v-for="option in subscriptionOptions"
:key="option.id"
class="option-card"
:class="{ selected: selectedPlan === option.id }"
@click="selectedPlan = option.id"
>
<ion-badge v-if="option.badge" color="danger">{{ option.badge }}</ion-badge>
<h3>{{ t(option.title) }}</h3>
<p>{{ option.price }}</p>
</div>
</div>
<ion-button expand="block" @click="subscribe">
{{ t('paywall.subscribe') }}
</ion-button>
<ion-button fill="clear" @click="skip">
{{ t('paywall.skip') }}
</ion-button>
<ion-button fill="clear" size="small" @click="restore">
{{ t('paywall.restore') }}
</ion-button>
</div>
</ion-content>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { IonContent, IonButton, IonBadge } from '@ionic/vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { usePurchases } from '../composables/usePurchases';
const { t } = useI18n();
const router = useRouter();
const { restorePurchases } = usePurchases();
const selectedPlan = ref('weekly');
const subscriptionOptions = [
{ id: 'weekly', title: 'paywall.weekly', price: '$4.99/week', badge: null },
{ id: 'yearly', title: 'paywall.yearly', price: '$129.99/year', badge: '50% OFF' },
];
async function subscribe() {
// Use RevenueCat to process purchase
router.replace('/tabs');
}
function skip() {
router.replace('/tabs');
}
async function restore() {
const restored = await restorePurchases();
if (restored) {
router.replace('/tabs');
}
}
</script>
タブナビゲーション
よく使う Ionicons
| 用途 | Ionicon 名 |
|---|---|
| ホーム | home |
| 探索 | compass |
| 設定 | settings |
| プロフィール | person |
| 検索 | search |
| お気に入り | heart |
| 通知 | notifications |
タブナビゲーション(Angular)
<ion-tabs>
<ion-tab-bar slot="bottom">
<ion-tab-button tab="home">
<ion-icon name="home"></ion-icon>
<ion-label>{{ 'tabs.home' | translate }}</ion-label>
</ion-tab-button>
<ion-tab-button tab="explore">
<ion-icon name="compass"></ion-icon>
<ion-label>{{ 'tabs.explore' | translate }}</ion-label>
</ion-tab-button>
<ion-tab-button tab="settings">
<ion-icon name="settings"></ion-icon>
<ion-label>{{ 'tabs.settings' | translate }}</ion-label>
</ion-tab-button>
</ion-tab-bar>
</ion-tabs>
// tabs/tabs.page.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { IonTabs, IonTabBar, IonTabButton, IonIcon, IonLabel } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons';
import { home, compass, settings } from 'ionicons/icons';
import { TranslateModule } from '@ngx-translate/core';
import { AdsService } from '../services/ads.service';
@Component({
selector: 'app-tabs',
standalone: true,
imports: [IonTabs, IonTabBar, IonTabButton, IonIcon, IonLabel, TranslateModule],
templateUrl: './tabs.page.html',
styleUrls: ['./tabs.page.scss'],
})
export class TabsPage implements OnInit, OnDestroy {
constructor(private adsService: AdsService) {
addIcons({ home, compass, settings });
}
async ngOnInit() {
await this.adsService.showBanner();
}
async ngOnDestroy() {
await this.adsService.hideBanner();
}
}
タブナビゲーション(React)
上記のルーティング(React)セクションにある TabsLayout コンポーネントを参照してください。
タブナビゲーション(Vue)
上記のルーティング(Vue)セクションにある TabsLayout.vue コンポーネントを参照してください。
設定ページ
設定ページには、必ず次の項目を含めてください。
- 言語 - アプリの言語を変更します
- テーマ - ライト/ダーク/システム
- 通知 - 通知の有効/無効を切り替えます
- 広告を削除 - ペイウォールに移動します(すでにプレミアムの場合は非表示)
- オンボーディングをリセット - オンボーディングのフローを再開します(テスト/デモ用)
設定ページ(Angular)
<ion-header>
<ion-toolbar>
<ion-title>{{ 'settings.title' | translate }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item>
<ion-icon name="language" slot="start"></ion-icon>
<ion-label>{{ 'settings.language' | translate }}</ion-label>
<ion-select [value]="currentLang" (ionChange)="changeLanguage($event)">
<ion-select-option value="en">English</ion-select-option>
<ion-select-option value="tr">Türkçe</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-icon name="color-palette" slot="start"></ion-icon>
<ion-label>{{ 'settings.theme' | translate }}</ion-label>
<ion-select [value]="currentTheme" (ionChange)="changeTheme($event)">
<ion-select-option value="system">{{ 'settings.system' | translate }}</ion-select-option>
<ion-select-option value="light">{{ 'settings.light' | translate }}</ion-select-option>
<ion-select-option value="dark">{{ 'settings.dark' | translate }}</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-icon name="notifications" slot="start"></ion-icon>
<ion-label>{{ 'settings.notifications' | translate }}</ion-label>
<ion-toggle [checked]="notificationsEnabled" (ionChange)="toggleNotifications($event)"></ion-toggle>
</ion-item>
<ion-item *ngIf="!isPremium" button (click)="removeAds()">
<ion-icon name="star" slot="start"></ion-icon>
<ion-label>{{ 'settings.removeAds' | translate }}</ion-label>
</ion-item>
<ion-item button (click)="resetOnboarding()">
<ion-icon name="refresh" slot="start"></ion-icon>
<ion-label>{{ 'settings.resetOnboarding' | translate }}</ion-label>
</ion-item>
</ion-list>
</ion-content>
// settings/settings.page.scss
// Add your settings page styles here
// settings/settings.page.ts
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import {
IonContent, IonHeader, IonTitle, IonToolbar,
IonList, IonItem, IonLabel, IonIcon, IonToggle, IonSelect, IonSelectOption,
} from '@ionic/angular/standalone';
import { NgIf } from '@angular/common';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { Preferences } from '@capacitor/preferences';
import { ThemeService } from '../services/theme.service';
import { OnboardingService } from '../services/onboarding.service';
import { PurchasesService } from '../services/purchases.service';
@Component({
selector: 'app-settings',
standalone: true,
imports: [
IonContent, IonHeader, IonTitle, IonToolbar,
IonList, IonItem, IonLabel, IonIcon, IonToggle, IonSelect, IonSelectOption,
NgIf, TranslateModule,
],
templateUrl: './settings.page.html',
styleUrls: ['./settings.page.scss'],
})
export class SettingsPage implements OnInit {
currentLang = 'en';
currentTheme = 'system';
notificationsEnabled = false;
isPremium = false;
constructor(
private router: Router,
private themeService: ThemeService,
private onboardingService: OnboardingService,
private purchasesService: PurchasesService,
private translate: TranslateService,
) {}
async ngOnInit() {
this.currentLang = this.translate.currentLang || 'en';
this.currentTheme = await this.themeService.getTheme();
this.isPremium = await this.purchasesService.isPremium();
}
changeLanguage(event: CustomEvent) {
const lang = event.detail.value;
this.translate.use(lang);
Preferences.set({ key: 'language', value: lang });
}
changeTheme(event: CustomEvent) {
this.themeService.setTheme(event.detail.value);
}
toggleNotifications(event: CustomEvent) {
this.notificationsEnabled = event.detail.checked;
}
removeAds() {
this.router.navigateByUrl('/paywall');
}
async resetOnboarding() {
await this.onboardingService.reset();
this.router.navigateByUrl('/onboarding', { replaceUrl: true });
}
}
設定ページ(React)
// pages/SettingsPage.tsx
import { useState, useEffect } from 'react';
import {
IonContent, IonHeader, IonTitle, IonToolbar, IonPage,
IonList, IonItem, IonLabel, IonIcon, IonToggle, IonSelect, IonSelectOption,
useIonRouter,
} from '@ionic/react';
import { language, colorPalette, notifications, star, refresh } from 'ionicons/icons';
import { useTranslation } from 'react-i18next';
import { Preferences } from '@capacitor/preferences';
import { useTheme } from '../hooks/useTheme';
import { useOnboarding } from '../hooks/useOnboarding';
import { usePurchases } from '../hooks/usePurchases';
import { ThemeMode } from '../utils/theme';
const SettingsPage: React.FC = () => {
const { t, i18n } = useTranslation();
const router = useIonRouter();
const { getTheme, setTheme } = useTheme();
const { reset } = useOnboarding();
const { isPremium } = usePurchases();
const [currentLang, setCurrentLang] = useState('en');
const [currentTheme, setCurrentTheme] = useState<ThemeMode>('system');
const [notificationsEnabled, setNotificationsEnabled] = useState(false);
const [premium, setPremium] = useState(false);
useEffect(() => {
setCurrentLang(i18n.language || 'en');
getTheme().then(setCurrentTheme);
isPremium().then(setPremium);
}, []);
const changeLanguage = (lang: string) => {
setCurrentLang(lang);
i18n.changeLanguage(lang);
Preferences.set({ key: 'language', value: lang });
};
const changeTheme = (mode: ThemeMode) => {
setCurrentTheme(mode);
setTheme(mode);
};
const resetOnboarding = async () => {
await reset();
router.push('/onboarding', 'forward', 'replace');
};
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>{t('settings.title')}</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonList>
<IonItem>
<IonIcon icon={language} slot="start" />
<IonLabel>{t('settings.language')}</IonLabel>
<IonSelect value={currentLang} onIonChange={(e) => changeLanguage(e.detail.value)}>
<IonSelectOption value="en">English</IonSelectOption>
<IonSelectOption value="tr">Türkçe</IonSelectOption>
</IonSelect>
</IonItem>
<IonItem>
<IonIcon icon={colorPalette} slot="start" />
<IonLabel>{t('settings.theme')}</IonLabel>
<IonSelect value={currentTheme} onIonChange={(e) => changeTheme(e.detail.value)}>
<IonSelectOption value="system">{t('settings.system')}</IonSelectOption>
<IonSelectOption value="light">{t('settings.light')}</IonSelectOption>
<IonSelectOption value="dark">{t('settings.dark')}</IonSelectOption>
</IonSelect>
</IonItem>
<IonItem>
<IonIcon icon={notifications} slot="start" />
<IonLabel>{t('settings.notifications')}</IonLabel>
<IonToggle
checked={notificationsEnabled}
onIonChange={(e) => setNotificationsEnabled(e.detail.checked)}
/>
</IonItem>
{!premium && (
<IonItem button onClick={() => router.push('/paywall')}>
<IonIcon icon={star} slot="start" />
<IonLabel>{t('settings.removeAds')}</IonLabel>
</IonItem>
)}
<IonItem button onClick={resetOnboarding}>
<IonIcon icon={refresh} slot="start" />
<IonLabel>{t('settings.resetOnboarding')}</IonLabel>
</IonItem>
</IonList>
</IonContent>
</IonPage>
);
};
export default SettingsPage;
設定ページ(Vue)
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>{{ t('settings.title') }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item>
<ion-icon name="language" slot="start" />
<ion-label>{{ t('settings.language') }}</ion-label>
<ion-select :value="currentLang" @ion-change="changeLanguage($event)">
<ion-select-option value="en">English</ion-select-option>
<ion-select-option value="tr">Türkçe</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-icon name="color-palette" slot="start" />
<ion-label>{{ t('settings.theme') }}</ion-label>
<ion-select :value="currentTheme" @ion-change="changeTheme($event)">
<ion-select-option value="system">{{ t('settings.system') }}</ion-select-option>
<ion-select-option value="light">{{ t('settings.light') }}</ion-select-option>
<ion-select-option value="dark">{{ t('settings.dark') }}</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-icon name="notifications" slot="start" />
<ion-label>{{ t('settings.notifications') }}</ion-label>
<ion-toggle :checked="notificationsEnabled" @ion-change="toggleNotifications($event)" />
</ion-item>
<ion-item v-if="!premium" button @click="removeAds">
<ion-icon name="star" slot="start" />
<ion-label>{{ t('settings.removeAds') }}</ion-label>
</ion-item>
<ion-item button @click="resetOnboardingFlow">
<ion-icon name="refresh" slot="start" />
<ion-label>{{ t('settings.resetOnboarding') }}</ion-label>
</ion-item>
</ion-list>
</ion-content>
</ion-page>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import {
IonPage, IonHeader, IonToolbar, IonTitle, IonContent,
IonList, IonItem, IonLabel, IonIcon, IonToggle, IonSelect, IonSelectOption,
} from '@ionic/vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { Preferences } from '@capacitor/preferences';
import { useTheme } from '../composables/useTheme';
import { useOnboarding } from '../composables/useOnboarding';
import { usePurchases } from '../composables/usePurchases';
import type { ThemeMode } from '../utils/theme';
const { t, locale } = useI18n();
const router = useRouter();
const { getTheme, setTheme } = useTheme();
const { reset } = useOnboarding();
const { isPremium } = usePurchases();
const currentLang = ref('en');
const currentTheme = ref<ThemeMode>('system');
const notificationsEnabled = ref(false);
const premium = ref(false);
onMounted(async () => {
currentLang.value = locale.value || 'en';
currentTheme.value = await getTheme();
premium.value = await isPremium();
});
function changeLanguage(event: CustomEvent) {
const lang = event.detail.value;
currentLang.value = lang;
locale.value = lang;
Preferences.set({ key: 'language', value: lang });
}
function changeTheme(event: CustomEvent) {
const mode = event.detail.value as ThemeMode;
currentTheme.value = mode;
setTheme(mode);
}
function toggleNotifications(event: CustomEvent) {
notificationsEnabled.value = event.detail.checked;
}
function removeAds() {
router.push('/paywall');
}
async function resetOnboardingFlow() {
await reset();
router.replace('/onboarding');
}
</script>
ローカライズ
翻訳ファイル(共通 - すべてのフレームワーク)
// en.json
{
"tabs": {
"home": "Home",
"explore": "Explore",
"settings": "Settings"
},
"onboarding": {
"next": "Next",
"start": "Get Started",
"skip": "Skip"
},
"paywall": {
"title": "Go Premium",
"weekly": "Weekly",
"yearly": "Yearly",
"subscribe": "Subscribe",
"skip": "Continue with ads",
"restore": "Restore Purchases"
},
"settings": {
"title": "Settings",
"language": "Language",
"theme": "Theme",
"system": "System",
"light": "Light",
"dark": "Dark",
"notifications": "Notifications",
"removeAds": "Remove Ads",
"resetOnboarding": "Reset Onboarding"
}
}
// tr.json
{
"tabs": {
"home": "Ana Sayfa",
"explore": "Keşfet",
"settings": "Ayarlar"
},
"onboarding": {
"next": "İleri",
"start": "Başla",
"skip": "Atla"
},
"paywall": {
"title": "Premium'a Geç",
"weekly": "Haftalık",
"yearly": "Yıllık",
"subscribe": "Abone Ol",
"skip": "Reklamlı devam et",
"restore": "Satın Alımları Geri Yükle"
},
"settings": {
"title": "Ayarlar",
"language": "Dil",
"theme": "Tema",
"system": "Sistem",
"light": "Açık",
"dark": "Koyu",
"notifications": "Bildirimler",
"removeAds": "Reklamları Kaldır",
"resetOnboarding": "Tanıtımı Sıfırla"
}
}
トルコ語のローカライズ(重要)
tr.json を作成する際は、必ず正しいトルコ語の文字を使用してください。
- ı(小文字のドットなし i) - i ではありません
- İ(大文字のドット付き I) - I ではありません
- ü、Ü、ö、Ö、ç、Ç、ş、Ş、ğ、Ğ
例:
- ✅ 「Ayarlar」、「Giriş」、「Çıkış」、「Başla」、「İleri」、「Güncelle」
- ❌ 「Ayarlar」、「Giris」、「Cikis」、「Basla」、「Ileri」、「Guncelle」
i18n の設定(Angular)
i18n は @ngx-translate/core を使用して app.config.ts で設定されています。アプリの設定(Angular)セクションを参照してください。
.html テンプレートファイルでの使用方法:
{{ 'settings.title' | translate }}
app.component.ts で言語を検出します。
import { TranslateService } from '@ngx-translate/core';
constructor(private translate: TranslateService) {
const browserLang = navigator.language.split('-')[0];
translate.setDefaultLang('en');
translate.use(['en', 'tr'].includes(browserLang) ? browserLang : 'en');
}
i18n の設定(React)
i18n は i18n/index.ts で設定されています。アプリの設定(React)セクションを参照してください。
コンポーネントでの使用方法:
import { useTranslation } from 'react-i18next';
const MyComponent: React.FC = () => {
const { t } = useTranslation();
return <h1>{t('settings.title')}</h1>;
};
i18n の設定(Vue)
i18n は vue-i18n を使用して main.ts で設定されています。アプリの設定(Vue)セクションを参照してください。
テンプレートでの使用方法:
<template>
<h1>{{ t('settings.title') }}</h1>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
</script>
フレームワークのベストプラクティス
Angular のベストプラクティス
必ず HTML、TS、SCSS ファイルを分けたスタンドアロンコンポーネントを使用してください。
❌ 誤り(NgModules + IonicModule):
// home.module.ts
@NgModule({
imports: [CommonModule, IonicModule],
declarations: [HomePage],
})
export class HomePageModule {}
❌ 誤り(インラインテンプレート):
@Component({
selector: 'app-home',
standalone: true,
imports: [IonContent, IonHeader, IonTitle, IonToolbar, TranslateModule],
template: `
<ion-header>
<ion-toolbar>
<ion-title>{{ 'home.title' | translate }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
</ion-content>
`,
})
export class HomePage {}
✅ 正しい(.html、.ts、.scss ファイルを分ける):
<ion-header>
<ion-toolbar>
<ion-title>{{ 'home.title' | translate }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
</ion-content>
// home/home.page.scss
// Page-specific styles here
// home/home.page.ts
@Component({
selector: 'app-home',
standalone: true,
imports: [IonContent, IonHeader, IonTitle, IonToolbar, TranslateModule],
templateUrl: './home.page.html',
styleUrls: ['./home.page.scss'],
})
export class HomePage {}
個別の Ionic コンポーネント(例:IonContent、IonButton)をインポートしてください。スタンドアロンコンポーネントでは、IonicModule をインポートしないでください。
必ず templateUrl と styleUrls を使用してください。インラインの template や styles は使用しないでください。
React のベストプラクティス
必ずフックを使った関数コンポーネントを使用してください。
❌ 誤り:
class HomePage extends React.Component {
render() {
return <IonContent>...</IonContent>;
}
}
✅ 正しい:
import { IonContent, IonHeader, IonTitle, IonToolbar, IonPage } from '@ionic/react';
import { useTranslation } from 'react-i18next';
const HomePage: React.FC = () => {
const { t } = useTranslation();
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>{t('home.title')}</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
{/* content */}
</IonContent>
</IonPage>
);
};
export default HomePage;
Ionic のページ遷移とライフサイクルを適切に機能させるため、ページのコンテンツは必ず <IonPage> でラップしてください。
Vue のベストプラクティス
必ず <script setup> を使った Composition API を使用してください。
❌ 誤り:
<script>
export default {
data() {
return { title: 'Home' };
},
};
</script>
✅ 正しい:
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>{{ t('home.title') }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
</ion-content>
</ion-page>
</template>
<script setup lang="ts">
import { IonPage, IonHeader, IonToolbar, IonTitle, IonContent } from '@ionic/vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
</script>
Ionic のページ遷移とライフサイクルを適切に機能させるため、ページのコンテンツは必ず <ion-page> でラップしてください。
作成後のクリーンアップ(必ず実施)
新しい Ionic プロジェクトを作成した後は、必ず次の作業を行ってください。
- プロジェクトの構成と競合する、デフォルトで生成されたページを削除します
- ビルド設定で正しい出力ディレクトリが指定されていることを確認します
capacitor.config.tsに正しいappIdとappNameが設定されていることを確認します- すべての Ionic インポートで、フレームワークに対応した正しいパッケージが使用されていることを確認します
コードの完成後に実行(必ず実行)
Angular
npm install
npx ng build
npx cap sync
React
npm install
npm run build
npx cap sync
Vue
npm install
npm run build
npx cap sync
npm installで新しい依存関係をインストールします- ビルドでプロジェクトをコンパイルします
cap syncでウェブアセットとプラグインをネイティブプロジェクトに同期します
これらの手順は必ず実行してください。
開発コマンド
Angular
npm install
ionic serve # Run in browser
ionic build # Build for production
npx cap sync # Sync web assets to native
npx cap open ios # Open in Xcode
npx cap open android # Open in Android Studio
npx cap run ios # Build and run on iOS device/simulator
npx cap run android # Build and run on Android device/emulator
React
npm install
ionic serve # Run in browser
ionic build # Build for production
npx cap sync # Sync web assets to native
npx cap open ios # Open in Xcode
npx cap open android # Open in Android Studio
npx cap run ios # Build and run on iOS
npx cap run android # Build and run on Android
Vue
npm install
ionic serve # Run in browser
ionic build # Build for production
npx cap sync # Sync web assets to native
npx cap open ios # Open in Xcode
npx cap open android # Open in Android Studio
npx cap run ios # Build and run on iOS
npx cap run android # Build and run on Android
コーディング規約
すべてのフレームワーク
- TypeScript の厳格な型チェックを使用します。
any型は禁止です - 文字列をハードコードせず、翻訳キーを使用します
- 永続ストレージには
@capacitor/preferencesを使用します - すべてのページを遅延読み込みします
- ネイティブ専用コードには
Capacitor.isNativePlatform()を使用してガードします - Capacitor プラグインのメソッドは必ず
awaitします
Angular 固有
- スタンドアロンコンポーネントを使用します(ページやコンポーネントに NgModules は絶対に使用しません)
- 必ず個別の
.html、.ts、.scssファイルを使用します。インラインのtemplateやstylesは絶対に使用しません @ComponentデコレーターではtemplateUrlとstyleUrlsを使用します- Angular の
inject()関数、またはコンストラクターインジェクションを使用します - ルートでは
loadComponentを使用して遅延読み込みします - 可能な場合は、リアクティブな状態管理に Angular Signals を使用します
- Ionic コンポーネントを個別にインポートします。
IonicModuleは絶対に使用しません ioniconsのaddIcons()を使用してアイコンを登録します
React 固有
- フックを使用した関数コンポーネントを使用します
- ページを
<IonPage>で囲みます - ナビゲーションには
useIonRouter()を使用します - i18n には
useTranslation()を使用します - アイコンは
ionicons/iconsから直接インポートします
Vue 固有
<script setup lang="ts">を使用した Composition API を使用します- ページを
<ion-page>で囲みます - ナビゲーションには
vue-routerのuseRouter()を使用します - i18n には
useI18n()を使用します - アイコンは
ionicons/iconsからインポートし、:iconプロパティで渡します
重要事項
- iOS の権限は
Info.plistで定義します(Capacitor プラグインによって追加されます) - Android の権限は
AndroidManifest.xmlで定義します(Capacitor プラグインによって追加されます) - 新しい Capacitor プラグインをインストールした後は、必ず
npx cap syncを実行します - ネイティブ専用コードには
Capacitor.isNativePlatform()を使用してガードします - 迅速な開発には
ionic serveを使ってブラウザーでテストし、その後デバイスでテストします
App Store と Play Store に関する注意事項
- パーソナライズ広告には iOS ATT 権限が必要です
- 購入の復元が正しく動作する必要があります
- Target SDK は最新の状態に保つ必要があります
- ネイティブビルドの前には
npx cap syncを使用します
テストチェックリスト
- すべての言語で UI をテスト済み
- ダークモード/ライトモード
- 通知
- プレミアムのフロー
- 購入の復元
- オフライン対応
- 複数の画面サイズ
- ブラウザー(ionic serve)とネイティブプラットフォーム
開発後
npm run build
npx cap sync
npx cap open ios
npx cap open android
注:
cap syncはビルド済みのウェブアプリをネイティブプロジェクトにコピーし、プラグインを同期します。ネイティブプラットフォームでテストする前に、ビルドのたびに実行してください。