Xamarin.FormsをMAUI移行中にツマった話

こちらの続き。

Visual Studio2022と、.NET 8が正式リリースされました。
ので、Xamarin.Formsアプリを、新規作成したMAUIプロジェクトに移行していく。
そこで修正が必要だったり、詰まったところをまとめていく。

なお、2024年1月時点のお話です。

Device系は置換が必要

これに限らず、ちょこちょこ置換が必要なものは多い。
ので、とりあえず一例。

BeginInvokeOnMainThread関数

Microsoft.Maui.Controls.Application.Current.Dispatcher.Dispatchに置き換えるだけ。

StartTimer関数

Application.Current.Dispatcher.StartTimerに置き換えるだけ。

Device.RuntimePlatform関数

Microsoft.Maui.Devices.DeviceInfo.Platformに置き換えるだけ。

地図系

地図の操作自体は公式でしっかり解説されてて、移行することはいっぱいあるけど、そんなに大変じゃない感じ。

Pinの外観を変えたい

これは公式に書いてないが、下記のやり方でいけました!ありがたや!
https://vladislavantonyuk.github.io/articles/Customize-map-pins-in-.NET-MAUI/

あと、CreateMauiAppの修正でちょっとはまった。
UseMauiMaps→ConfigureMauiHandlersの順が大事っぽい。

        public static MauiApp CreateMauiApp()
        {
            var builder = MauiApp.CreateBuilder();
            builder
                // Mapを利用する
                .UseMauiMaps()
                // カスタム用レンダラーを利用する
                .UseMauiCompatibility()
#if ANDROID
                // Android用クラスインスタンスで生成
                .UseMauiApp<Droid.HogeAppDroid>()
                // Activityインスタンスを渡す
                .ConfigureLifecycleEvents(events =>
                {
                    events.AddAndroid(android => android
                        .OnCreate((activity, bundle) => Droid.HogeAppDroid.OnCreate(activity, bundle))
                        );
                })
                // Android用カスタムコントロールのレンダラーを登録
                .ConfigureMauiHandlers((handlers) =>
                {
                    handlers.AddHandler(typeof(Microsoft.Maui.Controls.Maps.Map), typeof(Droid.CustomMapHandler));
                })
#elif IOS
                // iOS用クラスインスタンスで生成
                .UseMauiApp<iOS.HogeAppIOS>()
                // iOS用カスタムコントロールのレンダラーを登録
                .ConfigureMauiHandlers((handlers) =>
                {
                    handlers.AddHandler(typeof(Microsoft.Maui.Controls.Maps.Map), typeof(iOS.CustomMapHandler));
                })
#endif
                .ConfigureFonts(fonts =>
                {
                    fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                    fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
                });
#if DEBUG
    		builder.Logging.AddDebug();
#endif

            return builder.Build();
        }

ただ、後からラベルだけ変えるとかはできないっぽい?
変えたきゃPinのインスタンスごと作り直せばいける。

移動は表示してから?

位置の移動処理を実装しておいても、エラーにはならないけど、map.IsVisible = Falseの状態だとAndroidは効果がなかった。
プロパティの変更イベントハンドラを実装して、map.IsVisibleがTrueに変更されたら、別スレッドで1秒ほど待機→移動って処理にして回避。なんでや。

住所情報

以前は文字列で1発でとれたけど、Placemarkクラスの各プロパティを連結する必要がある。
これがAndroidとiOSでもかなり取れ方が違うっぽい。
Androidは同じ情報が複数プロパティに反映されがち。
分けて実装する必要がありそうだった。

