複数のアプリを作成管理する方法
完全に独立した複数アプリを管理する場合、「pnpm workspaces」や「npm workspaces」を活用したモノレポ構成が最もスッキリまとまり、依存関係の競合も防げるため最適です。
以下に、最も軽量で扱いやすい npm workspaces を使った具体的なディレクトリ構成と設定手順を解説します。
1. 推奨のディレクトリ構成
ルート(親)配下に、共通で使う共有ライブラリ(shared)と、完全に独立したCapacitorアプリ(app-a, app-b)を配置します。
my-capacitor-monorepo/
├── package.json # ルートの設定ファイル
├── node_modules/ # 依存パッケージ(一元管理)
└── apps/
├── app-a/ # アプリA(独立したCapacitor環境)
│ ├── package.json
│ ├── capacitor.config.ts
│ ├── android/ # アプリA専用のネイティブプロジェクト
│ └── ios/
└── app-b/ # アプリB(独立したCapacitor環境)
├── package.json
├── capacitor.config.ts
├── android/ # アプリB専用のネイティブプロジェクト
└── ios/
フォルダを上記のように配置し、package.jsonを隣に置いてpackage.jsonは下記のようにすればOK
2. ルートの package.json の設定
プロジェクトのルート直下にある package.json で、apps ディレクトリ配下をワークスペースとして指定します。
{
"name": "my-capacitor-monorepo",
"private": true,
"workspaces": [
"apps/*"
],
"scripts": {
"build:a": "npm run build --workspace=apps/app-a",
"sync:a": "npm run cap:sync --workspace=apps/app-a",
"build:b": "npm run build --workspace=apps/app-b",
"sync:b": "npm run cap:sync --workspace=apps/app-b"
}
}
3. 各アプリ(子プロジェクト)の設定
それぞれのアプリ(例: apps/app-a)の内部は、完全に独立したCapacitorアプリとして動作させます。
① apps/app-a/package.json の例
バージョン確認方法npx cap --version
{
"name": "app-a",
"version": "1.0.0",
"scripts": {
"build": "vite build",
"cap:sync": "cap sync"
},
"dependencies": {
"@capacitor/core": "^6.0.0"
},
"devDependencies": {
"@capacitor/cli": "^6.0.0"
}
}
② apps/app-a/capacitor.config.ts の例
各アプリの個別設定(App IDやWeb資産の出力先)を記述します。
import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.example.appa',
appName: 'App A',
webDir: 'dist', // 各アプリ内のビルド成果物フォルダを指定
bundledWebRuntime: false
};
export default config;
4. 運用・開発コマンド
モノレポ構成では、すべてのパッケージインストールやビルドをルートから制御できます。
- 全体の依存パッケージをインストールする(ルートで実行)
npm install※依存関係はルートのnode_modulesに集約されるため、ディスク容量を節約できます。 - 特定のアプリ(App A)だけをビルド・同期する
npm run build:a npm run sync:a - 特定のアプリのネイティブIDE(Android Studio / Xcode)を開く
各アプリのディレクトリに移動してコマンドを実行します。cd apps/app-a npx cap open android
5. 注意すべきポイント
- Capacitor CLIの実行:
npx cap syncなどのコマンドは、必ず対象アプリのディレクトリ(apps/app-aなど)に移動してから実行するか、上記のようにworkspaceスクリプト経由で実行してください。ルートで直接叩くとエラーになります。 - プラグインの競合: ネイティブプラグイン(Push通知やカメラなど)は各アプリの
package.jsonに個別にインストールしますが、モノレポの仕組み上、一部のプラグインがAndroid/iOSの自動リンクで干渉することがあります。その場合は、各アプリフォルダに独立したnode_modulesを強制生成する設定(pnpmのshamefully-hoistなど)への移行を検討してください。
“cap:sync”: “cap sync”
この記述があるとどうなりますか?
この記述があると、「各アプリのディレクトリに移動した状態で、npm run cap:sync という短いコマンドを入力するだけで、Capacitorの同期処理が実行できる」ようになります。
具体的な仕組みとメリットは以下の3点です。
1. npx を省略して実行できる
本来、Capacitorのコマンドを実行するには npx cap sync と入力する必要があります。
この記述を package.json の scripts に登録しておくことで、npm run cap:sync で呼び出せるようになります。
2. ルート(親)から子アプリを操作しやすくなる(モノレポの最大のメリット)
モノレポ構成(npm workspaces)では、ルートディレクトリにいながら、特定の子アプリのスクリプトを呼び出す機能があります。
子アプリ(app-a)の package.json にこの記述があることで、ルートディレクトリから一歩も移動せずに以下のコマンドで同期が可能になります。
# ルート(親)にいる状態で実行
npm run cap:sync --workspace=apps/app-a
もし子アプリ側にこの記述がないと、ルートから直接Capacitorを同期させることが難しくなり、毎回 cd apps/app-a で移動してから npx cap sync を叩く手間が発生します。
3. 他のビルドコマンドと連携(自動化)しやすくなる
アプリのWebコード(ViteやReactなど)をビルドした後に、自動でCapacitorへ同期させたい場合、コマンドを連続して実行(チェーン)できるようになります。
例えば、子アプリの package.json を以下のように書き換えることで、「Webのビルドとネイティブへの同期」を1つのコマンドにまとめることも可能になります。
"scripts": {
"build": "vite build",
"cap:sync": "cap sync",
"deploy": "npm run build && npm run cap:sync"
}
※ npm run deploy を実行するだけで、ビルドからCapacitorの同期までが自動で一気に完了します。
この設定は、これからの複数アプリ開発の効率を大きく左右する便利な「ショートカット登録」のようなものです。
もしよろしければ、現在開発を想定しているフロントエンドのフレームワーク(Next.js、Vite+React、Ionicなど)を教えていただけますか?それに応じた最適なスクリプトの組み合わせをご提案いたします。
iOS版を作成する
URLを指定してゲーム化しているページをそのままアプリにする(ラップする)。(存在しないページを指定するとただの白いページになる)
環境準備(Mac必須)
必要なもの:
- Node.js
- Xcode
プロジェクト作成
ターミナルで👇
|
1 2 3 4 5 |
mkdir my-game-app cd my-game-app npm init -y npm install @capacitor/core @capacitor/cli npx cap init |
設定を聞かれるので:
- App name → ゲーム名
- App ID → com.example.game(適当でOK)
Webゲームを組み込む
方法
WordPressのURLをそのまま使う👇
|
1 2 3 4 5 6 7 8 9 10 |
// capacitor.config.json { "appId": "com.example.game", "appName": "MyGame", "webDir": "public", "server": { "url": "https://あなたのサイトURL", "cleartext": false } } |
👉 これで「WebViewでサイト表示」になる
“webDir”: “public”, のカンマの追加を忘れないこと
|
1 2 3 4 |
mkdir public npm install @capacitor/ios npx cap add ios npx cap open ios |
Xcode側の設定(重要)
やること👇
① Team設定
- Signing & Capabilities
- Apple ID選択
② アイコン設定
- AppIconに画像入れる
③ フルスクリーン設定
Safariっぽさ消す👇
|
1 2 |
<key>UIViewControllerBasedStatusBarAppearance</key> <false/> |
④ HTTP対策(必要なら)
httpsじゃない場合:
|
1 2 3 4 5 |
<key>NSAppTransportSecurity</key> <dict> <key>NSAllowsArbitraryLoads</key> <true/> </dict> |
※本番はHTTPS必須
注意点
❌ iOS審査落ち
Appleはこれ見ます👇
- 「ただのWebViewアプリ」NGになりやすい
- 最低限:
👉 例:
- ローディング画面追加
- オフライン時の表示
- メニューUI改善
ファイルからビルドする
やりたいこと
👉 Webゲームをそのままローカルに置いてビルド
つまり👇
- ❌ URL読み込み(server.url)
- ✅ HTML/JS/CSSをアプリに同梱
UIやゲーム本体は自分で書いてAPIだけWPのものを使う感じ
コマンド
|
1 2 3 4 5 6 7 8 9 10 11 12 |
# Capacitorのインストール npm install @capacitor/core @capacitor/cli # 初期化(アプリ名とApp IDを設定) npx cap init # 各プラットフォームのインストール npm install @capacitor/android @capacitor/ios # プロジェクトの追加 npx cap add android npx cap add ios |
- Webビルド:
npm run buildなどで最新のWebファイルを生成。 - 同期:
npx cap syncを実行し、Web資産をネイティブ側に反映。 - 実行:
npx cap open androidまたはnpx cap open iosで Android Studio や Xcode を開き、そこから実機やエミュレータで起動します。
■ 全体の完成イメージ
|
1 2 3 4 5 6 7 |
my-game-app/ ├── capacitor.config.json ├── public/ ← ★ここにゲーム全部入れる │ ├── index.html │ ├── js/ │ ├── css/ │ └── assets/ |
① server設定を消す(重要)
|
1 2 3 4 |
"server": { "url": "https://...", "cleartext": false } |
👉 これ削除
② webDirを確認
|
1 |
"webDir": "public" |
👉 publicフォルダがルートになる
③ ファイル配置
最重要👇
👉 必ず index.html を置く
例:
|
1 2 3 4 |
public/ ├── index.html ├── style.css ├── game.js |
注意点
index.html修正(ここ超重要)
❌ ダメ(よくあるミス)
|
1 2 |
<script src="/js/game.js"></script> <link rel="stylesheet" href="/css/style.css"> |
👉 / から始まると動かない
Capacitor同期
|
1 2 3 |
npx cap sync npx cap open iOS // ビルド |
■ よくあるハマりポイント⚠️
① 画面真っ白
原因ほぼこれ👇
- パスミス
- JS読み込めてない
👉 Xcodeのコンソールでエラー確認
② WordPressのまま使ってる
👉 PHPは動かない
対策👇
- APIとして使う(fetch)
- または静的化する
③ 画像が出ない
👉 パス or 大文字小文字ミス
④ fetch先がブロックされる
👉 CORS問題
Capacitor準備が終わってXcode起動までメモ
名前にハイフンやアンダーバーを入れない方が良い、IDにアンダーバーとか入れるとエラーキャメルとかが安定か?
# 初期化(アプリ名とApp IDを設定)
npx cap init
[?] What is the web asset directory for your app?
This directory should contain the final index.html of your app.
にはフォルダのパスを入れる
npm install @capacitor/ios
npx cap add ios
npx cap open ios
出来上がったら次からはターミナルで該当アプリのiOSフォルダがあるところに移動し【npx cap open ios】を打ち込めばOK
同期コマンド【npx cap sync】
コードの編集
Capacitorの性質が元フォルダを同期させてIDEに送るので元ファイルを編集する必要がある。(npx cap syncすると元ファイルの状態にリセットされる)
そのためWebで使っているフォルダがある場合、別途コピーして置く必要がある。
IDE側で作成されるファイル(Swiftファイルとか)はそちらで編集してもリセットされない。
CapacitorとSwiftの連携方法
以下はCapacitorのバージョンやプロジェクト構成に寄り異なるようです。
Capacitorでは、
JavaScript ⇄ Capacitor Plugin / Bridge ⇄ Swift
という橋を作って通信します。
また、重要なのは、Capacitorで表示しているHTML/JavaScript部分は基本的にSwiftUIのViewではありません。
Xcode側にはWKWebViewがあり、そこにHTML/JSが表示されています。
全体像
現在の構成は、おそらくこんな感じです。
HTML
├─ index.html
├─ game.js
└─ style.css
↓
Capacitor
↓
WKWebView
↓
iOS App
↓
Xcode / Swift
ここにSwift側との通信を追加すると、
JavaScript
↓
Capacitor Bridge
↓
Swift Plugin
↓
Swiftの処理
↓
結果をJavaScriptへ返す
という構造になります。
SwiftUIはどこに入るのか?
ここが一番重要です。
例えば、
- HTMLゲームを表示する
- JavaScriptからiOSの機能を呼ぶ
- JavaScriptからSwiftの処理を呼ぶ
だけなら、SwiftUIは必須ではありません。
SwiftUIは、
ContentView.swift
などで
var body: some View {
...
}
とUIを作るためのものです。
一方、CapacitorのJavaScriptと通信する部分は、
SwiftUI Viewではなく、普通のSwiftコード / Capacitor Plugin
として作るのが基本です。
例えばこういうことができます
例えばHTML側に
console.log("iOSの処理を呼びたい");
があるとして、
JavaScriptから
NativeAPI.echo({
message: "こんにちは"
});
のようにSwiftを呼びます。
するとSwift側で、
func echo(...)
を実行して、
こんにちは
をJavaScriptへ返せます。
つまり、
JavaScript
↓
「Swiftさん、この処理して」
↓
Swift
↓
「結果はこれです」
↓
JavaScript
という通信です。
Capacitor Pluginを作る
例えば、
ios/
側にSwiftのPluginを作ります。
概念的にはこうです。
import Foundation
import Capacitor
@objc(MyNativePlugin)
public class MyNativePlugin: CAPPlugin {
@objc func echo(_ call: CAPPluginCall) {
let message = call.getString("message") ?? ""
print("Swift側で受信: \(message)")
call.resolve([
"message": message
])
}
}
これがSwift側です。
JavaScript側から呼ぶ
JavaScriptでは、
import { registerPlugin } from '@capacitor/core';
const MyNativePlugin = registerPlugin('MyNativePlugin');
async function testNative() {
const result = await MyNativePlugin.echo({
message: 'JavaScriptからこんにちは'
});
console.log(result.message);
}
という感じになります。
すると、
JavaScript
↓
MyNativePlugin.echo()
↓
Swiftのecho()
↓
call.resolve()
↓
JavaScript
という流れになります。
ただし、実際にはもう少し重要なポイントがあります
Capacitorには大きく2種類の考え方があります。
① 既存のCapacitor Pluginを使う
例えば、
Camera
Filesystem
Preferences
Device
Network
App
Browser
Haptics
など。
この場合はSwiftを書く必要がほとんどありません。
JavaScriptから、
import { Device } from '@capacitor/device';
const info = await Device.getInfo();
console.log(info.platform);
のように使えます。
② 自分専用のSwift処理を作る
例えばあなたのゲームで、
JavaScript
↓
Swift
↓
UserDefaults
とか、
JavaScript
↓
Swift
↓
Keychain
とか、
JavaScript
↓
Swift
↓
StoreKit
とか、
JavaScript
↓
Swift
↓
AdMob
などをしたい場合です。
この場合は独自Capacitor Pluginを作ります。
SwiftUIを使いたい場合
ここは少し話が変わります。
例えば、
HTMLゲーム
JavaScript
でゲームを表示している。
そこから、
「設定」
を押したら、
SwiftUIで作った設定画面
を表示したい。
これは可能です。
構造としては、
HTML / JavaScript
↓
Capacitor Plugin
↓
Swift
↓
SwiftUI View
となります。
つまりSwiftUIを使うこと自体はできます。
ただし、
JavaScriptの中にSwiftUIを書くことはありません。
例えば
JavaScriptから
await NativeUI.openSettings();
と呼ぶ。
Swift側で、
@objc func openSettings(_ call: CAPPluginCall) {
// SwiftUI画面を表示
}
として、
struct SettingsView: View {
var body: some View {
VStack {
Text("設定")
}
}
}
を表示する、という構成です。
つまり、ファイルの役割はこう考えると分かりやすいです
現在のあなたのプロジェクトなら、
www/
├─ index.html
├─ game.js
└─ style.css
ここは、
Web側
です。
そして、
ios/
└─ App/
├─ AppDelegate.swift
├─ ...
└─ MyNativePlugin.swift
などが、
iOSネイティブ側
です。
さらに必要なら、
SettingsView.swift
のようなSwiftUI Viewを追加します。
一番おすすめの構成
あなたのように、
HTML + JavaScriptで既にゲームが完成していて、CapacitorでiOSアプリ化している
なら、最初はこの構成が非常に分かりやすいです。
【Web】
HTML
CSS
JavaScript
│
│ Capacitor
↓
【Native】
MyPlugin.swift
│
├─ iOS API
├─ StoreKit
├─ UserDefaults
├─ Keychain
└─ その他
SwiftUIが必要になった場合だけ、
MyPlugin.swift
↓
SettingsView.swift
という形でSwiftUIを追加します。
例えば「JavaScriptからSwiftを呼びたい」だけなら
実際のコードは、
game.js
↓
registerPlugin()
↓
MyPlugin.swift
↓
call.resolve()
↓
game.js
という形になります。
そして逆方向も可能です
SwiftからJavaScriptへ通知することもできます。
例えばSwift側で、
「購入が完了した」
と分かったら、
Swift
↓
JavaScriptへイベント送信
↓
ゲーム側のUI更新
ということも可能です。
これがCapacitorのかなり重要な部分です。
今のあなたのケースなら
以前の構成を見る限り、あなたは
HTML + JavaScript + CSSのゲームをCapacitorでラップして、XcodeでiOSアプリとして動かしている
状態なので、次に覚えるべきなのは、
JavaScript
↕
Capacitor Plugin
↕
Swift
です。
SwiftUIをJavaScriptに直接入れるのではありません。
そして、SwiftUIは必要になったときに、
JavaScript
↓
Capacitor Plugin
↓
Swift
↓
SwiftUI
バージョン8.3.1での連携方法
質問です。
Swift側
@objc(MyNativePlugin)
public class MyNativePlugin: CAPPlugin {}
のMyNativePluginは合わせる必要がありますか?
もし合わせる必要がない場合はどっちの記述名がJS側、プラグイン登録等で必要になりますか?
はい、合わせる必要があります。ただし、Capacitorでは「名前が3か所ある」ので、そこを分けて考えると分かりやすいです。
Capacitor 8.5.0 では、今回の例なら基本的に次の名前を同じにしておくのが一番安全です。
@objc(MyNativePlugin)
public class MyNativePlugin: CAPPlugin {
...
}
ここで出てくる MyNativePlugin には、実は役割が違うものがあります。
① Swiftのクラス名
public class MyNativePlugin: CAPPlugin
これはSwiftのクラス名です。
たとえば、
public class WordPressPlugin: CAPPlugin
ならSwift側のクラス名は WordPressPlugin です。
② @objc(...) の名前
@objc(MyNativePlugin)
こちらはObjective-Cランタイム上での名前です。
CapacitorのPluginとして公開する際に使われます。
したがって、通常は、
@objc(MyNativePlugin)
public class MyNativePlugin: CAPPlugin
のように同じ名前にします。
別名にすることも技術的には可能ですが、混乱するだけなので、最初は同じ名前にするのがおすすめです。
例えば、
@objc(MyNativePlugin)
public class MyNativePlugin: CAPPlugin
なら、
Swiftクラス名 MyNativePlugin
Objective-C名 MyNativePlugin
です。
③ JavaScript側のPlugin名
ここが一番重要です。
JavaScriptで、
const MyNativePlugin = registerPlugin('MyNativePlugin');
と書いた場合、
'MyNativePlugin'
がPluginの名前です。
この名前がネイティブ側のPluginとして認識される名前になります。
そのため、今回の最小構成なら、
Swift
@objc(MyNativePlugin)
class MyNativePlugin
↓
JavaScript
registerPlugin('MyNativePlugin')
と揃える、と考えると分かりやすいです。
ただし、JavaScriptの変数名は別物です
ここは非常に重要です。
例えばJavaScriptで、
const NativeAPI = registerPlugin('MyNativePlugin');
と書いても構いません。
この場合、
NativeAPI
は単なるJavaScript側の変数名です。
一方、
registerPlugin('MyNativePlugin')
の
MyNativePlugin
がPlugin名です。
つまり、
const NativeAPI = registerPlugin('MyNativePlugin');
は、
JavaScript変数名 → NativeAPI
Plugin名 → MyNativePlugin
です。
なので、こんなこともできます。
const hoge = registerPlugin('MyNativePlugin');
そして、
const result = await hoge.hello({
message: 'こんにちは'
});
でも動く考え方です。
ただし、非常に分かりにくいので、
const MyNativePlugin = registerPlugin('MyNativePlugin');
とするのが自然です。
さらに「メソッド名」も別です
Swift側に、
@objc func hello(_ call: CAPPluginCall)
と書いた場合、
hello
がJavaScriptから呼ぶメソッド名になります。
JavaScript側は、
await MyNativePlugin.hello(...)
です。
したがって全体では、
Swift
────────────────────
@objc(MyNativePlugin)
public class MyNativePlugin: CAPPlugin {
@objc func hello(_ call: CAPPluginCall) {
...
}
}
↓
JavaScript
────────────────────
const MyNativePlugin =
registerPlugin('MyNativePlugin');
await MyNativePlugin.hello({
message: 'こんにちは'
});
という関係です。
4つに分解すると分かりやすいです
| 記述 | 役割 | 今回 |
|---|---|---|
class MyNativePlugin | Swiftクラス名 | MyNativePlugin |
@objc(MyNativePlugin) | Objective-C上の名前 | MyNativePlugin |
registerPlugin('MyNativePlugin') | CapacitorのPlugin名 | MyNativePlugin |
MyNativePlugin.hello() | JS側から呼ぶメソッド | hello |
そして、
const MyNativePlugin = ...
の左側の MyNativePlugin はJavaScriptの変数名なので、実は自由です。
なので今回なら、この形にします
Swift
import Foundation
import Capacitor
@objc(MyNativePlugin)
public class MyNativePlugin: CAPPlugin {
@objc func hello(_ call: CAPPluginCall) {
let message = call.getString("message") ?? ""
print("Swift側で受信: \(message)")
call.resolve([
"message": "Swiftから返事です。受信内容: \(message)"
])
}
}
JavaScript
import { registerPlugin } from '@capacitor/core';
const MyNativePlugin = registerPlugin('MyNativePlugin');
async function testSwift() {
try {
const result = await MyNativePlugin.hello({
message: 'JavaScriptからこんにちは!'
});
console.log('Swiftからの返事:', result.message);
} catch (error) {
console.error('Swift呼び出しエラー:', error);
}
}
そして今回のCapacitor 8.5.0について
今回、
Capacitor CLI 8.3.1
@capacitor/core 8.5.0
@capacitor/ios 8.5.0
という構成ですね。
ここで一点訂正があります。先ほど私が説明した「AppDelegate.swift に登録する」という部分は、Capacitor 8では、そのまま採用する前提で進めない方がいいです。Capacitor 8のPlugin登録方法に合わせて構築する必要があります。
今回のように独自Swift Pluginを作る場合は、Pluginの登録部分まで正しく設定しないと、JavaScriptから registerPlugin() してもSwiftへ到達しません。
なので次は、
MyNativePlugin.swift
+
Plugin登録
+
game.js
をCapacitor 8.5.0に合わせた正式な構成で作るのがよいです。
Capacitor 8公式ドキュメントでは、アプリ内のカスタムSwiftコードは CAPBridgedPlugin を実装したSwiftクラスを作り、CAPBridgeViewController の capacitorDidLoad() で registerPluginInstance() する方式が案内されています。JavaScript側の registerPlugin() に渡す名前はSwift側の jsName と一致させます。
今回作る最終形はこれです。
JavaScript
│
│ Echo.echo(...)
▼
Capacitor Bridge
│
▼
EchoPlugin.swift
│
│ Swiftで処理
▼
JavaScriptへ結果を返す
1. Xcodeで EchoPlugin.swift を作る
Xcode左側のプロジェクトナビゲータで、
App
を右クリックします。
New File...
→
Swift File
→ ファイル名を
EchoPlugin.swift
にします。
公式ドキュメントでもこの方法で EchoPlugin.swift を作る手順になっています。
中身を全部これにします。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import Capacitor @objc(EchoPlugin) public class EchoPlugin: CAPPlugin, CAPBridgedPlugin { public let identifier = "EchoPlugin" public let jsName = "Echo" public let pluginMethods: [CAPPluginMethod] = [ CAPPluginMethod( name: "echo", returnType: CAPPluginReturnPromise ) ] @objc func echo(_ call: CAPPluginCall) { let value = call.getString("value") ?? "" print("========== EchoPlugin ==========") print("JavaScriptから受信: \(value)") print("================================") call.resolve([ "value": "Swiftから返事: \(value)" ]) } } |
ここで名前を整理すると
@objc(EchoPlugin)
public class EchoPlugin
はSwift側のクラスです。
public let identifier = "EchoPlugin"
はPluginの識別子。
public let jsName = "Echo"
がJavaScriptから登録するときの名前です。
そして、
CAPPluginMethod(name: "echo", ...)
の echo がJavaScriptから呼ぶメソッド名です。
つまり今回の対応関係は、
Swiftクラス
EchoPlugin
identifier
EchoPlugin
JavaScript用Plugin名
Echo
JavaScriptから呼ぶメソッド
echo
です。
公式例もこの構成になっています。
2. なぜ CAPBridgedPlugin が必要なのか
ここは先ほどの回答からの重要な修正点です。
以前の説明では、
public class MyNativePlugin: CAPPlugin
だけを例にしていました。
Capacitor 8では今回のカスタムPluginの場合、これではなく
public class EchoPlugin: CAPPlugin, CAPBridgedPlugin
とします。
そして、
identifier
jsName
pluginMethods
を定義します。
これが現在の公式の方式です。(Capacitor)
また、@objc はCapacitorの動的なPlugin呼び出しに必要です。(Capacitor)
3. 次にPlugin登録用のViewControllerを作る
ここが今回の一番重要なところです。
Xcodeで再び、
App
を右クリック。
New File...
↓
Swift File
ファイル名:
MyViewController.swift
にします。
中身:
|
1 2 3 4 5 6 7 8 9 |
import UIKit import Capacitor class MyViewController: CAPBridgeViewController { override open func capacitorDidLoad() { bridge?.registerPluginInstance(EchoPlugin()) } } |
これで、
CapacitorのBridgeがロードされた
↓
EchoPluginを登録
という処理になります。
公式ドキュメントでも、
override open func capacitorDidLoad() {
bridge?.registerPluginInstance(EchoPlugin())
}
という方式が指定されています。
4. ここで非常に重要
MyViewController.swift を作っただけではダメです。
現在のアプリが使用している
CAPBridgeViewController
を、
MyViewController
に変更する必要があります。
ここはプロジェクトの作り方によって設定箇所が違います。
Capacitorアプリでは、通常Storyboard側でBridge View Controllerを指定しています。
Xcode左側で、
App
の中にある
Main.storyboard
またはStoryboard相当のUIファイルを探してください。
開くと、
CAPBridgeViewController
が配置されているはずです。
そこを選択して、右側の
Identity Inspector
を開きます。
プルダウン選択ではなく直接打ち込む


