こんばんは。
今週はサンプル動画を作るために、試作中の画像ビューワーアプリをせこせこ開発しておりまして。
そのPCーiPad 間のデータ転送の仕組みを再設計していたもので。。。
若干時間はかかりましたが、やっとブログ記事にまとまりそうなので書きます。
試作中の画像ビューワーは当然マルチプラットフォーム 対応しようと思っていて、アプリに画像を取り込むために例えばユーザーがインポートフォルダを指定してあげてその中の画像ファイルを全部取り込むみたいなことが考えられます。インポートフォルダは複数指定できることが要件です。そこでWindows ならMicrosoft .Win32.OpenFolderDialogを無理やり使ったりできますね。(言葉足らずだったため、2024/1/29 1:14に追記)
var dialog = new OpenFolderDialog()
{
InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
Multiselect = true ,
Title = "インポートするフォルダの選択"
};
if (dialog.ShowDialog() == true )
{
await Library.Value.ImportAsync(dialog.FolderNames);
}
みたいな?
iOS やiPadOSの場合はどうでしょう。。。?
iOS やiPadOSというと、システムが画像ピッカーのようなものを用意してくれているという印象がありました。
なのでそれに乗っかります。C# コードから。
特別にNuGetライブラリをインストールする必要はなさそうです。(間違ってたらごめんなさい)
どのようなコードをかけばいいかは以下に示します。
2つクラスが必要で、一つはPHPicerViewControllerを使用するクラス。
using PhotosUI;
using UIKit;
namespace boilersControl.Maui.MultiImagePicker.Platforms.iOS
{
internal class MultiImagePicker : UIViewController
{
private PVCDelegate pickerDelegate;
public async Task<IEnumerable<string >> PresentPhotoPickerAsync()
{
var tcs = new TaskCompletionSource<IEnumerable<string >> ();
await MainThread.InvokeOnMainThreadAsync(async () =>
{
var configuration = new PHPickerConfiguration();
configuration.SelectionLimit = 0 ;
configuration.Filter = PHPickerFilter.ImagesFilter;
var picker = new PHPickerViewController(configuration);
var currentUIViewController = WindowStateManager.Default.GetCurrentUIViewController();
pickerDelegate = new PVCDelegate(tcs, currentUIViewController);
picker.Delegate = pickerDelegate;
await currentUIViewController.PresentViewControllerAsync(picker, true );
});
return await tcs.Task;
}
}
}
もう一つはPhotosUI.PHPickerViewControllerDelegateを継承するカスタムなデリゲートクラスです。
using Foundation;
using ObjCRuntime;
using PhotosUI;
using UIKit;
using UniformTypeIdentifiers;
namespace boilersControl.Maui.MultiImagePicker.Platforms.iOS
{
internal class PVCDelegate : PhotosUI.PHPickerViewControllerDelegate
{
private readonly TaskCompletionSource<IEnumerable<string >> tcs;
private readonly UIViewController _currentViewController;
public bool IsCompleted { get ; private set ; } = false ;
public PVCDelegate(TaskCompletionSource<IEnumerable<string >> tcs,
UIViewController currentViewController) : base ()
{
this .tcs = tcs;
_currentViewController = currentViewController;
}
public NativeHandle Handle { get ; }
public void Dispose()
{
}
public override void DidFinishPicking(PHPickerViewController picker, PHPickerResult[] results)
{
var _imagePaths = new List<string >();
MainThread.BeginInvokeOnMainThread(async () =>
{
await _currentViewController.DismissViewControllerAsync(true );
});
var tasks = results.Select(async result =>
{
var uiimage = await result.ItemProvider.LoadObjectAsync<UIImage>();
var pngData = uiimage.AsPNG();
var tempDirectoryURL = NSFileManager.DefaultManager.GetTemporaryDirectory();
var filename = Guid.NewGuid().ToString() + ".png" ;
var fileURL = tempDirectoryURL.AppendPathComponent(filename, UTType.CreateFromExtension("png" ));
WriteNSDataToFile(pngData, fileURL.Path);
_imagePaths.Add(fileURL.Path);
});
Task.WhenAll(tasks).ContinueWith(t =>
{
IsCompleted = true ;
tcs.SetResult(_imagePaths);
});
}
public void WriteNSDataToFile(NSData data, string filePath)
{
using (var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write))
{
data.AsStream().CopyTo(fileStream);
}
}
}
}
処理の全体的な流れ
全体的な流れは
(1) UIViewControllerクラスを継承するMultiImagePickerクラスを作成します。
(2) MultiImagePicker.PresentPhotoPickerAsyncメソッド内の処理に入り、まずTaskCompletionSource<IEnumerable>インスタンス を作っておきます。
これは後ほど説明するカスタムデリゲートクラスのDidFinishPickingメソッド内の非同期処理が完了したタイミングでPresentPhotoPickerAsyncをreturnしたいためです。
(3)メインスレッドで以下を実行します。
(3-a) PHPickerConfigurationを作成して、SelectionLimitやFilterプロパティを設定します。
SelectionLimitは取得したい写真やビデオの数の上限数です。0を指定すると無制限になります。
Filterは取得したいファイルの種類を指定します。
なんかいろいろあります。
PHPickerFilterのプロパティ一覧
(3-b)PHPickerViewControllerクラスをnewします。この時コンストラク タの引数として(3-a)で作ったPHPickerConfigurationインスタンス を渡します。
(3-c)WindowStateManager.Default.GetCurrentUIViewControllerメソッドを呼び出し、カレントUIViewControllerを取得します。
(3-d)PHPickerViewController.Delegate プロパティにカスタムデリゲートクラスのインスタンス を渡します。コンストラク タでTaskCompletionSource<IEnumerable>インスタンス と、カレントUIViewControllerインスタンス を渡します。
(3-e)非同期でPresentViewControllerAsyncメソッドをコールします。
(4)TaskCompletionSource<IEnumerable>インスタンス のTaskプロパティをawaitしつつreturnします。
特筆すべき点
private PVCDelegate pickerDelegate;
上記のような記述があると思います。なぜカスタムデリゲートクラスのインスタンス をクラスレベルで保持しなければいけないか。
PHPickerViewControllerインスタンス はMainThread.InvokeOnMainThreadAsyncメソッドに渡されるラムダ式 のスコープに囚われています。
このスコープを出ると、ガベージコレクション に回収されてしまうおそれがあります。それはPHPickerViewControllerのプロパティに入っているインスタンス も同様です。
対処しないと、そのスコープを出た後にDidFinishPickingメソッドをコールするタイミングでObjectDisposedExceptionが出てしまいます。
なのでカスタムデリゲートクラスのインスタンス は、MultiImagePickerクラスのフィールドとして持たせることで、ガベージコレクション に回収されるのを防ぎます。
カスタムデリゲートクラス
PhotosUI.PHPickerViewControllerDelegateを継承すること!
ここ重要
まずPHPickerViewcontroller.Delegate プロパティに代入する、カスタムデリゲートクラスはPhotosUI.PHPickerViewControllerDelegate を継承してください。
作ったカスタムクラスにIPHPickerViewControllerDelegateインターフェースを実装しただけでは、画像ピッカーの追加、キャンセルボタンが機能しません。
DidFinishPickingメソッド
DidFinishPickingメソッドは画像を取得した後どのような処理をするかを書きます。パラメーターのPHPickerResult[]からは、UIImageオブジェクトを取得できますがこれはあまり外に出すものではなさそう?です。つまり、MAUIアプリのコードでUIImageオブジェクトを操作することははばかられるという意味で、できればこのDidFinishPickingメソッド内でUIImageデータを一時ファイルとして保存して、そのパスを返すような仕組みにしたほうが健全かなぁ?と思います。サンプルコードではAsPNGメソッドでNSDataオブジェクトを取得してそれを一時ファイルに書き込んでいます。そして一時ファイルの複数のパスをTaskCompletionSource<IEnumerable>にセットしてあげるという感じ。
サンプルの動画
VIDEO youtu.be
次回予告
MAUIでiOS アプリをデバッグ する時にFileNotFoundExceptionが発生する問題に戸惑ったので、その解決策について書きたいと思います。