/// <summary>
/// 住所更新(ラベル)
/// </summary>
private async Task UpdateAddress( Location pos)
{
    string address = "";


    // 住所文字処理
    try
    {
        // 住所文字列の取得
        IEnumerable<Placemark> addresses = await Geocoding.Default.GetPlacemarksAsync(pos.Latitude, pos.Longitude);
        Placemark placemark = addresses?.FirstOrDefault();
        // 結果をとる。
        if (placemark != null)
        {
            // AndroidとiOSで結果が異なるので作り方もわける
            // 郵便番号はいらない
            if (!string.IsNullOrEmpty(placemark.CountryName))
            {
                // 国名はつける(日本だけ削除)
                address = placemark.CountryName.Replace("日本", "");
            }
OID
                    // 郵便番号はいらない
                    address += placemark.AdminArea          // 都道府県
                                + placemark.Locality        // 市区町村
                                + placemark.SubLocality     // サブの局所性
                                + placemark.Thoroughfare;   // 町名

                    // 丁目
                    // 四国の海だとCountryNameとFeatureNameに"日本"が設定されて、他のプロパティは空なので
                    // AdminAreanoの空チェックも行う
                    if (!string.IsNullOrEmpty(placemark.AdminArea) && !string.IsNullOrEmpty(placemark.SubThoroughfare))
                    {
                        // 数字が連結されるならハイフンを挟む
                        if (IsIntParseOneChar(address, false) && IsIntParseOneChar(placemark.SubThoroughfare, true))
                        {
                            address += "‐";
                        }
                        address += placemark.SubThoroughfare;
                    }

                    // 号
                    if (!string.IsNullOrEmpty(placemark.AdminArea) && !string.IsNullOrEmpty(placemark.FeatureName))
                    {
                        // 数字が連結されるならハイフンを挟む
                        if (IsIntParseOneChar(address, false) && IsIntParseOneChar(placemark.FeatureName, true))
                        {
                            address += "‐";
                        }
                        address += placemark.FeatureName;
                    }

            address += placemark.AdminArea          // 都道府県
                        + placemark.Locality        // 市区町村
                        + placemark.Thoroughfare;   // 町名&丁目

            // 番地以降
            // 四国の海だとCountryNameとFeatureNameに"日本"が設定されて、他のプロパティは空なので
            // AdminAreanoの空チェックも行う
            if (!string.IsNullOrEmpty(placemark.AdminArea) && !string.IsNullOrEmpty(placemark.SubThoroughfare))
            {
                // 数字が連結されるならハイフンを挟む
                if (IsIntParseOneChar(address, false) && IsIntParseOneChar(placemark.SubThoroughfare, true))
                {
                    address += "‐";
                }
                address += placemark.SubThoroughfare;
            }

            // FeatureNameはSubLocality、Thoroughfare、SubThoroughfareがnullの場合
            if (string.IsNullOrEmpty(placemark.SubLocality) && string.IsNullOrEmpty(placemark.Thoroughfare) &&
                string.IsNullOrEmpty(placemark.SubThoroughfare) && !string.IsNullOrEmpty(placemark.FeatureName))
            {
                // 数字が連結されるならハイフンを挟む
                if (IsIntParseOneChar(address, false) && IsIntParseOneChar(placemark.FeatureName, true))
                {
                    address += "‐";
                }
                address += placemark.FeatureName;
            }
        }
    }
    catch (Exception ex)
    {
        DebugLog.Log(ex.Message);

        address = "";
    }

    // ピンを作り直してラベルに住所反映したり・・・
}

/// <summary>
/// 数字始まりまたは数字終わりかチェック
/// </summary>
/// <param name="str">チェックする文字列</param>
/// <param name="isFirst">先頭をチェックするならtrue、末尾ならfalse</param>
/// <returns>数字であればtrue</returns>
private bool IsIntParseOneChar(string str, bool isFirst)
{
    // 空ならfalse
    if (string.IsNullOrEmpty(str))
    {
        return false;
    }

    // 先頭または末尾1文字をチェック
    string check = "";
    if (isFirst)
    {
        check = str.Substring(0, 1);
    }
    else
    {
        check = str.Substring(str.Length - 1, 1);
    }

    // Strings.StrConv(check, VbStrConv.Narrow, 0)は使えないので半角に置き換えは手作業で行う
    check = check.Replace("0", "0").Replace("1", "1").Replace("2", "2").Replace("3", "3").Replace("4", "4")
                .Replace("5", "5").Replace("6", "6").Replace("7", "7").Replace("8", "8").Replace("9", "9");
    // 判定
    return int.TryParse(check, out _);
}

レイアウトまわり

AbsoluteLayoutにすべきか、Gridにすべきか

レイアウトが崩れたら、まず怪しむべき原因はこれな気がする。

重要
HorizontalOptions プロパティと VerticalOptions プロパティは、AbsoluteLayout の子には影響しません

https://learn.microsoft.com/ja-jp/dotnet/maui/user-interface/layouts/absolutelayout?view=net-maui-8.0

全部、ちゃんと子要素のサイズも指定するか、Gridに置き換えるかは、ケースバイケースな気がする。

GridはGridで怪しくて、列や行の高さをプログラムから変更するコードを書くと、Debug版では動くのに、Release版ではアプリが吹っ飛んだ。
また、特定の端末ではGridの隙間を0にしているのに微妙に空いてしまったり。
画像を配置して厳密に指定したいなら、AbsoluteLayoutの方が良い気がする。

あと、確証はないけど、OnSizeAllocatedをoverrideして、base.OnSizeAllocatedの呼び出しタイミングは最初よりは諸々処理した後が良さげ?

