[{"data":1,"prerenderedAt":350},["ShallowReactive",2],{"$f1hn1mxew4jmqe":3},{"sys":4,"total":6,"skip":7,"limit":8,"items":9,"includes":336},{"type":5},"Array",32,20,10,[10,60,92,124,154,184,215,247,277,307],{"metadata":11,"sys":14,"fields":35},{"tags":12,"concepts":13},[],[],{"space":15,"id":20,"type":21,"createdAt":22,"updatedAt":23,"environment":24,"publishedVersion":28,"revision":29,"contentType":30,"locale":34},{"sys":16},{"type":17,"linkType":18,"id":19},"Link","Space","jmfsolylwbqz","6WvdXIRPLsiBb0luuazqXY","Entry","2023-06-01T06:42:28.544Z","2023-08-30T02:38:12.674Z",{"sys":25},{"id":26,"type":17,"linkType":27},"master","Environment",63,3,{"sys":31},{"type":17,"linkType":32,"id":33},"ContentType","blogPost","ja-JP",{"title":36,"slug":37,"category":38,"body":56,"description":57,"legacy":58,"enableAd":59,"enableCommentField":59},"【Nuxt3】SSG時に、サイトマップを動的ルートも含めて全自動で生成する方法","nuxt3-sitemap",{"metadata":39,"sys":42,"fields":53},{"tags":40,"concepts":41},[],[],{"space":43,"id":45,"type":21,"createdAt":46,"updatedAt":46,"environment":47,"publishedVersion":29,"revision":49,"contentType":50,"locale":34},{"sys":44},{"type":17,"linkType":18,"id":19},"5MKCmbhhcqboMXodYszCG6","2021-11-16T05:31:22.875Z",{"sys":48},{"id":26,"type":17,"linkType":27},1,{"sys":51},{"type":17,"linkType":32,"id":52},"category",{"name":54,"slug":55},"役立ち情報","useful","## 経緯\n\nGoogle Search Index用にサイトマップを作りたくなった。サイトマップの生成自体は[sitemap](https:\u002F\u002Fgithub.com\u002Fekalinin\u002Fsitemap.js)パッケージでどうにかなるが、いちいち全部のルートを取得してリストを作るのは面倒臭い。そこで、nitroに備わっているクローラーを使って、サイト内にあるリンクからルート一覧を生成してみる。\n\nnitroのクローラーは、HTMLに含まれる、「\u002F」で始まるhref属性を含むタグを自動的に認識し、リンク先のルートを生成できる。\n\n## 壁\n\nnitroでは、クローラーによって自動的に動的ルートのページが生成される。しかし、クロール後のルート一覧を取得する方法が存在しない。そこで、ページを生成するタイミングでフックを挟んで、変数のリストに追加していくことにする。\n\n## コード\n\nまず、sitemapパッケージをインストールしておく。\n\n```sh\nyarn add sitemap\n```\n\n次に、`nuxt.config.ts`の`defineNuxtConfig`の上に以下を追加する。\n\n```TypeScript\nconst hostname = \"\u003Chostname>\"\nconst routes: string[] = []\n\nexport default defineNuxtConfig({\n...\n```\n\n続いて、`defineNuxtConfig`の中に以下を追加する。\n\n```TypeScript\nexport default defineNuxtConfig({\n...\n  nitro: {\n    hooks: {\n      \"prerender:route\"(route) {\n        routes.push(route.route)\n      },\n      close() {\n        if (routes.length > 0) {\n          const links: SitemapItemLoose[] = routes.map(route => ({\n            url: route,\n          }))\n\n          const stream = new SitemapStream({\n            hostname,\n          })\n\n          return streamToPromise(Readable.from(links).pipe(stream))\n            .then((sm) => {\n              return fs.writeFileSync(\"dist\u002Fsitemap.xml\", sm.toString())\n            })\n        }\n      },\n    },\n  },\n...\n})\n```\n\n9行目の`if (routes.length > 0)`は、これを入れないと`nuxt prepare`(`yarn install`)を実行した際にもこの処理が走ってしまうために入れています。","",false,true,{"metadata":61,"sys":64,"fields":76},{"tags":62,"concepts":63},[],[],{"space":65,"id":67,"type":21,"createdAt":68,"updatedAt":69,"environment":70,"publishedVersion":72,"revision":73,"contentType":74,"locale":34},{"sys":66},{"type":17,"linkType":18,"id":19},"72tkMUeCaysJbvdNLlO9U7","2023-03-22T08:12:26.688Z","2024-02-09T14:14:37.657Z",{"sys":71},{"id":26,"type":17,"linkType":27},81,5,{"sys":75},{"type":17,"linkType":32,"id":33},{"title":77,"slug":78,"category":79,"body":91,"description":57,"legacy":58,"enableAd":59,"enableCommentField":59},"【IntelliJ IDEA + macOS】GPGで署名してgit commitするとエラーが発生する問題の対処法","jetbrains-commit-signing-error-workaround",{"metadata":80,"sys":83,"fields":90},{"tags":81,"concepts":82},[],[],{"space":84,"id":45,"type":21,"createdAt":46,"updatedAt":46,"environment":86,"publishedVersion":29,"revision":49,"contentType":88,"locale":34},{"sys":85},{"type":17,"linkType":18,"id":19},{"sys":87},{"id":26,"type":17,"linkType":27},{"sys":89},{"type":17,"linkType":32,"id":52},{"name":54,"slug":55},"## 問題発生\n\nIntelliJ IDEA (というか全Jetbrains IDEで共通) でGitのコミットを署名する設定を、Macで[これ](https:\u002F\u002Fwww.jetbrains.com\u002Fhelp\u002Fidea\u002F2022.3\u002Fset-up-GPG-commit-signing.html)に従って設定したところ、以下のようなエラーが出てコミットできなくなった。\n\n```\nerror: gpg failed to sign the data\nfatal: failed to write commit object\n```\n\n## 原因\n\nいろいろ調べた結果、パスワード入力用のダイアログの表示に失敗している様子。\n\nGPGではセキュリティーのためか、pinentryというソフトでGUIのパスフレーズ入力ダイアログを出し、コンソールに直接パスフレーズを入力することを避けるようにしている。そのmacOS版であるpinentry-macをインストールすると、`pinentry`と`pinentry-mac`という2種類のコマンドが実行できるようになる。しかし、なんと __`pinentry`の方はmacOSでは動かない。__\n\nデフォルトではこの`pinentry`の方が使用されるようになっているためにエラーが発生している様子。`~\u002F.gnupg\u002Fgpg-agent.conf`でpinentryのパスを変更できるらしいが、なぜか設定しても反映されない。(いろいろ試したものの…)\n\nそこで、シンボリックリンクの作成という力技で対処することにする。\n\n## 対処法\n\nまず、__現在設定されているpinentryのパス__ と、 __`pinentry-mac`のパス__ を調査する。\n\n前者は以下のコマンドで調べられる。\n\n```\n% gpgconf\ngpg:OpenPGP:\u002Fopt\u002Fhomebrew\u002FCellar\u002Fgnupg\u002F2.4.0\u002Fbin\u002Fgpg\n...\npinentry:パスフレーズ入力:\u002Fopt\u002Fhomebrew\u002Fopt\u002Fpinentry\u002Fbin\u002Fpinentry\n```\n\n上の実行結果でいうと、`\u002Fopt\u002Fhomebrew\u002Fopt\u002Fpinentry\u002Fbin\u002Fpinentry`の部分が現在設定されているパス。そして、このファイルを削除する。\n\n```\n% rm \u003C現在設定されているpinentryのパス>\n```\n\n次に、`pinentry-mac`のパスを調べる。\n\n```\n% which pinentry-mac\n\u002Fopt\u002Fhomebrew\u002Fbin\u002Fpinentry-mac\n```\n\n最後に、以下のコマンドでシンボリックリンクを作成する。\n\n```\n% ln -s \u003Cpinentry-macのパス> \u003C現在設定されているpinentryのパス>\n```\n\nこれでコミットできるようになるはず。",{"metadata":93,"sys":96,"fields":108},{"tags":94,"concepts":95},[],[],{"space":97,"id":99,"type":21,"createdAt":100,"updatedAt":101,"environment":102,"publishedVersion":104,"revision":105,"contentType":106,"locale":34},{"sys":98},{"type":17,"linkType":18,"id":19},"2eGx1RCH6BM6XjRKqHtgE0","2023-03-13T15:19:07.171Z","2026-07-15T16:08:45.632Z",{"sys":103},{"id":26,"type":17,"linkType":27},168,6,{"sys":107},{"type":17,"linkType":32,"id":33},{"title":109,"slug":110,"category":111,"body":123,"description":57,"legacy":58,"enableAd":59,"enableCommentField":59},"【Firebase Cloud Functions + Puppeteer】Puppeteerのv19が動かない問題の対処法 (備忘録)","puppeteer-v19-cloud-functions-workaround",{"metadata":112,"sys":115,"fields":122},{"tags":113,"concepts":114},[],[],{"space":116,"id":45,"type":21,"createdAt":46,"updatedAt":46,"environment":118,"publishedVersion":29,"revision":49,"contentType":120,"locale":34},{"sys":117},{"type":17,"linkType":18,"id":19},{"sys":119},{"id":26,"type":17,"linkType":27},{"sys":121},{"type":17,"linkType":32,"id":52},{"name":54,"slug":55},"## 問題発生\n\nOGPのサムネイル生成用に、Puppeteerを使ったCloud Functionsの関数をデプロイしようとした。\n\n開発環境(エミュレーター)では正常に動作するのに、なぜかデプロイすると`Error: could not handle the request`と表示されて動作しなくなる。ログを確認すると、以下のようなエラーが出ていた。\n\n```\nError: Could not find Chromium (rev. 1095492). This can occur if either\n 1. you did not perform an installation before running the script (e.g. `npm install`) or\n 2. your cache path is incorrectly configured (which is: \u002Fwww-data-home\u002F.cache\u002Fpuppeteer).\nFor (2), check out our guide on configuring puppeteer at https:\u002F\u002Fpptr.dev\u002Fguides\u002Fconfiguration.\n```\n\nどうやら「Chromiumどこ…？」というエラーの様子。\n\n## 結論\n\n以下の手順で解決できる。\n\n1. `functions`ディレクトリに`.puppeteerrc.cjs`ファイルを作成し、以下のような中身にする。\n\n```JavaScript\n\u002F**\n * @type {import('puppeteer').Configuration}\n *\u002F\nmodule.exports = {\n  cacheDirectory: require(\"path\").join(__dirname, \".cache\", \"puppeteer\"),\n}\n```\n\n2. `firebase.json`に以下を追記する。(\u003Cspan style=\"color: red\">※JSONではコメントの記述はできないため、`\u002F\u002F`以下は削除してください\u003C\u002Fspan>)\n\n```JSON\n\"functions\": [\n  {\n    \"source\": \"functions\",\n    \"codebase\": \"frontend\",\n    \"ignore\": [\n      \"node_modules\",\n      \".git\",\n      \"firebase-debug.log\",\n      \"firebase-debug.*.log\",\n      \".cache\"  \u002F\u002F \u003C= 追記\n    ],\n    \"predeploy\": [\n      \"npm --prefix \\\"$RESOURCE_DIR\\\" run lint\",\n      \"npm --prefix \\\"$RESOURCE_DIR\\\" run build\"\n    ]\n  }\n],\n```\n\n3. `package.json`を以下のように変更する。(\u003Cspan style=\"color: red\">※JSONではコメントの記述はできないため、`\u002F\u002F`以下は削除してください\u003C\u002Fspan>)\n\n```JSON\n\"scripts\": {\n  \"lint\": \"eslint --ext .js,.ts .\",\n  ...\n  \"postinstall\": \"node node_modules\u002Fpuppeteer\u002Finstall.js\" \u002F\u002F \u003C= 追記\n},\n...\n\"engines\": {\n  \"node\": \"16\"  \u002F\u002F \u003C= 16にする。詳細は下記。\n}\n```\n\n## 解説\n\n調べてみると、Puppeteerのv19で以下のような破壊的変更があった模様。\n\n```\nuse ~\u002F.cache\u002Fpuppeteer for browser downloads\n```\n(https:\u002F\u002Fgithub.com\u002Fpuppeteer\u002Fpuppeteer\u002Freleases\u002Ftag\u002Fv19.0.0)\n\nCloud Functionsは、デプロイ時（`firebase deploy`実行時に裏で動くCloud Build）の環境と実行時の環境が異なります。実行用の環境に移されるパッケージはfunctionsディレクトリであり、ホームディレクトリは対象にならないため、実際の環境でChromiumが存在せずエラーとなっているようです。\n\nそのため、Puppeteerのインストール先をプロジェクト内に変更してやります。これが1.です。\n\nこの状態で、ローカルPCでnode_modulesを削除して`npm install`し直すと、設定したパスに開発端末のOSに合ったChromiumがインストールされます。ここで`firebase deploy`しようとすると、「パッケージが大きすぎるためデプロイできません」のようなエラーが発生します。これは巨大なChromium本体がアップロードの対象となっているためで、これはビルド時にダウンロードされれば良いので除外してやる必要があります。これが2.です。\n\nあとは、Cloud Functionsではなぜかnpm install時にChromiumが自動的にインストールされないので、postinstallでインストールスクリプトを実行してやります。(3.)\n\nまた、Nodeの最新バージョンは18ですが、2023\u002F03\u002F14現在、Node 18用のイメージにPuppeteerの実行に必要なライブラリが含まれていないため、16にします。",{"metadata":125,"sys":128,"fields":138},{"tags":126,"concepts":127},[],[],{"space":129,"id":131,"type":21,"createdAt":132,"updatedAt":132,"environment":133,"publishedVersion":135,"revision":49,"contentType":136,"locale":34},{"sys":130},{"type":17,"linkType":18,"id":19},"3yeoz9uWWMKWzCXZUSuOFH","2023-01-05T15:54:49.325Z",{"sys":134},{"id":26,"type":17,"linkType":27},99,{"sys":137},{"type":17,"linkType":32,"id":33},{"title":139,"slug":140,"category":141,"body":153,"legacy":58},"[Cloudflare Pages Functions + Nuxt3] 作成したFunctionがデプロイされない問題の対処法","cloudflare-pages-functions-nuxt3-function",{"metadata":142,"sys":145,"fields":152},{"tags":143,"concepts":144},[],[],{"space":146,"id":45,"type":21,"createdAt":46,"updatedAt":46,"environment":148,"publishedVersion":29,"revision":49,"contentType":150,"locale":34},{"sys":147},{"type":17,"linkType":18,"id":19},{"sys":149},{"id":26,"type":17,"linkType":27},{"sys":151},{"type":17,"linkType":32,"id":52},{"name":54,"slug":55},"## 経緯\n\nGitHub連携を用いて、Cloudflare Pagesでページを公開し、一部のパスでFunctionが実行されるようにしたかったが、なぜかFunctionsだけデプロイされない。\n\n[公式ドキュメント](https:\u002F\u002Fdevelopers.cloudflare.com\u002Fpages\u002Fplatform\u002Ffunctions\u002Fget-started\u002F)の通り、プロジェクトルートに`functions`ディレクトリを作成し、その中に`api\u002Fshowcase.ts`を作り、`example.com\u002Fapi\u002Fshowcase`でAPIが走るようにしたかった。しかし、そのままデプロイしてもNuxtの静的ページに飛び、Functionsに繋がらない。\n\n## 対処法\n\n`pages functions not recognized`で検索したところ、\u003Chttps:\u002F\u002Fgithub.com\u002Fcloudflare\u002Fwrangler2\u002Fissues\u002F1859>がヒットした。\n\n結論としては、`nuxt.config.ts`に以下を追記する。\n\n```TypeScript\n...\nnitro: {\n  preset: \"node-server\",\n},\n...\n```\n\n\u003Chttps:\u002F\u002Fgithub.com\u002Fcloudflare\u002Fwrangler2\u002Fissues\u002F1859#issuecomment-1269616054>によると、Nitroでは環境に応じて自動的に適切なプリセットが選択され、Cloudflare Pagesでは`cloudflare-pages`が設定されるようになります。本来は`nuxt generate`時に`.output\u002Fpublic`に`\u002Ffunctions`がコピーされるべきですが、Nuxt3の`\u002Fserver`として扱われ、認識できるexportが存在しないことからレンダリング前に`\u002Ffunctions`が空っぽにされ、結果全くデプロイされないようです。(私の認識が正しければ)",{"metadata":155,"sys":158,"fields":168},{"tags":156,"concepts":157},[],[],{"space":159,"id":161,"type":21,"createdAt":162,"updatedAt":162,"environment":163,"publishedVersion":165,"revision":49,"contentType":166,"locale":34},{"sys":160},{"type":17,"linkType":18,"id":19},"5FEAQN1vDsDh9AfXPmowso","2022-11-19T09:16:10.765Z",{"sys":164},{"id":26,"type":17,"linkType":27},29,{"sys":167},{"type":17,"linkType":32,"id":33},{"title":169,"slug":170,"category":171,"body":183,"legacy":58},"[Nuxt 3] IntelliJ IDEA \u002F WebStorm にて、script setupのNuxt系メソッドをインポート不要にする方法","nuxt-3-idea-method",{"metadata":172,"sys":175,"fields":182},{"tags":173,"concepts":174},[],[],{"space":176,"id":45,"type":21,"createdAt":46,"updatedAt":46,"environment":178,"publishedVersion":29,"revision":49,"contentType":180,"locale":34},{"sys":177},{"type":17,"linkType":18,"id":19},{"sys":179},{"id":26,"type":17,"linkType":27},{"sys":181},{"type":17,"linkType":32,"id":52},{"name":54,"slug":55},"## 問題は起きた。\n\nJetbrains社のIDE、IntelliJ IDEA \u002F WebStorm でNuxt3を使おうとすると、`#imports`からインポートしないとメソッドが解決されない。どうやら、VSCodeのプラグインはインポートしなくても解決できるようになっている模様。\n\nこれはめちゃくちゃ面倒臭い。\n\n## 対処法\n\n__imlファイルを編集し、`.nuxt`ディレクトリを処理(インデックス)の対象に含めます__。通常、IntelliJ IDEAでは、「.(ドット)」から始まる隠しディレクトリはGUIでインデックスの対象に含めることができません。なので、プロジェクトの設定ファイルを直接編集します。\n\n`[プロジェクトルート]\u002F.idea\u002F[プロジェクト名].iml`を開きます。そして、以下を追記します。\n\n```\n\u003C?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\u003Cmodule type=\"JAVA_MODULE\" version=\"4\">\n  \u003Ccomponent name=\"NewModuleRootManager\" inherit-compiler-output=\"true\">\n\t  \u003C!-- 略 -->\n    \u003Ccontent url=\"file:\u002F\u002F$MODULE_DIR$\u002F.nuxt\" \u002F> \u003C!-- 追記 -->\n\t\t\u003C!-- 略 -->\n  \u003C\u002Fcomponent>\n\u003C\u002Fmodule>\n```\n\nこうすると、composableなどがimportなしで使えるようになります。",{"metadata":185,"sys":188,"fields":199},{"tags":186,"concepts":187},[],[],{"space":189,"id":191,"type":21,"createdAt":192,"updatedAt":193,"environment":194,"publishedVersion":196,"revision":29,"contentType":197,"locale":34},{"sys":190},{"type":17,"linkType":18,"id":19},"53aoUNshAalcr4a9CQ8ly9","2022-11-03T09:14:55.215Z","2022-11-25T00:51:32.309Z",{"sys":195},{"id":26,"type":17,"linkType":27},11,{"sys":198},{"type":17,"linkType":32,"id":33},{"title":200,"slug":201,"category":202,"body":214,"legacy":58},"【IntelliJ IDEA】macOS 13 Ventura アプデ後に、フルスクリーンで使うと画面が点滅する問題の対処法","idea-ventura-flickering",{"metadata":203,"sys":206,"fields":213},{"tags":204,"concepts":205},[],[],{"space":207,"id":45,"type":21,"createdAt":46,"updatedAt":46,"environment":209,"publishedVersion":29,"revision":49,"contentType":211,"locale":34},{"sys":208},{"type":17,"linkType":18,"id":19},{"sys":210},{"id":26,"type":17,"linkType":27},{"sys":212},{"type":17,"linkType":32,"id":52},{"name":54,"slug":55},"## 問題\n\nタイトルの通り。\n\n## 対処法\n\n「カスタムVMオプションの編集」から、以下のオプションを追加してMetalを利用したレンダリングを無効化します。こうすることで、ファイルを更新する度に起こる点滅が1回に減ります(ゼロにはなりません)。これで暫定的に対処できます。\n\n```\n-Dsun.java2d.metal=false\n```\n\n## 追記 (2022\u002F11\u002F25)\n\nIntelliJ IDEA 2022.2.4がリリースされました。リリースノートには修正した旨の記載がありますが、なぜか現在も治っていない模様です。\n",{"metadata":216,"sys":219,"fields":231},{"tags":217,"concepts":218},[],[],{"space":220,"id":222,"type":21,"createdAt":223,"updatedAt":224,"environment":225,"publishedVersion":227,"revision":228,"contentType":229,"locale":34},{"sys":221},{"type":17,"linkType":18,"id":19},"3phSjuNYJPiHa4Qrw2kBhl","2022-10-29T10:27:38.045Z","2026-08-02T01:33:51.198Z",{"sys":226},{"id":26,"type":17,"linkType":27},764,12,{"sys":230},{"type":17,"linkType":32,"id":33},{"title":232,"slug":233,"category":234,"body":246,"description":57,"legacy":58,"enableAd":59,"enableCommentField":59},"【Flutter】MethodChannelは直接使うことなかれ。型安全で楽に実装できる「Pigeon」の使い方のほぼ全て。【Swift\u002FObjective-C\u002FKotlin\u002FJava\u002FC++】","flutter-pigeon",{"metadata":235,"sys":238,"fields":245},{"tags":236,"concepts":237},[],[],{"space":239,"id":45,"type":21,"createdAt":46,"updatedAt":46,"environment":241,"publishedVersion":29,"revision":49,"contentType":243,"locale":34},{"sys":240},{"type":17,"linkType":18,"id":19},{"sys":242},{"id":26,"type":17,"linkType":27},{"sys":244},{"type":17,"linkType":32,"id":52},{"name":54,"slug":55},"Flutterでネイティブとやり取りしたい時、一番最初に知るのはMethodChannelを使った方法だと思います。しかし、MethodChannelは**直接使うと非常に使いづらい**です。引数はおろか、メソッド名すらString型で渡す必要があり、IDEの入力支援も受けられず、タイプミスしたら実行するまでエラーが吐かれず、引数の個数や名前、型も全部自由で何でも渡せてしまいます。\n\nこれでは非常に開発効率が悪いです。そこで、Flutterの公式パッケージである [Pigeon](https:\u002F\u002Fpub.dev\u002Fpackages\u002Fpigeon) を使うとこれらの問題を解決できます。\n\n## Pigeonは何をしてくれる？\n\nPigeonは、Flutterエンジンとネイティブ間でやり取りする関数を**インターフェースとして1回書く**だけで、Dart・ネイティブ言語の両方で型安全に処理を書くことが可能です。そのインターフェースを読み取って、Dartと各プラットフォームの言語で、関数の中身を書くための抽象クラスと、そこに書いた実装を呼び出すための関数を生成してくれます。\n\n使い方は3ステップです。\n\n1. Dart言語でPigeon用のインターフェースを書く\n2. 呼ばれる側の言語で、生成された抽象クラスを継承し、呼び出し時の実装を書く\n3. 呼ばれる側の言語でアプリ起動時にセットアップ関数を実行し、呼び出し待機状態にする\n\n関数のテンプレート（インターフェース）では、返り値、引数の名前、それらの型を指定できます。非同期関数も定義できます。ただし、[サポートされているデータ型](https:\u002F\u002Fdocs.flutter.dev\u002Fdevelopment\u002Fplatform-integration\u002Fplatform-channels?tab=type-mappings-c-plus-plus-tab#codec)には制限がありますので、目を通しておくことをお勧めします。\n\n## 使う準備\n\nプロジェクトルートで以下のコマンドを実行します。\n\n```\nflutter pub add dev:pigeon\n```\n\n次に、Flutterとネイティブでやり取りするインターフェースを定義するDartファイルを作ります。プロジェクトルートに`pigeon.dart`を作り、中身を記述していきます。\n\nまず、ネイティブ側の生成コードで使用する言語を選択します。iOSではSwiftとObjective-C、AndroidではKotlinとJavaが選択でき、どちらかを選んで生成する形となっています。新しい言語は対応が後回しになっている機能があるため古い言語（Objective-C、Java）で生成することを推奨します。__どちらを選んでも任意の言語から呼び出すことが可能です。__\n\n設定が必要なプロパティの基本的な例は以下の通りですが、選択した言語によって設定する必要のあるプロパティが異なります。\n\n| 言語 | 設定が必要なプロパティ |\n| --- | --- |\n| Swift | `swiftOut` |\n| Objective-C | `objcOptions`（`ObjcOptions.prefix`）、`objcHeaderOut`、`objcSourceOut` |\n| Kotlin | `kotlinOut`、`kotlinOptions`（`KotlinOptions.package`） |\n| Java |  `javaOut`、`javaOptions`（`JavaOptions.package`） |\n| C++ | `cppOptions`（`CppOptions.namespace`）、`cppHeaderOut`、`cppSourceOut` |\n\nちなみに、以下のコードは上から順番に書いていくと自動補完が効きません。先に適当な名前でクラスを作り、その上から`@ConfigurePigeon`を書き始めると自動補完が効くようになります。\n\n```Dart\nimport 'package:pigeon\u002Fpigeon.dart';\n\n@ConfigurePigeon(PigeonOptions(\n  dartOut: \"lib\u002Fmessages.g.dart\", \u002F\u002F Dartファイルの生成先（必須）\n  \n  swiftOut: \"ios\u002FRunner\u002FMessages.g.swift\", \u002F\u002F iOS用Swiftの出力先\n  \n  objcOptions: ObjcOptions(\n    prefix: \"FLT\" \u002F\u002F iOS用に生成されるObjective-Cのクラス名の接頭辞\n  ),\n  objcHeaderOut: \"ios\u002FRunner\u002Fmessages.g.h\", \u002F\u002F iOS用Objective-Cヘッダーの出力先\n  objcSourceOut: \"ios\u002FRunner\u002Fmessages.g.m\", \u002F\u002F iOS用Objective-C実装の出力先\n  \n  kotlinOut: \"android\u002Fapp\u002Fsrc\u002Fmain\u002Fkotlin\u002Fcom\u002Fexample\u002Fpigeon\u002FMessages.g.kt\", \u002F\u002F Android用Kotlinの出力先 (※ com\u002Fexample\u002Fpigeon の部分は実際のパスに変更)\n  kotlinOptions: KotlinOptions(\n    package: \"com.example.pigeon\", \u002F\u002F 生成するAndroid用Kotlinのパッケージ名\n  ),\n  \n  javaOut: \"android\u002Fapp\u002Fsrc\u002Fmain\u002Fjava\u002Fcom\u002Fexample\u002Fpigeon\u002FMessages.g.java\", \u002F\u002F Android用Javaの出力先 (※ com\u002Fexample\u002Fpigeon の部分は実際のパスに変更)\n  javaOptions: JavaOptions(\n    package: \"com.example.pigeon\" \u002F\u002F 生成するAndroid用Javaのパッケージ名\n  ),\n  \n  cppHeaderOut: \"windows\u002Frunner\u002Fmessages.g.h\", \u002F\u002F Windows用C++ヘッダーの出力先\n  cppSourceOut: \"windows\u002Frunner\u002Fmessages.g.cpp\", \u002F\u002F Windows用C++ソースの出力先\n  cppOptions: CppOptions(\n    namespace: \"example\" \u002F\u002F 任意の名前空間名（普通はアプリのパッケージ名）\n  ),\n))\nclass ExampleApi {} \u002F\u002F この行は下でインターフェースを作成したら消して問題ありません\n```\n\n## インターフェースの作成\n\n次に、やり取りするメソッドを定義するクラスを作ります。このクラスをもとに、各プラットフォームのコードが生成されます。同じファイルの下に以下のように記述します。\n\n```Dart\n@HostApi() \u002F\u002F Flutter --(call)--> Native\nabstract class ExampleHostApi {\n  \u002F\u002F\u002F\n  \u002F\u002F\u002F ドキュメントは全プラットフォームに反映されます\n  \u002F\u002F\u002F\n  void example();\n\n  void openUrl(String url);\n\n  StateResult queryState(); \u002F\u002F Enum(列挙型)およびカスタムクラスも使用可\n\n  @async\n  String getToken(); \u002F\u002F 非同期メソッド（※Futureの戻り値は直接使用できないが、生成されたコードでは戻り値がFutureでラップされる）\n}\n\nenum State {\n  pending,\n  success,\n  error,\n}\n\nclass StateResult {\n  String? errorMessage;\n  late State state; \u002F\u002F 非null許容の場合はlateキーワードを付与\n}\n\n@FlutterApi() \u002F\u002F Native --(call)--> Flutter\nabstract class ExampleFlutterApi {\n  void handleUri(String uri);\n}\n```\n\n## Pigeon実行用シェルスクリプトの作成\n\nPigeonで生成処理を走らせるシェルスクリプトを作っておくと便利です。プロジェクトルートに`run_pigeon.sh`という名前で以下のように書きます。\n\n```Shell\n#!\u002Fbin\u002Fbash\n\ndart pub run pigeon --input pigeon.dart\n```\n\nシェルスクリプトに実行権限がない場合があるので与え、実行します。\n\n```Shell\n% chmod u+x .\u002Frun_pigeon.sh\n% .\u002Frun_pigeon.sh\n```\n\n何も表示されなければ完了です。\n\n## Flutter側の実装\n\n以下はどのプラットフォーム（ホスト）でも共通の内容です。\n\n### Flutter側からネイティブコードを呼ぶ場合の例\n\n```Dart\nimport \"messages.g.dart\"\n\nExampleHostApi().openUrl(\"https:\u002F\u002Fexample.com\");\nawait ExampleHostApi().getToken();\n```\n\n### ネイティブ側からFlutter側のコードを呼ぶ場合\n\n> ※ネイティブからFlutter側のコードを呼ぶ場合、MethodChannelよりもEventChannelの方が都合が良い場合が多いです。本記事では触れませんが、EventChannelも現在のPigeonはサポートしていますので、詳しくはお調べ下さい。\n\n`lib\u002Fapi\u002Fexample_flutter_api_impl.dart`のようなファイルに以下のように書き、Flutter側で実行するコードを記述しておきます。\n\n```Dart\nclass ExampleFlutterApiImplementation implements ExampleFlutterApi {\n  @override\n  void handleUri(String uri) {\n    \u002F\u002F ...\n  }\n}\n```\n\n次に、この実装を用いて、ネイティブ側からのメッセージ(メソッド呼び出し)をFlutter側で受け取れるようにセットアップします。ここでは`lib\u002Fmain.dart`で初期化していますが、任意の場所に書けます。\n\n```Dart\nvoid main() {\n  ExampleFlutterApi.setup(ExampleFlutterApiImplementation());\n}\n```\n\n## Android (Kotlin) 側の実装\n\n### IDEの準備\n\n今回はKotlinで記述していく前提で話を進めます（Javaの場合は適宜読み替えてください）。まず、`android`ディレクトリをAndroid StudioもしくはIntelliJ IDEAで開いてください。\n\nGradle Syncの処理が完了するまで待ってから、Projectツールウィンドウから赤枠のドロップダウンより「Android」を選択します。「app」→「kotlin+java」を展開します。\n\n![image.webp](https:\u002F\u002Fimages.ctfassets.net\u002Fjmfsolylwbqz\u002F32l0YQlg4LFH2srG2bKeYN\u002Fec21729203c97126d0ef8ce68b88b845\u002Fimage.webp)\n\n### Flutter側からネイティブコードを呼ぶ場合\n\nアプリのパッケージ名を右クリックし、「New」→「Kotlin Class\u002FFile」を選択します。\n\n![image.webp](https:\u002F\u002Fimages.ctfassets.net\u002Fjmfsolylwbqz\u002F3kOTKoSNZkaIZi5JHuuUEK\u002F9af77b58af79e22075f16998fb806e0b\u002Fimage.webp)\n\n任意の名前（例えば`ExampleHostApiImplementation`）でクラスを作成します。このクラスに、Pigeonが生成した`ExampleHostApi`クラスを継承させ、ネイティブ側での実装を記述します。\n\n```Kotlin\nclass ExampleHostApiImplementation() : ExampleHostApi {\n    override fun openUrl(url: String) {\n\t\t  \u002F\u002F ...\n    }\n\n    override fun getToken(result: Result\u003CString>) { \u002F\u002F async function\n        FirebaseMessaging.getInstance().token.addOnSuccessListener {\n            result.success(it)\n        }.addOnFailureListener {\n            result.error(it)\n        }\n    }\n}\n```\n\nそして、Flutter側からのメソッド呼び出しを受け取れるよう、初期化します。\n\n```Kotlin\nclass MainActivity : FlutterActivity() {\n    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {\n        super.configureFlutterEngine(flutterEngine)\n\n        ExampleHostApi.setUp(flutterEngine.dartExecutor.binaryMessenger, ExampleHostApiImplementation()) \u002F\u002F 初期化\n\n        \u002F\u002F ...\n    }\n}\n```\n\n### ネイティブ側からFlutter側のコードを呼ぶ場合\n\nネイティブ側から呼ぶ（FlutterApi）場合は以下のようにします。`binaryMessenger`は、MainActivityの中に変数を作って保持しておくと使いやすいと思います。\n\n```Kotlin\nExampleFlutterApi(flutterEngine.dartExecutor.binaryMessenger).handleUri(uri) {}\n```\n\n## iOS (Swift) 側の実装\n\n### 準備\n\nまず、XcodeでiOS用ワークスペースを開きます。Finderにて`ios\u002FRunner.xcworkspace`をダブルクリックして開きます（もしくはターミナルで`open ios\u002FRunner.xcworkspace`でも可）。\n\nXcodeが開いたら、左のProject navigatorで「Runner」→「Runner」と展開しておきます。\n\n次に、Pigeonによって生成されたファイルをプロジェクトで使えるようにします。下の「Runner」を右クリックし、「Add Files to \"Runner\"...」をクリックします。\n\n![image.webp](https:\u002F\u002Fimages.ctfassets.net\u002Fjmfsolylwbqz\u002F5rBN2qdcRkNn1QozLrk7Gl\u002Fa31c6205512cbce3c12d18e0d87928fe\u002Fimage.webp)\n\n生成された`messages.g.swift`（または`messages.g.h`と`messages.g.m`）を選択します。次に表示されたダイアログでは、「Reference files in place」を選択し、Finishをクリックします。\n\n![image.webp](https:\u002F\u002Fimages.ctfassets.net\u002Fjmfsolylwbqz\u002F770y1tSaPoZJqsbDzZs6rf\u002F1a7ba4c77eb0cd5da16c01284ae9b805\u002Fimage.webp)\n\n続いて、__Pigeonの生成言語でObjective-Cを選択し、かつ実装をSwiftで書く方は__ `Runner-Bridging-Header.h`に以下を追記します（ない場合は同じ名前で作成してください）。これをしないとSwift側からObjective-Cのコードを使用できません。`messages.g.h`の部分は、生成されたファイルの名前です。\n\n```objc\n#import \"messages.g.h\"\n```\n\n### Flutter側からネイティブコードを呼ぶ場合（Swiftで生成）\n\n以下は、生成コードとして __Swift__ を選択した場合の書き方です。\n\n「Runner」を右クリックして「New Empty File」から任意の名前（例えば「ExampleHostApiImplementation.swift」）でSwiftファイルを作成します。\n\n```Swift\nclass ExampleHostApiImplementation: ExampleHostApi {\n    func openUrl(url: String) throws {\n        \u003C#code#>\n    }\n    \n    func isDarkMode() throws -> Bool {\n        return false;\n    }\n    \n    func getToken(completion: @escaping (Result\u003CString, any Error>) -> Void) {\n        completion(.success(\"token\"))\n    } \u002F\u002F Swiftの非同期関数を利用することは、Swift生成コードではPigeon v22.7.0現在できないようです。\n}\n```\n\n### Flutter側からネイティブコードを呼ぶ場合（Objective-Cで生成）\n\n以下は、生成コードとして __Objective-C__ を選択した場合の書き方です。\n\n「Runner」を右クリックして「New Empty File」から任意の名前（例えば「ExampleHostApiImplementation.swift」）でSwiftファイルを作成します。Objective-Cで書く場合は、適宜読み替えてください。\n\n```Swift\nclass ExampleHostApiImplementation: NSObject, FLTExampleHostApi { \u002F\u002F 継承するクラス名は、接頭辞としてConfigurePigeonのObjcOptions.prefixで設定したものが付きます。\n  func openUrl(_ url: String, error: AutoreleasingUnsafeMutablePointer\u003CFlutterError?>) { \u002F\u002F @ObjCSelector(\"openUrl:\")\n\t  \u003C#code#>\n\t}\n  \n  func isDarkModeWithError(_ error: AutoreleasingUnsafeMutablePointer\u003CFlutterError?>) -> NSNumber? {\n        return false as NSNumber\n    }\n\n\t\tfunc getTokenWithCompletion(_ completion: @escaping (String?, FlutterError?) -> Void) {\n        completion(\"token\", nil)\n    }\n\n\u002F\u002F    または\n\u002F\u002F    func token() async -> (String?, FlutterError?) {\n\u002F\u002F        return (\"token\", nil)\n\u002F\u002F    }\n}\n```\n\nそして、Flutter側からのメッセージをネイティブ側で受け取れるよう、`AppDelegate.swift`で初期化します。\n\n```Swift\n@main\n@objc class AppDelegate: FlutterAppDelegate {\n  override func application(\n    _ application: UIApplication,\n    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?\n  ) -> Bool {\n    \u002F\u002F ...\n    \u002F\u002F -- 追記 --\n    guard let controller = window?.rootViewController as? FlutterViewController else {\n      GeneratedPluginRegistrant.register(with: self)\n      return super.application(application, didFinishLaunchingWithOptions: launchOptions)\n    }\n    \n    ExampleHostApiSetup.setUp(binaryMessenger: controller.binaryMessenger, api: ExampleHostApiImplementation())\n    \n    \u002F\u002F -- 追記ここまで --\n      \n    GeneratedPluginRegistrant.register(with: self)\n    return super.application(application, didFinishLaunchingWithOptions: launchOptions)\n  }\n}\n```\n\n### メソッド名がおかしい場合\n\nと、ここで違和感に気づく場合があると思います。メソッド名がなんかおかしいです。実はObjective-CやSwiftでは「セレクター」と呼ばれるIDでメソッドを識別しており、それには特殊な命名規則が存在します。しかしPigeonでコードを生成すると、なぜか適切なセレクターが設定されません。詳しい命名規則は割愛しますが、セレクターは`@ObjCSelector`アノテーションでカスタマイズすることが可能です。\n\n`pigeons\u002Fmessages.dart`\n```Dart\n@ObjCSelector(\"openUrl:\")\nvoid openUrl(String url);\n\n@ObjCSelector(\"findUserWithName:AgeOf:\")\nvoid findUser(String name, int age);\n```\n\n先程の`run_pigeon.sh`を実行します。\n\n`ios\u002FRunner\u002FExampleIOSApi.swift`\n```Swift\nfunc openUrl(_ url: String, error: OpaquePointer) {\n\t\t\u003C#code#>\n}\nfunc findUser(withName name: String, ageOf age: Int, error: OpaquePointer) {\n        \u003C#code#>\n    }\n```\n\n### BooleanやInt、Longなどの型が返せない場合\n\nBooleanやInt、Longなどの型を戻り値に設定すると、ネイティブのコードでは戻り値がNSNumberに設定されるため、Swiftの型はそのままでは返せません。そのため、`as NSNumber`でキャストします。\n\n```Swift\ncompletion(true as NSNumber, nil)\ncompletion(50 as NSNumber, nil)\n```\n\n### ネイティブ側からFlutter側のコードを呼ぶ場合\n\nKotlin等と同じように呼ぶことができます。\n\n```Swift\nExampleFlutterApi(binaryMessenger).handleUri(uri)\n```\n\n`binaryMessenger`は、Kotlin同様、`AppDelegate`に変数を作って保持しておくと良いでしょう。\n\n## Windows側での実装\n\n### 準備\n\nVisual Studioが必要ですので、事前にインストールしておきます。ワークロードは「C++によるデスクトップ開発」を選択します。\n\nまず、一度`flutter run windows`を実行し、ソリューションファイルを生成します。\n\nVisual Studioを開き、「プロジェクトやソリューションを開く」をクリックし、「(Flutterプロジェクトルート)\\\\build\\\\windows\\\\x64\\\\(アプリ名).sln」を開きます。右側のソリューションエクスプローラーで「(アプリ名)」→「Source Files」を展開しておきます。\n \n`CMakeLists.txt`を開き、`add_executable`の中に生成されたファイルを追加します。\n\n```CMake\n# ...\nadd_executable(${BINARY_NAME} WIN32\n  \"flutter_window.cpp\"\n  \"main.cpp\"\n  \"utils.cpp\"\n  \"win32_window.cpp\"\n  \"${FLUTTER_MANAGED_DIR}\u002Fgenerated_plugin_registrant.cc\"\n  \"Runner.rc\"\n  \"runner.exe.manifest\"\n  \"messages.g.cpp\" # 追加\n)\n# ...\n```\n\n### Flutter側からネイティブコードを呼ぶ場合\n\nソリューションエクスプローラーで`flutter_window.cpp`をダブルクリックして開きます。エディター側でCtrl+K、Ctrl+Oの順に押し、ヘッダーファイルに切り替えます。以下を追記します。「名前空間」とは、上で`CppOptions.namespace`に設定した値です。\n\n```cpp\n\u002F\u002F A window that does nothing but host a Flutter view.\nclass FlutterWindow : public Win32Window {\n\u002F\u002F ...\n private:\n  \u002F\u002F The project to run.\n  flutter::DartProject project_;\n\n  std::unique_ptr\u003Cexample::ExampleHostApi> exampleHostApi_; \u002F\u002F 追記　(std::unique_ptr\u003C名前空間::APIクラス名> APIクラス名のキャメルケース+アンダースコア)\n\n  \u002F\u002F The Flutter instance hosted by this window.\n  std::unique_ptr\u003Cflutter::FlutterViewController> flutter_controller_;\n};\n```\n\n再度Ctrl+K、Ctrl+Oの順に押して実装ファイルに切り替え、以下を追記します。\n\n```cpp\n#include \"flutter_window.h\"\n\n#include \u003Cflutter\u002Fbinary_messenger.h> \u002F\u002F 追記\n\n#include \u003Cmemory> \u002F\u002F 追記\n#include \u003Coptional>\n\n#include \"flutter\u002Fgenerated_plugin_registrant.h\"\n#include \"messages.g.h\" \u002F\u002F 追記\n\n\u002F\u002F 追記ここから\nnamespace {\n    using example::ExampleHostApi;\n    using example::ErrorOr;\n    using example::FlutterError;\n\n    class ExampleHostApiImplementation : public ExampleHostApi {\n    public:\n        ExampleHostApiImplementation() {}\n        virtual ~ExampleHostApiImplementation() {}\n\n       void OpenUrl(\n           const std::string& url\n       ) {\n           \u002F\u002F 何らかの処理\n       }\n       \n       void GetToken(\n           std::function\u003Cvoid(ErrorOr\u003Cstd::string> reply)> result\n       ) {\n           \u002F\u002F 何らかの処理\n           result(std::string(\"token\"));\n       }\n    };\n}\n\u002F\u002F 追記ここまで\n\nFlutterWindow::FlutterWindow(const flutter::DartProject& project)\n    : project_(project) {}\n\nFlutterWindow::~FlutterWindow() {}\n\n\u002F\u002F ...\n```\n\nそして、Flutter側からのメッセージをネイティブ側で受け取れるよう、初期化します。\n\n```cpp\n\u002F\u002F ...\nbool FlutterWindow::OnCreate() {\n  if (!Win32Window::OnCreate()) {\n    return false;\n  }\n\n  RECT frame = GetClientArea();\n\n  \u002F\u002F The size here must match the window dimensions to avoid unnecessary surface\n  \u002F\u002F creation \u002F destruction in the startup path.\n  flutter_controller_ = std::make_unique\u003Cflutter::FlutterViewController>(\n      frame.right - frame.left, frame.bottom - frame.top, project_);\n  \u002F\u002F Ensure that basic setup of the controller was successful.\n  if (!flutter_controller_->engine() || !flutter_controller_->view()) {\n    return false;\n  }\n  RegisterPlugins(flutter_controller_->engine());\n  SetChildContent(flutter_controller_->view()->GetNativeWindow());\n\n  exampleHostApi_ = std::make_unique\u003CExampleHostApiImplementation>(); \u002F\u002F 追記\n  ExampleHostApi::SetUp(flutter_controller_->engine()->messenger(), exampleHostApi_.get());　\u002F\u002F 追記\n\n  return true;\n}\n\n\u002F\u002F ...\n```\n\n### ネイティブ側からFlutterコードを呼ぶ場合\n\nwip",{"metadata":248,"sys":251,"fields":261},{"tags":249,"concepts":250},[],[],{"space":252,"id":254,"type":21,"createdAt":255,"updatedAt":255,"environment":256,"publishedVersion":258,"revision":49,"contentType":259,"locale":34},{"sys":253},{"type":17,"linkType":18,"id":19},"wHT041IZNN6Eu0z3JYDN1","2022-10-22T09:33:52.558Z",{"sys":257},{"id":26,"type":17,"linkType":27},27,{"sys":260},{"type":17,"linkType":32,"id":33},{"title":262,"slug":263,"category":264,"body":276,"legacy":58},"【Flutter】iOSで Integration Test を利用したスクリーンショット自動化が失敗する","ios-screenshot-automation-fails",{"metadata":265,"sys":268,"fields":275},{"tags":266,"concepts":267},[],[],{"space":269,"id":45,"type":21,"createdAt":46,"updatedAt":46,"environment":271,"publishedVersion":29,"revision":49,"contentType":273,"locale":34},{"sys":270},{"type":17,"linkType":18,"id":19},{"sys":272},{"id":26,"type":17,"linkType":27},{"sys":274},{"type":17,"linkType":32,"id":52},{"name":54,"slug":55},"## 事象\n\nFlutterでIntegration Test内でのスクリーンショット撮影が、iOSでのみ以下のようなエラーで失敗する。\n\n```\nFailure in method: screenshot\n══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞═════════════════\nThe following MissingPluginException was thrown running a test:\nMissingPluginException(No implementation found for method\ncaptureScreenshot on channel plugins.flutter.io\u002Fintegration_test)\n\nWhen the exception was thrown, this was the stack:\n#0      MethodChannel._invokeMethod (package:flutter\u002Fsrc\u002Fservices\u002Fplatform_channel.dart:294:7)\n\u003Casynchronous suspension>\n#1      IOCallbackManager.takeScreenshot (package:integration_test\u002F_callback_io.dart:94:33)\n\u003Casynchronous suspension>\n#2      IntegrationTestWidgetsFlutterBinding.takeScreenshot (package:integration_test\u002Fintegration_test.dart:190:39)\n\u003Casynchronous suspension>\n#3      takeScreenshot (file:\u002F\u002F\u002FUsers\u002Fchika\u002FStudioProjects\u002Fsubmon\u002Fintegration_test\u002Fscreenshot_test.dart:57:3)\n\u003Casynchronous suspension>\n#4      main.\u003Canonymous closure> (file:\u002F\u002F\u002FUsers\u002Fchika\u002FStudioProjects\u002Fsubmon\u002Fintegration_test\u002Fscreenshot_test.dart:26:5)\n\u003Casynchronous suspension>\n#5      testWidgets.\u003Canonymous closure>.\u003Canonymous closure> (package:flutter_test\u002Fsrc\u002Fwidget_tester.dart:171:15)\n\u003Casynchronous suspension>\n#6      TestWidgetsFlutterBinding._runTestBody (package:flutter_test\u002Fsrc\u002Fbinding.dart:841:5)\n\u003Casynchronous suspension>\n\nThe test description was:\n  screenshot\n═════════════════════════════════════════════════════════════════\n```\n\nこれは以下のIssueでも議論されているが、未だにCloseされていない。\n\n[[integration_test] Plugin not registered unless running via XCTest #91668 ](https:\u002F\u002Fgithub.com\u002Fflutter\u002Fflutter\u002Fissues\u002F91668)\n\n## 解決方法\n\n以下のページを参考にした。\n\n[Grabs screenshots from your Flutter app on multiple platforms (iOS and Android)](https:\u002F\u002Fflutterawesome.com\u002Fgrabs-screenshots-from-your-flutter-app-on-multiple-platforms-ios-and-android\u002F)\n\n`flutter(Flutter SDKのフォルダー)\u002Fpackages\u002Fintegration_test\u002Fios\u002FClasses\u002FIntegrationTestPlugin.m`を開き、`registerWithRegistrar`メソッドの内容を以下のように書き換える。\n\n```\n+ (void)registerWithRegistrar:(NSObject\u003CFlutterPluginRegistrar> *)registrar {\n  \u002F\u002F No initialization happens here because of the way XCTest loads the testing\n  \u002F\u002F bundles.  Setup on static variables can be disregarded when a new static\n  \u002F\u002F instance of IntegrationTestPlugin is allocated when the bundle is reloaded.\n  \u002F\u002F See also: https:\u002F\u002Fgithub.com\u002Fflutter\u002Fplugins\u002Fpull\u002F2465\n  [[IntegrationTestPlugin instance] setupChannels:registrar.messenger]; \u002F\u002F 追加\n}\n```\n\nそのままビルドして実行すると、撮影が可能になる。",{"metadata":278,"sys":281,"fields":291},{"tags":279,"concepts":280},[],[],{"space":282,"id":284,"type":21,"createdAt":285,"updatedAt":285,"environment":286,"publishedVersion":288,"revision":49,"contentType":289,"locale":34},{"sys":283},{"type":17,"linkType":18,"id":19},"34EHnKb69VfpqPEmcrabyX","2022-10-20T14:56:27.714Z",{"sys":287},{"id":26,"type":17,"linkType":27},22,{"sys":290},{"type":17,"linkType":32,"id":33},{"title":292,"slug":293,"category":294,"body":306,"legacy":58},"GitHub Actions (CI) にて、「Error (Xcode): Invalid record」というエラーが出てきてFlutter iOSがビルドできない","gh-actions-xcode-invalid-record",{"metadata":295,"sys":298,"fields":305},{"tags":296,"concepts":297},[],[],{"space":299,"id":45,"type":21,"createdAt":46,"updatedAt":46,"environment":301,"publishedVersion":29,"revision":49,"contentType":303,"locale":34},{"sys":300},{"type":17,"linkType":18,"id":19},{"sys":302},{"id":26,"type":17,"linkType":27},{"sys":304},{"type":17,"linkType":32,"id":52},{"name":54,"slug":55},"## 症状\n\nGitHub ActionsでFlutterのiOSアプリをビルドしようとしたら、エラーが出てきて失敗する。どうやら使っているデータベースの Isar が関係ある模様。\n\n```\nError (Xcode): Invalid record (Producer: 'LLVM14.0.6-rust-1.64.0-stable' Reader: 'LLVM APPLE_1_1300.0.29.30_0') for architecture arm64\n```\n\n## 原因と解決方法\n\n__Xcodeのバージョンがプロジェクトのバージョンと違う__ のが原因だった。あのエラー文でそんなことある？？？\n\nXcodeのProject Formatでは「Xcode 14.0-compatible」に設定してあるのにも関わらず、ワークフローのYMLファイルには `runs-on: macos-latest` と記述していた。これは現在(2022\u002F10\u002F20)、__macOS 11 Big Surのことを指す__ ため、macOS 12以上にしか対応していないXcode 14は使えない。`-latest`は、GitHub側が安定版だと判断した場合に付けられるらしい。\n\nそのため、以下のようにすれば解決する。\n\n```\nruns-on: macos-12\n```",{"metadata":308,"sys":311,"fields":320},{"tags":309,"concepts":310},[],[],{"space":312,"id":314,"type":21,"createdAt":315,"updatedAt":315,"environment":316,"publishedVersion":165,"revision":49,"contentType":318,"locale":34},{"sys":313},{"type":17,"linkType":18,"id":19},"4RQ32zSG8afhSWEt0JGwSq","2022-03-09T05:49:45.058Z",{"sys":317},{"id":26,"type":17,"linkType":27},{"sys":319},{"type":17,"linkType":32,"id":33},{"title":321,"slug":322,"category":323,"body":335,"legacy":58},"Vercel + Nuxt.js で reCAPTCHA を導入する方法 (リメイク)","vercel-recaptcha-2",{"metadata":324,"sys":327,"fields":334},{"tags":325,"concepts":326},[],[],{"space":328,"id":45,"type":21,"createdAt":46,"updatedAt":46,"environment":330,"publishedVersion":29,"revision":49,"contentType":332,"locale":34},{"sys":329},{"type":17,"linkType":18,"id":19},{"sys":331},{"id":26,"type":17,"linkType":27},{"sys":333},{"type":17,"linkType":32,"id":52},{"name":54,"slug":55},"皆さん、こんにちは。\n\n今回は、Vercelを用いてreCAPTCHAを導入してBot対策する方法を解説していきます。Vercelを用いた方法の日本語記事はざっと探しても見当たらなかったので、記事を書いてみます。\n\n以前投稿した記事は一部誤っている部分があったため、再投稿です。\n\n## 下準備\n\nまず、reCAPTCHA のコンソール画面から利用登録します。以下のページに飛んでください。\n\n:::btn\n[reCAPTCHA](https:\u002F\u002Fwww.google.com\u002Frecaptcha\u002Fadmin)\n:::\n\nすると、以下の画面が表示されるはずです。\n\n![image](\u002F\u002Fimages.ctfassets.net\u002Fjmfsolylwbqz\u002FgicNmmRcMmWMUIyjOZkfi\u002Fbe36e79624a45eac031e52de19400be3\u002Fimage.png)\n\nここでは、以下のように選択・入力してください。\n\n・__ラベル__：任意の名前 (サイト名)\n\n・__reCAPTCHA タイプ__：reCAPTCHA v3 を選択\n\n・__ドメイン__：公開予定のドメインを入力\n\n・__「reCAPTCHA 利用条件に同意する」にチェックを入れる__\n\n送信後、__「サイトキー」__ と __「シークレットキー」__ が表示されますが、これは後で使用しますのでページは閉じないでください。\n\n## 実装\n\nここからは実装です。reCAPTCHA は、以下のような仕組みで成り立っています。\n\n1. ユーザーがフォームの「送信」をクリックする\n2. クライアント側で検証し、トークンを生成\n3. トークンをサーバーに送り、検証し、結果を返却\n4. 結果が設定したスコアより高ければフォームの送信処理をする\n\nreCAPTCHAでは、利用者がどれだけ人間だと思われるかのスコアが結果として帰ってきます。しきい値を上げればボットをたくさん弾けますし、低くすれば誤検知のリスクが下がります。\n\n### クライアント側\n\n今回はNuxt.jsを用いて実装しますが、それぞれのフレームワークに適宜読み替えてください。\n\nまず、スクリプトをロードします。以下をnuxt.config.jsに追記してください。\n\n```JavaScript\nexport default {\n  head() {\n    return {\n      script: [\n        {\n          src: \"https:\u002F\u002Fwww.google.com\u002Frecaptcha\u002Fapi.js?render=\u003CSITE-KEY>\"\n        }\n      ]\n    }\n  }\n}\n```\n\n__\\\u003CSITE-KEY>部分は先程のサイトキーに置き換えてください。__\n\n次に、送信ボタンのクリック時の処理を書きます。axiosパッケージをインストールしていない場合は、`npm i axios` もしくは `yarn add axios` でインストールしておいてください。\n\n```JavaScript\ngrecaptcha.ready(() => {\n  grecaptcha.execute(\"\u003CSITE-KEY>\", {action: 'submit'}).then((token) => {    \n    axios.get(\"\u002Fapi\u002FcheckRecaptcha?response=\" + encodeURIComponent(token)).then(async (response) => {\n      if (response.data.success) {\n\t\t\t\t\u002F\u002F スコアは任意で変更\n        if (response.data.score >= 0.5) {\n\t\t\t\t\t\u002F\u002F 送信処理\n\t\t\t\t}\n      }\n    })\n  })\n})\n```\n\n__同じく、\\\u003CSITE-KEY>部分は先程のサイトキーに置き換えてください。__\n\nここで、VercelのサーバーのAPIを叩いて、トークンを検証します。スコアのしきい値の目安は0.5くらいが良いと思います。試してみたところ、ボタンを1回だけ押すと0.9、数回連打すると0.7、何回も連打すると0.3、というように下がっていきました。\n\n### サーバー側\n\n次は、Nuxt.jsのプロジェクトルートに`api`フォルダーを作成し、その中に`checkRecaptcha.js`を作成します。中身は以下です。\n\n```JavaScript\nconst axios = require(\"axios\").default\n\nmodule.exports = (req, res) => {\n  const response = req.query.response\n  console.log(\"recaptcha response\", response)\n  axios.post(`https:\u002F\u002Frecaptcha.google.com\u002Frecaptcha\u002Fapi\u002Fsiteverify?secret=\u003CSECRET-KEY>&response=${response}`).then(result => {\n    \u002F\u002F console.log(\"recaptcha result\", result)\n\n    if (result.data.success) {\n      res.send({success: true})\n    } else {\n      res.send({\n        success: false\n      })\n    }\n  }).catch(reason => {\n    console.log(\"Recaptcha request failure\", reason)\n    res.send(\"Recaptcha request failed.\")\n  })\n}\n```\n\n__\\\u003CSECRET-KEY>部分は最初のシークレットキーに置き換えてください。__\n\nエミュレーションするには、`yarn dev`ではなく、Vercelのコマンドラインツールを利用します。`yarn add vercel`でインストールし、`vercel dev`コマンドを実行します。初回は色々表示されるかもしれませんが、読めば分かるはずです。\n\nこれでVercelにデプロイすれば、reCAPTCHAで認証が可能になります。",{"Entry":337},[338],{"metadata":339,"sys":342,"fields":349},{"tags":340,"concepts":341},[],[],{"space":343,"id":45,"type":21,"createdAt":46,"updatedAt":46,"environment":345,"publishedVersion":29,"revision":49,"contentType":347,"locale":34},{"sys":344},{"type":17,"linkType":18,"id":19},{"sys":346},{"id":26,"type":17,"linkType":27},{"sys":348},{"type":17,"linkType":32,"id":52},{"name":54,"slug":55},1794190172827]