本文へ移動
rdlabo.devdocs

Ionic アプリで Native UI をセットアップする

既存の Web 外観を維持し、対応する Ionic コントロールを iOS でネイティブ描画します。このガイドに iOS テーマのスタイルシートは不要です。

iOS 27 テーマを使用中ならテーマに Native UI を追加するを参照してください。Ionic がなければ HTML 設定を使用します。

Native UI が初めてなら投影の仕組みから読んでください。このガイドは既存の Ionic コントロールをその表示へ接続します。

始める前に

要件 通常のネイティブコントロール
Ionic >=8.8.1 <10
ネイティブプロジェクト iOS プロジェクトを持つ Capacitor 8
ビルドツール Xcode 26 以降
デバイス・シミュレーター iOS 26 以降
ネイティブ依存管理 既存の SPM または CocoaPods 連携を維持。
Vertical Bars 別のデバイス要件を参照。

まだ Capacitor を使っていない場合は、以下の手順の前に Capacitor iOS プロジェクトを作成・設定してください。ブラウザープレビューは Web コントロールを維持し、UIKit・SwiftUI 描画の確認にはなりません。

1. プラグインをインストールする

デバイス状態の標準連携には Capacitor Foldable を使います。配置と折りたたみ状態の API でレイアウトの適応をアプリが制御できます。導入後は初期値と変更イベントを接続します。

Capacitor パッケージはバージョン 8 を維持します。次をアプリの直接依存に追加します。

npm install @rdlabo/capacitor-native-ui @erkamyaman/capacitor-foldable

Ionic アダプターはアプリに既存の @ionic/core を使います。ページ内容とフォールバックの装飾に必要な既存 Web スタイルを削除しないでください。

2. Shell を有効にする

ion-app のマウント後、アプリの起動時に一度呼びます。

import { enableNativeUI } from '@rdlabo/capacitor-native-ui';

const shell = await enableNativeUI();

機能無効化や投影停止に備えてハンドルを保持します。アプリの所有元を破棄するとき await shell.destroy() を呼びます。ページごとに Shell を起動しないでください。

既存の Ionic マークアップがラベル、アイコン、無効状態、操作を提供します。たとえば固定ページヘッダーに次を置けます。

<ion-header>
  <ion-toolbar>
    <ion-title>My app</ion-title>
    <ion-buttons slot="end">
      <ion-button aria-label="Add">
        <ion-icon slot="icon-only" name="add"></ion-icon>
      </ion-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

フレームワークのハンドラーは元のボタンに維持し、通常どおりアイコンを登録します。対応マークアップを確認してください。非対応コントロールは Web に残ります。

フレームワークのアプリルートに起動処理を置く

ルーティングされるページより上に、アプリ全体で 1 つの Shell を維持します。以下のアプリ所有ヘルパーは React 開発時の再マウントを含め非同期起動・終了を直列化します。共通の起動オプションをここに置き、他の enableNativeUI() 呼び出しは削除します。

// native-ui.ts — ルートコンポーネントの隣に配置します
import { enableNativeUI, type NativeUIHandle } from '@rdlabo/capacitor-native-ui';
// Ionic がなければ @rdlabo/capacitor-native-ui/core を使います。

let lifecycle = Promise.resolve();
const report = (error: unknown) => console.error('Native UI lifecycle failed', error);

export function mountNativeUI(): () => void {
  let disposed = false;
  let shell: NativeUIHandle | undefined;
  lifecycle = lifecycle
    .then(async () => {
      if (disposed) return;
      shell = await enableNativeUI(); // ここにオプションを指定します。
      if (disposed) await shell.destroy();
    })
    .catch(report);
  return () => {
    disposed = true;
    lifecycle = lifecycle
      .then(async () => {
        await shell?.destroy();
        shell = undefined;
      })
      .catch(report);
  };
}

以下からフレームワークを 1 つ選びます。ヘルパーはアプリのコードで、追加インストールするパッケージではありません。

Angular: ルートコンポーネントの最初の DOM 描画後に呼び、DestroyRef にクリーンアップを登録します。

import { afterNextRender, DestroyRef, inject } from '@angular/core';
import { mountNativeUI } from './native-ui';

// ルートの AppComponent のコンストラクター内:
const destroyRef = inject(DestroyRef);
afterNextRender(() => {
  const stop = mountNativeUI();
  destroyRef.onDestroy(stop);
});