透明なラベルはGridを跨ぐべからず

タップ用に透明なラベルを配置して、ボタンの画像を正常時・タップ中で切り替えたかった。
が、Gridの行や列を跨いで配置すると、透明ラベルが表示しているテキストの領域に引きづられる = つまりTextが空文字だとラベル領域が消えた。
これが、iOSやAndroid 9では起こらず、手持ちのAndroid 13で、配置したボタンの一部でしか出ない。なんでや。

Aspect=”AspectFill”が効くようになった?

Aspect=”AspectFill”(アスペクト比を固定する)を選択してるのに、Xamarinは効果がなかったっぽい?
MAUIではちゃんと適用されたことがきっかけで、画像サイズがいまいちだったものが見つかった。

ステータスバーの色

XamarinはiOSのステータスバーもホームバーも無視して突き抜けてた。
けど、これが土台のViewの背景色が反映されつつ、突き抜けなくなった。
でも、ステータスバーの色はむしろ突き抜けててほしい。
Community.Toolkit.Maui.CoreをNugetからインストールして、プログラムから変更するようにした。

/// <summary>
/// ステータスバーにアプリ上部の色を適用する
/// </summary>
private void updateStatusBarColor()
{
    // 失敗してもエラーにはしない
    try
    {
        CommunityToolkit.Maui.Core.Platform.StatusBar.SetColor(Color.FromRgb(0x66, 0x66, 0x66));
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
}

Android.App.AlertDialogの色(未解決)

Android.App.AlertDialog.Builderを使ってダイアログを出すとき、今までは背景のみ白を指定して、問題なかった。
それが、Xperia Ace Ⅱだと背景もボタンが白くて見えなくなっちゃった。

じゃあ、指定もメンドイし、Styleの指定をやめて、デフォルトで行こう。
Pixel6で、背景もボタンも黒くて見えない。なんでや。

なので、ボタンは黒を明示的に指定するようStyle.xmlを修正。
こちらを参考にさせてもらいました。
https://qiita.com/hanaaaa/items/dd58ab6946d8ff4a0994

<?xml version="1.0" encoding="utf-8" ?>
<resources>
  <style name="MyAlertDialogStyle" parent="Theme.AppCompat.Light.Dialog.Alert">
    <item name="buttonBarPositiveButtonStyle">@style/PositiveButtonStyle</item>
    <item name="buttonBarNegativeButtonStyle">@style/NegativeButtonStyle</item>
    <item name="android:background">@android:color/white</item>
    
  </style>

  <!-- AlertDialogPositiveButtonStyle -->
  <style name="PositiveButtonStyle" parent="Widget.AppCompat.ButtonBar.AlertDialog">
    <item name="android:textColor">@android:color/black</item>
    <item name="backgroundTint">@android:color/white</item>
  </style>

  <!-- AlertDialogrNegativeButtonStyle -->
  <style name="NegativeButtonStyle" parent="Widget.AppCompat.ButtonBar.AlertDialog">
    <item name="android:textColor">@android:color/black</item>
    <item name="android:textColor">@android:color/white</item>
  </style>
</resources>

これで、Xperia Ace Ⅱだと白背景+ボタン文字が緑に、Pixel6だと白背景+ボタン文字が黒に。統一せぇ。
そしてXperia Aceで背景もボタンが白くて見えなくなっちゃった。なんなの…。

起動&クラッシュ系

イベントのBeginInvoke(context, intent, null, null)

コンパイルエラーにはならないんだけど、実行すると吹っ飛ぶ。
こちらを参照。
https://learn.microsoft.com/ja-jp/dotnet/core/porting/net-framework-tech-unavailable
https://devblogs.microsoft.com/dotnet/migrating-delegate-begininvoke-calls-for-net-core/

Task.Run(() => { イベント名?.Invoke(context, intent); });に置き換えるだけ。

アプリのストレージを削除すると起動できなくなる

これはDebug版だけで起こる。
「Fast Deployment の使用」を無効にするか、一度アンインストールして配置し直すこと。
https://stackoverflow.com/questions/42336546/xamarin-android-application-crashed-after-clear-data-in-settings

iPhoneでのデバッグは、権限を付与してから?

ちゃんと裏は取ってない。
iPhoneでデバッグ実行するとき、一度起動して、必要な権限はすべて許可しておかないと、デバッグ起動してもすぐにアプリが落ちる気がする。


他にもニッチなIT関連要素をまとめていますので、よければ一覧記事もご覧ください。

返信を残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)