Custom Class のところを、
CAPBridgeViewController
から
MyViewController
に変更します。
イメージとして、
Before
CAPBridgeViewController
↑
Main.storyboard
After
MyViewController
↑
Main.storyboard
↑
CAPBridgeViewControllerを継承
です。
5. JavaScript側を作る
HTML側のJavaScriptの読み込み方による
ここからWeb側です。
あなたの現在の構成が、
www/
index.html
game.js
style.css
なら、まず game.js に以下を追加します。
import { registerPlugin } from '@capacitor/core';
const Echo = registerPlugin('Echo');
async function testSwift() {
try {
console.log('JavaScript → Swift 開始');
const result = await Echo.echo({
value: 'JavaScriptからこんにちは!'
});
console.log('Swift → JavaScript 成功');
console.log('Swiftからの返事:', result.value);
} catch (error) {
console.error('Swift呼び出しエラー:', error);
}
}
公式ドキュメントでも、
const Echo = registerPlugin('Echo');
としており、ここで使う "Echo" はSwift側の
public let jsName = "Echo"
と一致させる必要があります。
6. HTMLからテストする
index.html に、
<button onclick="testSwift()">
Swiftを呼ぶ
</button>
を追加します。
ただし、ここで一つ注意があります。
あなたの既存ゲームが普通の、
<script src="game.js"></script>
でJavaScriptを読み込んでいる場合、
import { registerPlugin } from '@capacitor/core';
を使うにはES Moduleとして読み込む必要があります。
つまり、
<script type="module" src="game.js"></script>
に変更する必要があります。
ただ、ここはあなたの既存ゲームのJavaScript構成を崩す可能性があります。
なので、実際の index.html を見てから変更するのが安全です。
7. JavaScript → Swift → JavaScript
ここまでできれば、流れはこうなります。
ボタンを押す。
testSwift()
↓
Echo.echo({
value: 'JavaScriptからこんにちは!'
})
↓
Capacitor Bridge
↓
Swift:
@objc func echo(_ call: CAPPluginCall)
↓
let value = call.getString("value")
↓
Swiftで、
call.resolve([
"value": "Swiftから返事: \(value)"
])
↓
JavaScriptに戻る。
result.value
↓
Swiftから返事: JavaScriptからこんにちは!
となります。
これはまさに、
JavaScript
↓
Swift
↓
JavaScript
です。
8. Xcodeで確認する
Xcodeから実機またはSimulatorで実行します。
その後、SafariのWeb InspectorなどでWebViewのコンソールを見ると、
JavaScript → Swift 開始
Swift → JavaScript 成功
Swiftからの返事: Swiftから返事: JavaScriptからこんにちは!
となるはずです。
さらにXcode側のコンソールには、
========== EchoPlugin ==========
JavaScriptから受信: JavaScriptからこんにちは!
================================
が出ます。
これなら完全に通信できています。
9. ファイル構成は最終的にこうなります
あなたのプロジェクト
│
├─ www/
│ ├─ index.html
│ ├─ game.js
│ └─ style.css
│
├─ capacitor.config.json
│
└─ ios/
└─ App/
│
├─ AppDelegate.swift
│
├─ EchoPlugin.swift ← 今回追加
│
├─ MyViewController.swift ← 今回追加
│
└─ Main.storyboard ← MyViewControllerを指定
この構造です。
10. 名前の関係をもう一度
今回、非常に重要なのでここだけ整理します。
Swift
@objc(EchoPlugin)
public class EchoPlugin: CAPPlugin, CAPBridgedPlugin {
Swift Plugin名
public let jsName = "Echo"
JavaScript
const Echo = registerPlugin('Echo');
呼び出し
Echo.echo(...)
つまり、
@objc(EchoPlugin)
↓
Swiftクラス
jsName = "Echo"
↓
JavaScript Plugin名
registerPlugin("Echo")
↓
JavaScript側の接続
Echo.echo()
↓
Swiftの echo()
です。
11. identifier と jsName は同じでなくていい
ここも前の質問につながります。
例えば、
public let identifier = "MyAwesomeNativePlugin"
public let jsName = "Echo"
でも構いません。
この場合、
registerPlugin('Echo')
です。
JavaScriptが合わせるべきなのは、
identifier ではなく jsName
です。
Capacitor公式の例でも、
identifier = "EchoPlugin"
jsName = "Echo"
と別名になっています。
これは前の質問への正確な回答にもなります。
12. 最初はこの名前で固定するのがおすすめ
今回は混乱を避けるため、
Swift class EchoPlugin
identifier EchoPlugin
jsName Echo
JS variable Echo
method echo
で進めるのがよいです。
HTMLのJSの読み込み方が<script src="ふぁいるめい.js"></script>の場合のJSコード(import不要)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// ======================================== // Capacitor → Swift テスト // ======================================== async function testSwiftBridge() { try { console.log('[JS] Swift呼び出し開始'); const result = await window.Capacitor.Plugins.Echo.echo({ value: 'JavaScriptからこんにちは!' }); console.log('[JS] Swiftから返事:', result.value); } catch (error) { console.error('[JS] Swift呼び出し失敗:', error); } } // 起動時に1回だけテスト testSwiftBridge(); |
APIの取得コードの例
元コード
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
async fetchSingleAds() { try { const res = await fetch(AdCanvas.SINGLE_ADS_URL); if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`); this.singleAdsData = await res.json(); return this.singleAdsData; } catch (err) { console.error('[AdCanvas] fetchSingleAds 失敗:', err); throw err; } } |
修正後
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
async fetchSingleAds() { try { console.log('[AdCanvas] fetchSingleAds 開始'); const response = await window.Capacitor.Plugins.CapacitorHttp.get({ url: AdCanvas.SINGLE_ADS_URL }); console.log('[AdCanvas] CapacitorHttp response:', response); console.log('[AdCanvas] status:', response.status); console.log('[AdCanvas] data:', response.data); if (response.status < 200 || response.status >= 300) { throw new Error( `HTTP ${response.status}` ); } this.singleAdsData = response.data; console.log('[AdCanvas] fetchSingleAds 成功:', this.singleAdsData); return this.singleAdsData; } catch (err) { console.error('[AdCanvas] fetchSingleAds 失敗:', err); throw err; } } |
画像ダウンロードのコード例、画像がpublic内にある場合
iOSの共有にダウンロード
FilesystemとShareを追加
プロジェクトのルートで、
npm install @capacitor/filesystem @capacitor/sharenpx cap sync ios
を実行します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
document.getElementById('modalDownload').addEventListener('click', async () => { nyankoPlaySE("decisionSound"); filename = `${cat.rarity}_${cat.name}.png`; try { console.log('[Download] 開始:', cat.img); // 画像を取得 const response = await fetch(cat.img); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } // Blob化 const blob = await response.blob(); // Blob → Base64 const base64 = await blobToBase64(blob); console.log('[Download] Base64変換成功'); // Capacitor Filesystem const fileResult = await window.Capacitor.Plugins.Filesystem.writeFile({ path: filename, data: base64, directory: 'DOCUMENTS', recursive: true }); console.log('[Download] 保存成功:', fileResult); // 保存したファイルを共有シートで表示 await window.Capacitor.Plugins.Share.share({ title: filename, url: fileResult.uri }); console.log('[Download] 共有シート表示完了'); } catch (e) { console.error('[Download] 失敗:', e); // 最終フォールバック window.open(cat.img, '_blank'); } }); async function blobToBase64(blob) { return await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => { // data:image/png;base64,XXXX // ↓ // XXXX だけ取り出す const result = reader.result; if (typeof result !== 'string') { reject(new Error('Base64変換に失敗しました')); return; } resolve(result.split(',')[1]); }; reader.onerror = reject; reader.readAsDataURL(blob); }); } |
iOSの写真にダウンロード
Media Pluginを追加
プロジェクトのルートで実行してください。
npm install @capacitor-community/medianpx cap sync ios
このPluginはiOS/Androidの写真・動画保存用です。savePhoto() がカメラロール保存を担当します。
iOSの写真アクセス許可を追加
Xcodeで、
App└─ Info
または Info.plist を開きます。
Privacy - Photo Library Usage Description
を追加して、例えば、
写真を保存するために写真ライブラリへのアクセスを使用します。
としてください。
このPluginのiOS設定では NSPhotoLibraryUsageDescription が必要です。
コード
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
document.getElementById('modalDownload').addEventListener('click', async () => { nyankoPlaySE("decisionSound"); filename = `${cat.rarity}_${cat.name}.png`; try { console.log('[Download] 開始:', cat.img); // 画像取得 const response = await fetch(cat.img); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const blob = await response.blob(); // Blob → Base64 const base64 = await blobToBase64(blob); console.log('[Download] Base64変換成功'); // Base64 → data URL const dataUrl = `data:image/png;base64,${base64}`; // iPhoneの写真アプリへ保存 const result = await window.Capacitor.Plugins.Media.savePhoto({ path: dataUrl }); console.log('[Download] 写真アプリへの保存成功:', result); } catch (error) { console.error('[Download] 失敗:', error); console.error('[Download] error JSON:', JSON.stringify(error)); } }); async function blobToBase64(blob) { return await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => { const result = reader.result; if (typeof result !== 'string') { reject(new Error('Base64変換に失敗しました')); return; } resolve(result.split(',')[1]); }; reader.onerror = reject; reader.readAsDataURL(blob); }); } |
inputにフォーカスするとズームされる問題
iOSのSafari / WKWebViewで、入力欄の文字サイズが小さいと自動的にページをズームする挙動によるものです。
今回の
<input type="text" id="display-name" maxlength="10" placeholder="名前(10文字以内)">
なら、まずCSSで font-size: 16px 以上にするのが一番簡単で確実です。
#display-name {
font-size: 16px;
}
たとえば既存のスタイルに、
#display-name {
width: 100%;
box-sizing: border-box;
font-size: 16px;
}
と追加してください。
なぜ16pxなのか
iOSのWebViewでは、input の文字サイズが16px未満だと、入力時に「文字を見やすくするため」と考えられる自動ズームが発生することがあります。
そのため、
font-size: 14px;
などになっていた場合、
font-size: 16px;
に変更するだけで、今回のズーム問題が解消する可能性が高いです。
もし 16px にしてもズームする場合
HTMLの <head> に viewport 設定があるか確認してください。
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
特に、
maximum-scale=1.0
user-scalable=no
を追加すると、ユーザーによるピンチズームも無効化できます。
ただし、アクセシビリティの観点ではユーザーのズーム操作を完全に禁止する方法はあまり推奨されません。
今回のような「入力欄をタップした瞬間だけ勝手にズームされる」という問題なら、まずは
#display-name {
font-size: 16px;
}
を試すのがおすすめです。
もう一つ注意点があります
今回のゲームはCapacitorでiOSアプリ化しているので、CSS側だけではなく、キーボード表示時のWebViewのリサイズによって「画面全体がズレたように見える」場合もあります。
つまり、
① 入力欄フォーカス → 自動ズーム
と
② キーボード表示 → WebViewの表示領域が変化
は別問題です。
Windows導入方法
Windows 11環境に Android Studio と Capacitor を導入し、Androidアプリの開発環境を構築する手順です。
1. 前提条件のインストール
Capacitorの動作には Node.js が必須です。
- Node.jsの導入: Node.js 公式サイト から LTS(推奨版) をダウンロードしてインストールします。
- 確認方法: コマンドプロンプトを開き、以下を実行してバージョンが表示されればOKです。
node -v
npm -v
2. Android Studio のインストールと設定
ダウンロード: Android Studio 公式サイト から Windows 用インストーラーをダウンロードします。
セットアップ: インストーラーを起動し、すべてデフォルト設定のまま進めて完了させます。
SDKの準備: 初回起動時のセットアップウィザードに従い、Android SDK や Android Virtual Device(エミュレータ) をインストールします。
環境変数の設定 (重要):
Windowsのスタートメニューで「環境変数」を検索し、「システム環境変数の編集」を開きます。
「環境変数」ボタンをクリックします。
「ユーザー環境変数」の「新規」から以下を追加します。
変数名: ANDROID_HOME
変数値: C:\Users\ユーザー名\AppData\Local\Android\Sdk(※ユーザー名はご自身のWindowsアカウント名)
「ユーザー環境変数」または「システム環境変数」の Path を選択して「編集」をクリックし、末尾に以下を追記します。
C:\Users\ユーザー名\AppData\Local\Android\Sdk\platform-tools
3. Capacitor のプロジェクトへの導入
既存、または新規のWebアプリプロジェクト(React, Vue, Angular, Vanilla JSなど)にCapacitorを組み込みます。
プロジェクトの作成・移動:
コマンドプロンプトでプロジェクトのルートフォルダに移動します。
Capacitorのインストール:
npm install @capacitor/core @capacitor/cli
Capacitorの初期化:
- アプリ名とパッケージID(例:
com.example.myapp)を設定します。
npx cap init
Androidプラットフォームの追加:
npm install @capacitor/android
npx cap add android
4. ビルドと実行
Webアプリのビルド:
- お使いのフレームワークに応じて、Web資産(
distやbuildフォルダ)を出力します。
npm run build
Capacitorとの同期:
- Webアプリの成果物をAndroidプロジェクトにコピーします。
npx cap sync
Android Studio で開く / 実行:
- 以下のコマンドを実行すると、自動的にAndroid Studioが起動してプロジェクトが読み込まれます。
npx cap open android
Android Studioの画面右上にある 再生ボタン(Runボタン) を押すと、エミュレータまたは実機でアプリが起動します