React: IonApp を描画するルートコンポーネントで effect を 1 つ使います。

import { useEffect } from 'react';
import { mountNativeUI } from './native-ui';

// ルートの App コンポーネント内:
useEffect(() => mountNativeUI(), []);

Vue: ルートコンポーネントの <script setup> にライフサイクルフックを登録します。

import { onMounted, onBeforeUnmount } from 'vue';
import { mountNativeUI } from './native-ui';

let stop: (() => void) | undefined;
onMounted(() => {
  stop = mountNativeUI();
});
onBeforeUnmount(() => {
  stop?.();
});

これらのフックはページごとの遷移ではなくアプリの寿命を管理します。以下のナビゲーションも設定してください。

3. ナビゲーションを調整する

任意の Ionic アニメーション連携には @rdlabo/ionic-theme-ios27 を導入し、Native UI と同時更新して版を揃えます。Ionic の初期化前にナビゲーションを設定します。アダプターはテーマ CSS を読み込まず Ionic 標準の遷移を維持できます。

npm install @rdlabo/ionic-theme-ios27 @rdlabo/capacitor-native-ui @erkamyaman/capacitor-foldable
import { iosTransitionAnimation, mdTransitionAnimation, type AnimationBuilder } from '@ionic/core';
import { withNativeUITransition } from '@rdlabo/ionic-theme-ios27';

const defaultTransition: AnimationBuilder = (baseEl, opts) =>
  (opts.mode === 'ios' ? iosTransitionAnimation : mdTransitionAnimation)(baseEl, opts);

const ionicConfig = {
  navAnimation: withNativeUITransition(defaultTransition),
};

Angular の provideIonicAngular、React の setupIonicReact、Vue の IonicVue に渡します。独自アニメーションは遷移アダプターの手順を参照してください。この任意のアダプターにはテーマパッケージが必要ですが、スタイルシートは不要でアニメーションも置き換えません。

プラグインのみのアプリは後述のテーマなしのナビゲーションを使います。遷移をテストする前にいずれか 1 方式を設定してください。

4. ビルド・同期して実行する

アプリの Web ビルド後に同期します。

npx cap sync ios

Xcode から iOS 26 以降のデバイス・シミュレーターで起動します。対応する固定コントロールがネイティブ表示され元のハンドラーを呼ぶこと、前後の遷移と戻るスワイプのキャンセルを確認します。ブラウザー・Android ビルドでは Web コントロールが動作することを確認してください。

症状 確認事項
iOS ですべて Web のまま ネイティブパッケージが直接依存で、同期・再ビルド済み、対応 OS で起動処理が実行されていること。
1 つだけ Web のまま マークアップとスタイルを対応コントロールと比較。
遷移中にコントロールが重なる 遷移前にアダプターまたは suspend/resume のライフサイクルを設定。
スタイルや余白がない アプリの Web CSS を維持。プラグインはページ配置を提供しない。

次のステップ

テーマなしのナビゲーション

Native UI はテーマや Ionic アニメーションビルダーに依存しません。ルーターでページ変更前に投影を中断し、完了・キャンセル後に再開できます。

const suspension = await shell.suspend();
try {
  await navigate();
} finally {
  await suspension.resume();
}

Promise は URL 変更だけでなく、DOM とページアニメーションが落ち着いてから完了する必要があります。ブラウザーの戻る、ジェスチャーキャンセル、プログラムによる遷移も同じライフサイクルへ接続します。Ionic は前述のアダプターでアニメーションと対話的な遷移に対応します。

ルーターなしの HTML アプリでは、次が完全なアニメーション付きページ切り替えです。キャンセルされたアニメーションは投影再開前に旧ページを復元します。

async function showPage(current: HTMLElement, next: HTMLElement) {
  const lease = await shell.suspend();
  try {
    current.hidden = true;
    next.hidden = false;
    if (!matchMedia('(prefers-reduced-motion: reduce)').matches) {
      const animation = next.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 150 });
      try {
        await animation.finished;
      } catch {
        next.hidden = true;
        current.hidden = false;
      }
    }
  } finally {
    await lease.resume();
  }
}

Promise 確定前に次の遷移を許すアプリではページ切り替えを直列化します。ルーター・履歴の処理はソースのクリックハンドラーに維持してください。プラグインは代わりに遷移しません。