VSとMacのペアリング時のメモリ不足をキャッシュ削除により解消

MAUIでiPhoneアプリを開発する場合、ビルドのためにVisualStudioとMacをペアリングする必要があります。
アプリ開発の終盤、そのペアリング時やビルド時にMacのメモリが不足している旨の警告が出るないしエラーが出て失敗することが頻発しました。
その解決方法の共有となります。

タイトルの通り、キャッシュの削除により解決できました。
Windows・Macそれぞれで削除が必要です。

・Windowsでは以下の2フォルダがキャッシュなので削除
%localappdata%\Temp\Xamarin\XMA
%localappdata%\Xamarin\MonoTouch(←こちらはMacの接続先情報なので不要かもですが念のため)

・Macでは以下のフォルダがキャッシュなので削除
$HOME/Library/Caches/Xamarin/XMA

これで警告やエラー出ずにペアリング・ビルドできるようになりました。

以下はキャッシュが溜まる要因の予想です。
・ペアリング時にVisualStudioのバージョンないしMAUIのバージョンごとにキャッシュが溜まる
・異なるPCや異なるプロジェクトでペアリングを行うと、バージョンが異なりがちでその度にキャッシュが溜まる
くらいにふんわり考えています。

若干面倒なのであまりキャッシュ溜まらないことを願っています。
今後は1プロジェクトを改造・保守するのが主なので、上記予想が正しければ大丈夫なのではないかと思うのですが。。。

以上です。

[参考]
Clearing the Broker, IDB, Build, and Designer Agents on the Mac – Microsoft Learn
c# – Can't connect VS2022 .Net Maui Project with Mac – Stack Overflow
macos – I can't connect to Mac with Xamarin Mac Agent from Visual Studio 2015 – Stack Overflow

GooglePlayストアページからアプリのバージョンを取得する方法【令和最新版】

はじめに
Mauiのアプリ開発が完了しました。

開発終盤に開発した機能に、
Androidアプリにて現在のアプリのバージョンがストアのバージョンよりも古い場合、
古いバージョンは使用して欲しくないのでアプリを終了し、ユーザに更新を促す処理があります。

処理は以下のような流れで、
0. アプリを起動
1. ストアのアプリバージョンを取得
2. 現在のバージョンと比較
3. 現在のバージョンが古ければ、ストアページに遷移してアプリを終了

この内の1の工程におけるGooglePlayストアからアプリのバージョンを取得する処理についての共有記事となります。
重要なことなので先に言っておきますと、
デバイスによりバージョンが異なるアプリ(大手のアプリの多く、YouTubeやGoogleChromeなどなど)の場合は取得できません。

先行事例
iPhoneのAppStoreアプリの場合、iTunesAPIによりバージョンが取得できるので、
GooglePlayストアにも該当するAPIがないか調べてみたのですが見つからず、
代わりにストアページのhtmlを取得してパースする方法が見つかりました。
Google Playにあるアプリのバージョンを取得したい – すなばいじり

これで解決かと思い、ストアページのhtmlを覗いてみたところ、
どうも記事の投稿時点(6年前)と今とでは形式が異なるようで、
パースする正規表現に関しては考え直す必要がありました。

手法
考え直してC#にて実装した方法が以下のやりかたです。

    /// <summary> ストア上のアプリバージョンを取得します。 </summary>
    public async Task GetStoreAppVersion(string appPackageId) {
        // 毎回HttpClientのインスタンスを作り直すのは良くないそうですがサンプルコードなので
        using var httpClient = new HttpClient(); 

        // ストアページのhtmlをhttpで取得してバージョンをパースする
        var playStoreUrl = $"https://play.google.com/store/apps/details?id={appPackageId}";

        // Getリクエストを投げて、結果のContentをstringで取得する処理(実装省略
        var content = await GetContent(httpClient, playStoreUrl);

        // バージョン部 (?\d+\.\d[\d\.]*): 数字1個以上 ドット1個 数字ないしドット0個以上
        // 一致例:<script class="ds:0" nonce="EssQcMMS9rrE4BPTbLD7PA">AF_initDataCallback( ...... ],[[[1.0.0]],[ ...... );</script>
        var match = Regex.Match(content, @"<script class=""ds:\d+"" nonce=""[^""]+"">AF_initDataCallback\(.+?\[\[\[""(?\d+\.\d[\d\.]*)""\]\].+?\);</script>");

        if (match.Success.Not()) return null;

        var ver = match.Groups["ver"].Value;
        return new Version(ver);
    }

