【Flutter】DraggableScrollableSheetの最大高さを子要素のサイズに合わせたい!

2024/08/01・役立ち情報

.code-note { background: #1f8cc2; color: white; display: inline-block; padding: 8px 16px; border-radius: 8px 8px 0 0; transform: translateX(16px) translateY(16px); } 今回はタイトルの通り、DraggableScrollableSheetにおけるmaxChildSizeを子要素の実際のサイズに合わせる方法を解説します。DraggableScrollableSheetのコンテンツの高さが場合によって変わる場合、展開したときの最大サイズをそのコンテンツの高さに合わせたい場合があると思います。何も指定ない状態では、以下のようにコンテンツの量に対してシートが多くの面積を占領してしまうケースがあります。 :::asdf ::: これをどうにかしていきましょう。 ※StatefulWidgetを使用したコードについては、今後追加予定です。 実現のために必要な要素 maxChildSizeを子要素の実際のサイズに合わせるには、子要素のサイズに合わせてmaxChildSizeを変えてやる必要があります。これを実現するためには、以下の4つ数値をどうにかして入手する必要があります。 実際の子要素のサイズ DraggableScrollableSheetの最大高さ 子要素の最大サイズ割合 (maxChildSize) (サイズがこの値で頭打ちするように) 初期サイズ割合 (initialChildSize) (この値より小さい値がmaxChildSizeに割り当てられることを防ぐため) DraggableScrollableSheetのmaxChildSizeプロパティは、DraggableScrollableSheetが展開できる最大サイズに対する割合 で取るため、子要素のサイズに加えてその最大サイズが分からないといけません。これらはプログラムで取得する必要があります。 子要素の最大サイズ割合は、子要素が十分に大きい際に頭打ちする値です。初期サイズ割合については、これより小さい値がmaxChildSizeに設定されてしまうとエラーが発生するため、これを防ぐために必要です。 コンテンツ(子要素)の高さを取得する まず、コンテンツ(子要素)の高さを取得するため、当該の子要素にkeyをつけます。 :::code-note Flutter Hooksを利用する場合 ::: class MyBottomSheet extends HookWidget { const MyBottomSheet({super.key}); @override Widget build(BuildContext context) { final contentsKey = useMemoized(() => GlobalKey()); // <= return DraggableScrollableSheet( expand: false, snap: true, maxChildSize: 0.9, builder: (context, scrollController) { return SingleChildScrollView( controller: scrollController, child: Column( key: contentsKey // <= crossAxisAlignment: CrossAxisAlignment.stretch, children: [ for (var i = 0; i < 15; i++) Card( child: Text("Item $i"), ), ], ), ); }, ); } } StatefulWidgetを利用する場合 class MyBottomSheetWoHooks extends StatefulWidget { const MyBottomSheetWoHooks({super.key}); @override State<MyBottomSheetWoHooks> createState() => _MyBottomSheetWoHooksState(); } class _MyBottomSheetWoHooksState extends State<MyBottomSheetWoHooks> { final contentsKey = GlobalKey(); // <= @override Widget build(BuildContext context) { return DraggableScrollableSheet( expand: false, snap: true, maxChildSize: 0.9, builder: (context, scrollController) { return SingleChildScrollView( controller: scrollController, child: Column( key: contentsKey, // <= crossAxisAlignment: CrossAxisAlignment.stretch, children: [ for (var i = 0; i < 15; i++) Card( child: Text("Item $i"), ), ], ), ); }, ); } } このkeyに対して、key.currentContext.findRenderObject()を通してWidgetの実際の描画サイズにアクセスできます。 【注意】SingleChildScrollViewにpaddingを適用している場合、その子要素のPaddingに対してkeyを設定する必要があり、SingleChildScrollViewのプロパティでpaddingを設定してはいけません。こうしないと、ScrollViewで見えている範囲の高さとなってしまい、正しいコンテンツの高さが取得できません。 builder: (context, scrollController) { return SingleChildScrollView( controller: scrollController, child: Padding( key: contentsKey, // <= ここに設定 padding: const EdgeInsets.only(left: 16.0, bottom: 16.0, right: 16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ for (var i = 0; i < 15; i++) Card( child: Text("Item $i"), ), ], ), ), ); }, DraggableScrollableSheetが占領できる最大の高さを取得する この高さは、要するにmaxChildSize: 1.0の場合のDraggableScrollableSheetの最大高さです。これを取得するには、LayoutBuilderを使います。 Flutter Hooksを利用する場合 class MyBottomSheet extends HookWidget { const MyBottomSheet({super.key}); @override Widget build(BuildContext context) { final contentsKey = useMemoized(() => GlobalKey()); final availableHeight = useRef<double?>(null); // <= 追加 return LayoutBuilder( // <= 追加 builder: (context, constraints) { availableHeight.value = constraints.maxHeight; // <= 追加 return DraggableScrollableSheet( expand: false, snap: true, maxChildSize: 0.9, builder: (context, scrollController) { return SingleChildScrollView( controller: scrollController, child: Padding( key: contentsKey, padding: const EdgeInsets.only(left: 16.0, bottom: 16.0, right: 16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ for (var i = 0; i < 15; i++) Card( child: Text("Item $i"), ), ], ), ), ); }, ); }, ); // <= 追加 } } ※availableHeightはbuildメソッドの中で値を取得・設定しているため、値が変更されても再描画が走らないuseRefを使用しています。(buildメソッドの処理が終わっていないタイミングで再描画が走るとエラーになります) 最大領域に対する子要素のサイズの割合を計算する DraggableScrollableSheetが展開できる最大サイズに対する、子要素のサイズの割合を計算します。 Flutter Hooksを利用する場合 class MyBottomSheet extends HookWidget { const MyBottomSheet({super.key}); @override Widget build(BuildContext context) { final contentsKey = useMemoized(() => GlobalKey()); final availableHeight = useRef<double?>(null); const maxChildSize = 0.9; // <= 追加 const initialChildSize = 0.5; // <= 追加 double calculateChildSizeRatio() { // <= 追加 final renderBox = contentsKey.currentContext?.findRenderObject() as RenderBox?; if (renderBox == null || availableHeight.value == null) { return maxChildSize; } return max(initialChildSize, min(renderBox.size.height / availableHeight.value!, maxChildSize)); } return LayoutBuilder( builder: (context, constraints) { availableHeight.value = constraints.maxHeight; return DraggableScrollableSheet( expand: false, snap: true, maxChildSize: maxChildSize, // <= 変更 initialChildSize: initialChildSize, // <= 追加 builder: (context, scrollController) { return SingleChildScrollView( controller: scrollController, child: Padding( key: contentsKey, padding: const EdgeInsets.only(left: 16.0, bottom: 16.0, right: 16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ for (var i = 0; i < 15; i++) Card( child: Text("Item $i"), ), ], ), ), ); }, ); }, ); } } 実際に割合を計算しているのはこの部分ですね。あまり難しい計算はしていないです。 max(initialChildSize, min(renderBox.size.height / availableHeight.value!, maxChildSize)); maxChildSizeを、計算した値に設定する いよいよ、計算した値を設定していきます。Hooksを使用している場合は、useEffectを用いて初回のレンダリングが終わったタイミングで設定します。 Flutter Hooksを利用する場合 class MyBottomSheet extends HookWidget { const MyBottomSheet({super.key}); @override Widget build(BuildContext context) { final contentsKey = useMemoized(() => GlobalKey()); final availableHeight = useRef<double?>(null); const maxChildSize = 0.9; const initialChildSize = 0.5; final currentMaxChildSize = useState(maxChildSize); // <= 追加 double calculateChildSizeRatio() { final renderBox = contentsKey.currentContext?.findRenderObject() as RenderBox?; if (renderBox == null || availableHeight.value == null) { return maxChildSize; } return max(initialChildSize, min(renderBox.size.height / availableHeight.value!, maxChildSize)); } useEffect(() { // <= 追加 WidgetsBinding.instance.addPostFrameCallback((_) { currentMaxChildSize.value = calculateChildSizeRatio(); }); return null; }, [],); return LayoutBuilder( builder: (context, constraints) { availableHeight.value = constraints.maxHeight; return DraggableScrollableSheet( expand: false, snap: true, maxChildSize: currentMaxChildSize.value, // <= 変更 initialChildSize: initialChildSize, builder: (context, scrollController) { return SingleChildScrollView( controller: scrollController, child: Padding( key: contentsKey, padding: const EdgeInsets.only(left: 16.0, bottom: 16.0, right: 16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ for (var i = 0; i < 15; i++) Card( child: Text("Item $i"), ), ], ), ), ); }, ); }, ); } } 実行結果 子要素に合わせて展開サイズが変わるようになりました。パチパチ

【Bun】bun.lockb でマージコンフリクトが起こった際の対処法

2023/10/21・役立ち情報

前提 Bunを使っていてGitでマージしようとした時、ロックファイルがコンフリクトすることがあります。しかし、Bunのロックファイルはバイナリなので、手作業でコンフリクトの解決をすることができません。以下は、Bunの自動コンフリクト解決機能を用いる方法です。 方法 リベース/マージを試みます。コンフリクトが起こります。 bun iを実行します。自動的にコンフリクトが解消されます。 git <rebase/merge> --continueを実行します。コミットメッセージをよしなに編集します。 これでコンフリクトが解消できます。

Xcode 15.0 Beta 5以降、pod install に失敗する問題の対処法

2023/08/27・役立ち情報

概要 Xcode 15.0 Beta 5以降、こんなエラーが出て CocoaPods の pod installに失敗する。 DT_TOOLCHAIN_DIR cannot be used to evaluate LIBRARY_SEARCH_PATHS, use TOOLCHAIN_DIR instead (in target 'TARGET' from project 'PROJECT') この不具合は既にIssueが立っており、修正がマージされている。しかし、v1.13.0にマイルストーンが立っており、リリースは進捗的に少し先になりそうな様子。それまでは以下の対処法でビルドを通すことができる。 2023/09/25追記: この問題の修正版、v1.13.0がリリースされた。現在は以下の対処法ではなく、CocoaPodsを更新することで対処できる。 対処法 Podfileを編集する。 # ... post_install do |installer| # ... installer.pods_project.targets.each do |target| # ... target.build_configurations.each do |config| # --- Workaround for Xcode 15.0 --- xcconfig_path = config.base_configuration_reference.real_path xcconfig = File.read(xcconfig_path) xcconfig_mod = xcconfig.gsub(/DT_TOOLCHAIN_DIR/, "TOOLCHAIN_DIR") File.open(xcconfig_path, "w") { |file| file << xcconfig_mod } # --------------------------------- end end end 参考 "Error 'DT_TOOLCHAIN_DIR cannot be used to evaluate LIBRARY_SEARCH_PATHS, use TOOLCHAIN_DIR instead' in Xcode 15 beta 5" (GitHub)

[Flutter] URIをネイティブからFlutter側に渡すためのベストプラクティス (多分)

2023/08/25・役立ち情報

前提 ネイティブからFlutterにURIを渡すためには、EventChannelを利用すると思います。単純にFlutter側でmain()でリスナーを設定し、ネイティブでapplication(_:open:options:)やonNewIntentなどでURIを送り込むと、アプリ起動中にURIが流れてきた場合は動作するしれませんが、URIによってアプリが起動された際にはURIを渡すことができません。何故なら、Flutterエンジンの初期化には時間がかかるためです。 そのため、リッスンされるまではネイティブのコードでURIを貯蔵しておき、リッスンが開始された際に流し込む、というようなアプローチが必要になります。 ホスト(ネイティブ)側のコード iOS class UriEventApi: NSObject { static let channelName = "com.example.app.event/uri" // Channel name (任意に変更) var channel: FlutterEventChannel var eventSink: FlutterEventSink? // リッスンが開始されたタイミングで代入され、キャンセルされたタイミングでnilにされる var pendingUri: String? init(binaryMessenger: FlutterBinaryMessenger) { channel = FlutterEventChannel(name: UriEventApi.channelName, binaryMessenger: binaryMessenger) } func initHandler() { channel.setStreamHandler(self) } func onUri(uri: String) { if (eventSink != nil) { // Flutter側で既にリッスンが開始されている場合、 eventSink!(uri) // そのままeventSinkを発火させる } else { // リッスンが開始されていない = まだFlutterエンジンが初期化されていない 場合、 pendingUri = uri // pendingUriに代入して貯めておく } } } extension UriEventApi: FlutterStreamHandler { func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { // リッスンが開始された際、 eventSink = events if (pendingUri != nil) { // 保留中のURIが存在する場合、 eventSink!(pendingUri) // eventSinkを発火させてFlutter側に伝える pendingUri = nil } return nil } func onCancel(withArguments arguments: Any?) -> FlutterError? { eventSink = nil return nil } } Android // 実装の内容はiOS(Swift)と同様 class UriEventApi(binaryMessenger: BinaryMessenger) : EventChannel.StreamHandler { private val channel = EventChannel(binaryMessenger, CHANNEL_NAME) companion object { const val CHANNEL_NAME = "com.example.event/uri" } private var eventSink: EventChannel.EventSink? = null private var pendingData: String? = null fun initHandler() { channel.setStreamHandler(this) } fun onUri(uri: String) { if (eventSink == null) { pendingData = uri } else { eventSink!!.success(uri) } } override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { eventSink = events if (pendingData != null) { eventSink!!.success(pendingData) pendingData = null } } override fun onCancel(arguments: Any?) { eventSink = null } } Flutter側のコード 扱いやすくするためにUriEventApiという名前でクラスを作ります。 // lib/event_api/uri_event_api.dart class UriEventApi { static const _eventName = "com.example.app.event/uri"; static const _channel = EventChannel(_eventName); StreamSubscription listen() { return _channel.receiveBroadcastStream().listen((uri) { // process }); } } これを任意のタイミングでリッスンします。 UriEventApi().listen(); このコードで基本的には動くはずです。

【Xcode 15/iOS/WidgetKit】"Error (Xcode): Cycle inside Runner; building could produce unreliable results." エラー

2023/08/08・役立ち情報

問題発生 久々にFlutterアプリをビルドしようとしたら、以下のようなエラーが発生して失敗する問題が起きた。 Error (Xcode): Cycle inside Runner; building could produce unreliable results. Cycle details: → Target 'Runner': CodeSign /Users/xxx/Library/Developer/Xcode/DerivedData/Runner-xxx/Build/Intermediates.noindex/ArchiveIntermediates/Runner/InstallationBuildProductsLocation/Applications/Runner.app ○ That command depends on command in Target 'Runner': script phase “[CP] Embed Pods Frameworks” ○ Target 'Runner' has copy command from '/Users/chika/Library/Developer/Xcode/DerivedData/Runner-xxx/Build/Intermediates.noindex/ArchiveIntermediates/Runner/BuildProductsPath/Release-iphoneos/WidgetKitExtension.appex' to '/Users/xxx/Library/Developer/Xcode/DerivedData/Runner-xxx/Build/Intermediates.noindex/ArchiveIntermediates/Runner/InstallationBuildProductsLocation/Applications/Runner.app/PlugIns/WidgetKitExtension.appex' ○ That command depends on command in Target 'Runner': script phase “Thin Binary” ○ Target 'Runner' has process command with output '/Users/xxx/Library/Developer/Xcode/DerivedData/Runner-xxx/Build/Intermediates.noindex/ArchiveIntermediates/Runner/InstallationBuildProductsLocation/Applications/Runner.app/Info.plist' ○ Target 'Runner' has copy command from '/Users/xxx/Library/Developer/Xcode/DerivedData/Runner-xxx/Build/Intermediates.noindex/ArchiveIntermediates/Runner/BuildProductsPath/Release-iphoneos/WidgetKitExtension.appex' to '/Users/xxx/Library/Developer/Xcode/DerivedData/Runner-xxx/Build/Intermediates.noindex/ArchiveIntermediates/Runner/InstallationBuildProductsLocation/Applications/Runner.app/PlugIns/WidgetKitExtension.appex' 環境 Flutter 3.10.6 % flutter --version Flutter 3.10.6 • channel stable • https://github.com/flutter/flutter.git Framework • revision f468f3366c (4 weeks ago) • 2023-07-12 15:19:05 -0700 Engine • revision cdbeda788a Tools • Dart 3.0.6 • DevTools 2.23.1 Xcode 15.0 Beta 4 対処法 Appleのエラーメッセージは非常に分かりにくいのですが、改めて隅々まで読んでみると Build Phasesの順序が問題で、[CP] Embed Pods FrameworksとThin BinaryのBuild Phaseは、WidgetKitのExtensionのコピー作業を前提としているために失敗している、と書かれています。多分。 そのため、Embed App ExtensionsのBuild Phaseを上記の2つより上に持ってくる ことで解決しました。 Build Phasesの内容は環境によってかなり異なるため、上記の画像と違う場合にはエラーメッセージを詳しく読んでみてください。

MinecraftのドラクエMOD、DQMを手軽に導入できるソフト「DQM Installer」の使い方

2023/06/29・作ったモノ

こんにちは。 MinecraftにおけるドラクエMODである「DQMV」「DQMIV」「DQM in 不思議のダンジョン」を簡単かつ手軽に導入できるソフト「DQM Installer」の使い方を解説します。このMODはかなり古いMinecraftバージョンをベースとしているため、動かすにはかなりの手数が必要になります。しかし、このソフトがそれらを全部やってくれます。 事前準備 まず、ランチャーがWindows 10/11版のみインストールされていることを確認します。まず、スタートボタンを右クリックし、「インストールされているアプリ」をクリックします。 次に、表示されたアプリ一覧の検索窓で「minecraft」と検索し、表示されたランチャーの種類を確認します。 緑色のアイコンのものはWindows 10/11版です。灰色のアイコンのものはレガシーランチャーです。両方がインストールされているとインストーラーがうまく動作しないことがあるため、必ず灰色のアイコンのレガシーランチャーは事前にアンインストール してください。Windows 10/11版がインストールされていない場合はこちらからインストールしてください。 導入手順 1. インストーラーをダウンロード 以下のボタンからダウンロードページに移動してください。 :::btn ダウンロードページへ ::: 下のようなページに飛ぶと思うので、ご利用のOSに合うものをクリックしてダウンロードしてください。 そして、ダウンロードしたファイルを解凍 してください。「すべて展開」をクリックで解凍できます。(※macOS版は不要です) 2. インストーラーを起動する Windowsの場合 中にある「 dqm_installer_flt.exe 」をダブルクリックして起動してください。 ここで、以下のような画面が表示される場合があります。これは、証明書がソフトに入っていないことに起因するもので、Windowsがウイルスを検知して出しているものではありませんので、「詳細情報」をクリックして「実行」をクリックしてください。 ※起動できない場合、https://aka.ms/vs/17/release/vc_redist.x64.exe からMicrosoft Visual C++ 再頒布可能パッケージをダウンロードし、インストールしてください。 macOSの場合 dmgイメージをダブルクリックしてマウントし、中にある「dqm_installer_flt.app」をダブルクリックして起動してください。 Linuxの場合 中にある「dqm_installer_flt」をダブルクリックして起動してください。 3. インストール前の準備 起動するとこんな画面になります。(v1.1.0現在) まず、Step 1に表示されている問題を解決していきます。ここに表示される内容は環境によって異なりますが、主要なもののみ解説します。 「xxx」を空にしてください 「.minecraftフォルダーを開く」をクリックし、その中にある「xxx」フォルダーを別の場所へ退避させるか削除して、フォルダーが存在しない、もしくは空になっている状態を作ってください。 これらのディレクトリにDQMに関係のないファイルが入っていると、ゲームが正常に動作しなかったり、間違ったセーブデータを開いてしまいワールドが破損するといった事故が起こる可能性があります。 起動構成「xxx」のゲームディレクトリを設定してください ランチャーを起動し、「起動構成」から、上で表示された起動構成にゲームディレクトリを設定してください。詳しくはこちらの記事を参照してください。 DQMのベースとなっているMinecraft 1.5.2はゲームディレクトリに対応していないため、ほかの起動構成にゲームディレクトリを設定することでMODやワールド同士の衝突を避ける必要があります。 4. 1.5.2の起動 インストーラーを見ながら手順を踏んでください。注意事項としては、 プロファイル作成後は ランチャーを再起動する必要があります。 1.5.2起動後クラッシュする場合がありますが、インストール上問題ありませんので、「×」をクリックして閉じてください。 5. 必要なファイルのダウンロード インストーラーにあるリンクから「DQM 本体MOD/前提MOD/SE・BGM」「Forge 1.5.2-7.8.1.738 Universal」をダウンロードしてください。 ※ダウンロード先はすべて同じフォルダーにしてください。 DQMは、公式サイトから遊びたいものを選んでダウンロードしてください。基本はDQMVだと思います。 本体/前提MODに関しては、遷移先に2個ファイルが表示されるので、両方ダウンロードしてください。 BGMもダウンロードしてください。これはすべて共通です。 Forgeは5秒待つと右上にSKIPが表示されるので、クリックします。 6. ファイルの割り当て インストーラーに戻り、「ファイルから自動的に認識」をクリックし、ファイルのダウンロード先フォルダー(通常はダウンロードフォルダー)を選択します。すると、自動的に適切なファイルが当たります。 ちゃんと割り当てられたファイルが正しいことを確認したら次に進んでください。 ※当たらなかった場合は、手動で選択してください。 7. 導入推奨MODの選択 説明を読んで、必要だと思ったらチェックを入れてください。自動的に導入されます。 8. インストール インストールの準備ができたら、「インストール」をクリックしてください。インストール中、必要なファイルのダウンロード(約50MB)を行うのでご注意ください。 DQMを起動する インストールが完了したらランチャーを再起動します。新しい起動構成が作成されているので、選択して「プレイ」をクリックしてください。 これでDQMの導入は完了です。お疲れ様でした。 Apple Silicon Macでの表示不具合について Apple Silicon MacでMinecraft 1.5.2を起動すると色の表示がおかしくなる不具合が存在します。以下のURLに修正方法が記載してあるのでご覧ください。 https://github.com/chika3742/dqm_installer_flt#apple-silicon-mac%E3%81%A7%E3%81%AE%E8%A1%A8%E7%A4%BA%E3%81%AE%E4%BF%AE%E6%AD%A3 一部で報告が上がっている現象について 一部において、「MOBやブロックの色がおかしくなる」という現象の報告を頂いています。 どうやら旧来のバージョンのMinecraftではまれに発生するらしく、グラフィックドライバーが原因の可能性があるようです。コメント欄に対処法らしきものを書いてくださった方(「奈々菜」というユーザー名)がいるため、探してみてください。 最後に 説明が足りていない部分があったり、ソフトの不具合を疑っている場合は、遠慮なくコメントを送ってください。皆さんのコメントを参考に、随時記事に説明を追加したり、記述を改善したりしております。

【重要】SubmonにおけるTwitterサインイン機能の削除について

2023/06/11・Submonのお知らせ

いつもAndroid/iOS用アプリ「Submon」をご利用いただきありがとうございます。 この度「Submon」においても Twitterサインイン機能を削除 することといたしましたので、お知らせします。現在すでに「requestTokenRequestFailed」と表示されてサインインに失敗する状況です。 経緯 こちらの記事に記載のものと同等です。アプリとしてアップデートを配信する必要があったことや、ほかのプロダクトの開発で忙しかったため、Twitterサインイン機能が利用不能になってから少々お時間をいただきました。 今後の対応 Twitterサインイン機能を削除し、アカウントの連携・連携解除機能を新たに追加する予定です。現在Twitterサインインを利用してログインされている方は、一度アプリをアンインストールしたりログアウトしたり するとその地点で 二度とアカウントにログインすることができなくなってしまう ため、ご注意ください。 該当の変更は今後リリース予定のv1.2.0で実装予定です。公開日時は未定です。 利用者で必要な対応について 上述のアカウントの連携・連携解除機能を利用して、GoogleもしくはAppleアカウントとの連携を行ってください。また、メールアドレスログインに変更する手段の提供予定は現状ありません。

【重要】従来版(旧バージョン)「DQMインストーラー」開発/サポート終了と、新バージョンへの移行について

2023/06/11・お知らせ

こんにちは、chikaです。 この度、旧バージョンのDQMインストーラーのサポートと配布を終了し、新バージョンへ移行する形とすることをお知らせします。 経緯 動画投稿地点からかなり環境が変わり、現在の(従来の)ソフトとコメント等での説明だけではカバーしきれなくなってきました。そのため、せっかくならと完全リニューアルすることとしました。UIを格段にわかりやすいフロー形式にし、導入推奨MODもチェック1つで導入可能です。また、macOS/Linux向けのインストーラーも統合しました。 今後について Windows版(動画・配布ページ)およびmacOS/Linux版(動画・配布ページ)はサポートを終了し、ソフト本体は 2023/6/30 に配布を終了する予定です。今後は新バージョンをご利用ください。 利用方法の解説はこちら

【GitHub Actions】Windowsホストで$GITHUB_OUTPUTが使えない

2023/06/08・役立ち情報

ハマった 以下のように、WindowsホストランナーでOutputしようとするとうまくいかない。 jobs: build_windows: runs-on: windows-latest steps: - uses: actions/checkout@v3 - id: test run: echo "FILENAME=test.zip" >> "$GITHUB_OUTPUT" - run: echo ${{ steps.test.outputs.FILENAME }} 対処法 WindowsホストのシェルであるPowershellにおいては、環境変数にアクセスするにはenv:のプレフィックスが必要な模様。 run: echo "FILENAME=test.zip" >> "${env:GITHUB_OUTPUT}"

【Nuxt3】SSG時に、サイトマップを動的ルートも含めて全自動で生成する方法

2023/06/01・役立ち情報

経緯 Google Search Index用にサイトマップを作りたくなった。サイトマップの生成自体はsitemapパッケージでどうにかなるが、いちいち全部のルートを取得してリストを作るのは面倒臭い。そこで、nitroに備わっているクローラーを使って、サイト内にあるリンクからルート一覧を生成してみる。 nitroのクローラーは、HTMLに含まれる、「/」で始まるhref属性を含むタグを自動的に認識し、リンク先のルートを生成できる。 壁 nitroでは、クローラーによって自動的に動的ルートのページが生成される。しかし、クロール後のルート一覧を取得する方法が存在しない。そこで、ページを生成するタイミングでフックを挟んで、変数のリストに追加していくことにする。 コード まず、sitemapパッケージをインストールしておく。 yarn add sitemap 次に、nuxt.config.tsのdefineNuxtConfigの上に以下を追加する。 const hostname = "<hostname>" const routes: string[] = [] export default defineNuxtConfig({ ... 続いて、defineNuxtConfigの中に以下を追加する。 export default defineNuxtConfig({ ... nitro: { hooks: { "prerender:route"(route) { routes.push(route.route) }, close() { if (routes.length > 0) { const links: SitemapItemLoose[] = routes.map(route => ({ url: route, })) const stream = new SitemapStream({ hostname, }) return streamToPromise(Readable.from(links).pipe(stream)) .then((sm) => { return fs.writeFileSync("dist/sitemap.xml", sm.toString()) }) } }, }, }, ... }) 9行目のif (routes.length > 0)は、これを入れないとnuxt prepare(yarn install)を実行した際にもこの処理が走ってしまうために入れています。