この実装により、数字とドットからなる一般的な記法のバージョンをパース可能です。
※バージョンが特殊な記法の場合はバージョン部の正規表現を変える必要があります。
 また、バージョンがデバイスにより異なる場合も取得できません。
 一般向けの大手のアプリはデバイスにより異なる場合が多く取得できないので、
 残念ながら汎用性はないです。
※正規表現文字列は変数化しようかと思ったのですが、
 VSがいい感じに見やすく色分けしてくれたのでそのままにしました

取得例
例として先行事例での対象アプリ、Nintendo Switch Onlineから取得してみます。
引数appPackageIdに”com.nintendo.znca”を与えてGetStoreAppVersionを呼ぶと、
html内の以下の記述にマッチし、バージョン2.10.1が取得できます。

<script class=”ds:0″ nonce=”EssQcMMS9rrE4BPTbLD7PA”>AF_initDataCallback( …… ],[[["2.10.1"]],[ …… );</script>

ストアのページでバージョンを確認してみると実際に2.10.1と表示されているので、
正しくバージョンを取得できていることがわかります。
(この表示から取得するので、ここに”デバイスにより異なります”と記載されている場合は取得できません。)

おわりに
アプリの更新を促す処理の実装のために、
GooglePlayストアページのhtmlからアプリのバージョンをパースして取得する方法の
令和最新版(24/8/16現在)についての共有でした。

この方法はデバイスによりバージョンの異なるアプリは取得できませんが、
自社で開発するアプリならバージョンを統一できるので問題なく使用できます。

今後は工場向けのモバイルアプリも増えていくかと思いますので、
その開発の際にも役に立つ機能です。

ちなみに更新を促す機能を実装する手段としては他に、
Google Play CoreライブラリのIn-app updates機能を使用して
バージョンチェックから更新処理までの一連の流れを
ライブラリの機能で実装する方法もあるようでした。
おそらくそちらが主流だと思われたのですが、
以下の理由などにより泣く泣く不採用(バージョン取得の方を採用)としました。

・ライブラリの調査にかかる時間が不透明
・デバッグしながらの開発ができず開発コストが増える
(GooglePlayConsoleにリリースビルドしてアップロードしないとおそらくテストできないため)
・iPhoneアプリにて既にitunesAPIでバージョン取得・自前で比較・ストアに遷移まで行っており、
 バージョン取得の方法であれば実装の共通化が可能でその分開発コストが下がる

今の方法だとストアのhtmlの形式が変更される度に
バージョンをパースする部分をメンテしないといけないので、
あまり変更されないと、、、良いな、、、とGoogle本社の方角に向けてお祈りしております。

以上です。

夏バテ

夏が始まってしまい暑くなって参りましたが、皆さんいかがお過ごしでしょうか。

自分は悲しきかな既に夏バテ気味でおります。
暑さで体力を奪われると体調を崩したり活動に支障を来したりで困るので、
早く涼しくなって欲しいものです。

皆さんは夏バテに負けずに元気に過ごされることを祈っております。

MAUIにてFirebase使用にあたり困ったこと ~VSのビルドアクションの選択肢にGoogleServicesJsonが出て来ない~

先日、MAUIのAndroidアプリにてプッシュ通知を実現するため、
Firebase Cloud Messagingを用いる方法を調査していました。
その際に詰まったことがあったので共有します。

Firebaseから取得した設定ファイル google-services.json をプロジェクトに含める必要があるのですが、
含めた後にプロパティからビルドアクションをGoogleServicesJsonに変更しないと
Firebaseの処理が動作しません。

しかしビルドアクションの選択肢にGoogleServicesJsonが出てきませんでした。

これは定番の困りごとのようで、調べてみると解決策がいくつか出てきたのですが
自分の環境ではどれも上手く行かず、ようやく上手く行ったのは以下の手順による方法でした。

1. nugetで以下のパッケージをインストール
※1つずつ試してはないので不要なもの含まれている可能性有

Xamarin.Firebase.Common
Xamarin.Firebase.Config
Xamarin.Firebase.Iid
Xamarin.GooglePlayServices.Base
Xamarin.GooglePlayServices.Basement
Xamarin.GooglePlayServices.Tasks

2. .csprojに以下を直接記述して、対象のパッケージの.targetファイルをインポート


<Import Project="C:\Users\<my user>\;.nuget\packages\xamarin.googleplayservices.basement\118.3.0.1\buildTransitive\net7.0-android33.0\Xamarin.GooglePlayServices.Basement.targets" Condition="Exists('C:\Users\<my user>\.nuget\packages\xamarin.googleplayservices.basement\118.3.0.1\buildTransitive\net7.0-android33.0\Xamarin.GooglePlayServices.Basement.targets')" />

3. 2の記述にてnugetフォルダパスが個人のローカルパスになっているため、マクロに置き換え
※個人プロジェクトなら不要です

C:\Users\<my user>\.nuget\packages\ → $(NuGetPackageRoot)


<Import Project="$(NuGetPackageRoot)xamarin.googleplayservices.basement\118.3.0.1\buildTransitive\net7.0-android33.0\Xamarin.GooglePlayServices.Basement.targets" Condition="Exists('$(NuGetPackageRoot)xamarin.googleplayservices.basement\118.3.0.1\buildTransitive\net7.0-android33.0\Xamarin.GooglePlayServices.Basement.targets')" />

これで無事ビルドアクションの選択肢にGoogleServicesJsonが出て来て、
Firebaseのプッシュ通知を動作させることができました。

以下のページにより解決に辿り着けました。感謝します。
xamarin – Can't set Build Action to GoogleServiceJson – Stack Overflow

.NET MAUIへの移行

今月とうとうXamarinのサポートが終了しましたね。
後継のクロスプラットフレームワークである.NET MAUI[1]に移行するため、
調査や準備を進めています。

現行のアプリのUIを1から新規プラットフォームで構築し直すのは大変です。
しかし、今回は以下の理由などにより、カメラなどネイティブの機能が必要ない箇所を除いて、
WebViewにhtmlを表示するWebアプリ化を行うことになりました。
・サーバーと通信するアプリである
・Webアプリの構築基盤が既に存在する
このWebアプリ化を移行前に行うことにより、MAUIへの移行もスムーズに行えると思われます。

MAUIに関しては、3月に手が空いていたので業務時間内に調査・勉強して、
実機でアプリを実行できるところまで進んでいます。
WPFと同様xamlでUIを記述する形式なため、WPF開発者ならスムーズに開発できる印象を受けました。
VisualStudioをMacとペアリングすることで、開発まではWindows上で行えるのも便利で良いですね。

MAUIの開発が進みましたらまた何か書こうと思います。
以上です。

[1].NET Multi-platform App UI. 2022年に正式リリース。

VisualStudioのGitでブランチの変更内容を確認

Gitでコミットをする場合、都度ざっくりコミット内容のチェックは行うのですが、
とは言え見落としなどしてしまうことがあります。

なのでブランチをmasterにマージする前に全ての変更を最終確認したいと思っていたのですが、
VisualStudioのGitなら以下の手順で簡単に確認できることに今更気が付きました。

1. VS上部のGitからブランチの管理を開く
2. ウィンドウ左部のブランチにてmasterを右クリックして対象のブランチにmasterをマージ

3. ウィンドウ右部のローカル履歴にてブランチの最新のコミット(masterをマージしたコミット)と
    masterの最新のコミットを同時に選択し、右クリックしてコミットの比較

小規模かつ短期間のブランチなら都度確認だけでも問題ないかもしれませんが、
開発が長期に渡った場合や、ブランチ内で修正を何度も行った場合などは、
ゴミとなるコードが残っていないかや意図せず破壊的変更を行ってしまっていないかなど
最終確認するのが良いのかなと思っています。
以上です。

C#で扱える環境変数の種類とその取得/設定に関して

案件で環境変数への値の取得/設定に関して少し調査を行ったので、
ブログ用に少し内容増やして調査結果以下に記載します。

C#において扱える環境変数の種類(EnvironmentVariableTarget)は以下の3つです。

Process: プロセスごとの環境変数

User: ユーザごとの環境変数
(システムの詳細設定から確認可能、レジストリ値 HKEY_CURRENT_USER\Environment内)

Machine: PC共通のシステム環境変数
(システムの詳細設定から確認可能。レジストリ値
 HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment内)

取得、設定、削除(nullを設定)は以下のメソッドにより可能です。

string? Environment.GetEnvironmentVariable (string variable, EnvironmentVariableTarget target);
void Environment.SetEnvironmentVariable (string variable, string? value, EnvironmentVariableTarget target);

以下の表に、取得/設定時の種類ごとの主な違いをまとめてみました。

以下知見や所感です。
・ユーザ及びマシンは、プロセスの再起動など不要で、
 同時に起動している異なるプロセスにて共有可能でした。

・管理者権限必要なのはマシンだけでした。
 ユーザも内部的にはレジストリ値なので権限必要に思えたのですが、
 HKEY_CURRENT_USER下のキーの変更は権限不要らしいです。

・ユーザとマシンは他のアプリとも共有で影響及ぼすかもなので取得や設定、削除に注意が必要です。
 また、プロセスとは異なり、システム詳細設定やレジストリエディターから
 値が容易に確認可能なため、プロキシ設定などの認証情報の書き込みを行うと
 ユーザIDとパスワードが平文で見えてしまう点も注意です。

・ユーザやマシンへのSetは2秒程度かかり、実行時暫し動作が固まったので、
 場合によってはプログレス表示必要かもしれません。
 内部でレジストリへの書き込みを行う関係か少し時間がかかるようですね。
(参考PCスペック:CPU=intel core i9-13900, RAM=32GB)

・どれを使うかは用途次第ですが、扱いやすいのは管理者権限不要なプロセスとユーザで、
 プロセス間で共有するかどうかで使い分けるのが良いかと思いました。

以上です。

Eyeshotにてクリックで3Dオブジェクトの座標を選択

弊社の案件では3Dオブジェクトを表示するのにEyeshotライブラリを使用しています。

ある機能改造にて、Eyeshotで表示している3Dオブジェクトをクリックして座標を選択し、
コードからオブジェクトを基準座標に移動させることとなりました。

クリックして座標選択可能かわからなかったため調べてみたところ、
以下のScreenToWorldというメソッドにより実現できるとわかりました。

public Point3D ScreenToWorld(Point mousePos)

これはEyeshotの3D表示コントロールであるViewportLayoutクラスに定義されたメソッドで、
引数でマウスカーソル位置のスクリーン座標を与えることで、
戻り値として、カーソル下に3D物体があればその表面のワールド座標を、なければnullを返すメソッドとなっています。

これをマウスクリックイベントに登録したメソッド内で呼んで解決……かと思ったのですが、
そうもいかず、座標選択できるものとそうでないものがあり、
どうも3Dオブジェクトが半透明(カラーにα値の設定あり)の場合はScreenToWorldがnullを返してしまうようでした。

ちらつき発生しますが、クリック時に一時的に3Dモデルを不透明(α値最大)とすると座標選択でき、
無事改造内容実現できました。

以上です。

2023年お疲れ様でした

今年も早いものでもう終わりですね。

弊社は今年も本日は午前のみ業務で午後からは大掃除を行い、仕事納めとなります。
年末年始ゆっくりと過ごしたいです。

皆様本年はお世話になりました。
来年も何卒よろしくお願いいたします。
それではよいお年をお迎えください。

C#のrecord型と、.Net5未満での使用時のエラーについて

随分前に追加されたrecord型を今更ながら使い始めました。

内部的にはクラスと同じ扱いで、プロパティは全て不変とは以前から知っていたのですが、
先日recordもstructと同様にIEquatableが実装されていると聞き、
それならDictionaryのキーなどでも使いやすく、
1行で定義を済ませられるのも楽で良いなと思い使ってみることにした次第です。

ひとまず、以下のようなrecordを定義してみました。

/// <summary> 汎用結果クラス </summary>
/// <param name="Succ"> 成功したか </param>
/// <param name="ErrMsg"> 失敗時のエラーメッセージ </param>
record Result(bool Succ, string ErrMsg = "");

/// <summary> 汎用結果クラス </summary>
/// <param name="Succ"> 成功したか </param>
/// <param name="Value"> 戻り値 </param>
/// <param name="ErrMsg"> 失敗時のエラーメッセージ </param>
record Result(bool Succ, T Value = default, string ErrMsg = "");

よくタプルで成否のbool値と成功時の戻り値、失敗時のエラーメッセージを書いていたのですが、
クラスにするほどでもないけれど一々書くには若干手間だったので、
手軽に定義・使用できて良かったです。

今のところ以下のような場合にrecordを使うのが良いのかなと思っています。
・不変にしたい
・辞書のキーとしてのみ使用する
・特にメソッドなど実装予定なし

どこで使うべきかは正直、手を動かして使ってみないことには見極められない気がしているので、
今後も機を見て使っていこうと思います。

また、業務プロジェクト(C#9.0, .Netframework4.6-4.8)でrecordを宣言すると、
次のようなエラーが出て使用できませんが、
「定義済みの型’System.Runtime.CompileServices.IsExternalInit’は定義またはインポートされていません。」
以下の記事の通り、IsExternalInitクラスを自前でinternalで定義するとエラー解消し、使用できました。
(記事タイトル通り、record型だけでなくinitアクセサも使用できるようになりました)

言語か.netのバージョンが不足しているのだと思い諦めかけたのですが、
調べてみたら解決して良かったです。

以上です。