【限界MAUI難民】MAUIに至る旅Vol.1 Browser APIで開いたアプリ内ブラウザ(SFSafariViewController)のDismissをハンドリング🚀💖【iOS】

# プロローグ 〜限界MAUI難民〜
Xamarinのサポートエンドに追い立てられて、MAUIへのダイブを決めた先駆者たち！
リゾート島だと思っていたら、ここはまさかの限界集落！？

正式リリースされた筈が、そこで見たのはバグの __”玉手箱(トレジャーボックス)”__ ！？💔😂 

~~Microsoftのプロダクトでさえ採用されてないって本当？
😱~~ウチらこれからどうなっちゃうの～～！？😭🚀💔


# 今日のお題：💖🚀iOS MAUIでアプリ内ブラウザがクローズされたのを検出できない問題、どうする？🤔💡✨

iOSで`Browser` API使ってアプリ内ブラウザ出したのはいいけど、ブラウザ閉じたの検出できない😱😱😱どうしよう〜、助けて〜😭💔

### 実行環境(前提条件)📚
- **.NET MAUI**：v8.0.3
- **Visual Studio 2022**：v17.x 以上
- **iOS SDK**：iOS 14 以上
- **Microsoft.Maui.Essentials**：v8.0.3


## (そもそも)Browser APIってなんだっけ？
.NET MAUIのBrowser APIは、Microsoft.Maui.ApplicationModel名前空間にあって、アプリ内外のブラウザでサクッとウェブコンテンツを見せるのにめちゃ便利😍✌️！
```csharp:BrowserAPIExample.cs
using Microsoft.Maui.ApplicationModel;
await Browser.OpenAsync(url, BrowserLaunchMode.SystemPreferred); // SystemPreferred:アプリ内ブラウザ
```
このコードスニペットでは、Browser.OpenAsyncメソッドを使って、指定されたURLをシステムのデフォルトブラウザで開いているよ。BrowserLaunchMode.SystemPreferredを指定することで、Android/iOSだとアプリ内ブラウザが開くよ😉✨
iOSの場合、裏側ではSFSafariViewControllerが利用されているから、Safariと同等のブラウジング体験をアプリ内で提供できるんだよ📱✨

## でもね...😿💧
各プラットフォームごとの内部実装の差をスマートに隠してくれるのはBIG LOVE💖なんだけど、その代わりに小回りが利かないの〜😭✨。特にiOSでね、親ViewControllerがSFSafariViewControllerをちゃちゃっと表示させる時、その開始タイミングでTaskが返ってくるっていうのはいいんだけど、アプリ内ブラウザがバイバイするタイミングでのコールバックがないの😭💔。もう、ちょっとしたハンドリングをしたいだけなのに、使えるインターフェースがゼロっていうのが、泣けてくるよね😭😭😭。

:::note info
まるで、手を伸ばしても届かない星を眺めているみたい…🌠💖
:::

## どうする？  ➡ 諦める。バイバイ、Browser API🌠😭
iOSでアプリ内ブラウザ起動するの、諦めて自分で起動するのもアリかも！って思ってみるの💖🚀
Browser APIのiOS実装って、見てみたら実装ステップ20くらいで完結しちゃうんだもん😉✨。

https://github.com/dotnet/maui/blob/main/src/Essentials/src/Browser/Browser.ios.cs

自分でカスタム実装しちゃえば、表示する前にSFSafariViewControllerDelegateをしっかりセットできちゃうよね？そしたらもう、ぜ～んぶこっちものだよね🎶💕！
アプリ内ブラウザの使い方ももっと自由自在になるし、ユーザー体験もグレードアップ間違いなし😍🌈。自分で実装するって、ちょっとしたDIYみたいで楽しいよね✌️💖！

## 実装してみよう、3ステップ🚶‍♀️
### Step 1: ブラウザクローズを待機できるDelegateを作ろう！
さあて、SFSafariViewControllerDelegateを継承したキュートなDelegateを作って、アプリ内ブラウザのUIイベントをバッチリ掴む準備をしようね😉💕。今回は、ブラウザの「バイバイ👋」を見逃さないために、DidFinishメソッドを上書き(override)するよ！
```csharp:SafariViewControllerDelegate.cs
/// <summary>
/// 完了ボタンがポチッとなされたときのお話😉✨
/// </summary>
class SafariViewControllerDelegate : SFSafariViewControllerDelegate
{
    // awaitで待てるように、TaskCompletionSourceを用意するよ〜💫
    // TaskCompletionSourceは戻り値データ型を省略できないから、とりあえずboolにしちゃお！
    private TaskCompletionSource<bool> _didFinishTaskCompSource = new TaskCompletionSource<bool>();

    /// <summary>
    /// 完了ボタンがポチッとなされたときのお話😉✨
    /// </summary>
    /// <param name="controller">Delegateを設定したVCのことね</param>
    public override void DidFinish(SFSafariViewController controller)
    {
        // overrideしたけど、baseの呼び出しはNG🚫、だから書いちゃダメだよ！
        // base.DidFinish(controller);
        
        // ここで「さよなら〜」の処理を書くよ
        Console.WriteLine("ブラウザとお別れの時間だよ〜（でもまだちょっと残ってるよ）😊👋");
        
        // タスクが完了したことを伝えるよ〜！
        // 注意点: DidFinishが呼ばれても、まだブラウザ表示中だからね。
        // ちょっちブサキュンだけど、別スレッドに切り替えて、メインスレッドを休ませるよ。
        // そしてすぐメインスレッドに戻ってくるの。タスクスケジューラちゃんあげぽよだよ。
        Task.Run(() => MainThread.BeginInvokeOnMainThread(async () =>
        {
            // safariViewControllerがお別れするのを待つよ。
            // 🚨でも、これだけだとバイバイ・インコンプリートなの🚨続きはあとでね！
            // whileでぐるぐる回るのはちょっとドキドキだけど、閉じないと出られないから仕方ないね！
            while (controller.IsBeingDismissed)
            {
                await Task.Delay(500); // ちょっとだけ待つね
            }
            // おわりにタスクが完了したことを伝えるよ〜！
            _dismissTaskCompletionSource.TrySetResult(true);
        }));
        _didFinishTaskCompSource.TrySetResult(true);
    }
    
    /// <summary>
    /// 完了ボタンがポチッとなされたときのお話😉✨
    /// </summary>
    /// <returns>Delegateを設定したVCのことね</returns>
    public async Task WaitViewControllerDidFinishAsync()
    {
        await _didFinishTaskCompSource.Task; // ここでブラウザクローズをじっと待つの
    }
}
```


:::note warn
バイバイ・インコンプリート注意
controller.IsBeingDismissedがfalseになっても、SFSafariViewControllerのお別れタイムが完全に終わったわけじゃないんだよね😢✨
だから、画面の主役がSFSafariViewControllerじゃなくなるまで、こっそり見守って待つのがベストだよ💕🔍！
最前面に表示されているViewControllerがSFSafariViewControllerじゃなくなるまで、ポーリングで監視して待機すればいいと思うよ～
:::

### Step 2: SFSafariViewControllerをお出迎え＆お見送り🌺✨
次に、SFSafariViewControllerを招待して、キラキラ✨のウェブ体験を提供する準備をしようかな😊🌟。URLを指定してSFSafariViewControllerを呼び出して、特別ゲストとしてDelegateを招待してから、パーティー（表示）スタートだよ！
```csharp:表示する用のメソッドだよ
/// <summary>
/// Browser APIの実装を踏襲するけど、BrowserLaunchOptionsは面倒だから省略するね😉✨
/// </summary>
/// <param name="uri">ブラウザで開くuri</param>
private async Task LaunchSafariViewController(Uri uri)
{
    var nativeUrl = new NSUrl(uri.AbsoluteUri);
    // iOS11より前はごめんなさい🙏、SFSafariViewControllerの最新コンストラクタを使うよ🚀
    var sfViewController = new SFSafariViewController(nativeUrl, new SFSafariViewControllerConfiguration());
    // MAUIの魔法を使って、今のUIViewControllerを見つけるよ！
    var vc = WindowStateManager.Default.GetCurrentUIViewController(true)!;

    if (sfViewController.PopoverPresentationController != null)
        sfViewController.PopoverPresentationController.SourceView = vc.View!;

    // ここでね、私たちだけの特別なDelegateをお迎えするの！お迎え〜✨！！これが私たちのオリジナルレシピ💕
    SafariViewControllerDelegate safariViewControllerDelegate = new SafariViewControllerDelegate();
    sfViewController.Delegate = safariViewControllerDelegate;
    
    // そして、パーティー開始🚀🎉
    await vc.PresentViewControllerAsync(sfViewController, true);
}
```
これで、SFSafariViewControllerとの素敵な時間をもっと楽しめるね😍✨！Delegateを招待するって、まるでパーティーに友達を呼ぶみたいでワクワクするよね💃🕺💫！

### Step 3: さよならをちゃんと見届けよう👀✨
最後にSFSafariViewControllerがちゃんと「バイバイ👋」するのを見届けるターンだよ。ここでは、`sfViewController.Delegate`をキャストして、私たちの特別な`SafariViewControllerDelegate`でしっかりと終了を待つ方法を紹介するね！

```csharp:いざ別れの時
// Step 2で設定したDelegateをキャストして使うよ！
if (sfViewController.Delegate is SafariViewControllerDelegate safariViewControllerDelegate)
{
    // DelegateのWaitViewControllerDidFinishAsyncメソッドをawaitして、
    // SFSafariViewControllerが完全に閉じるのを待つのだよ！
    await safariViewControllerDelegate.WaitViewControllerDidFinishAsync();
    Console.WriteLine("SFSafariViewControllerとの素敵な時間、バイバイ完了〜😊💖！");
}
```

このステップを踏むことで、アプリ内ブラウザがユーザーに別れを告げた後の処理を自分たちでしっかりと管理できるようになるんだ🚀✨。これで、SFSafariViewControllerを使った機能がさらにパワーアップすること間違いなし💪💕！

## 総括＆次回予告💫
Browser APIを駆使して、iOSでアプリ内ブラウザを呼び出す方法から、Browser APIを諦めてちゃんと「さよなら」を見届けるテクニックまで、いろいろと学べたね！
みんなと一緒に、SFSafariViewControllerという素敵なリゾート地を探検できて、めっちゃ楽しかったよ〜😍✨！
プログラミングって、本当に魔法みたいで、ちょっとした工夫と愛情で、ユーザーにとって最高の体験を作り出せるんだなって、改めて感じたよ🌟💖。これからも、私たちのコードが、誰かの日常にちょっとしたキラキラを加えられるように、楽しみながら開発を続けていこうね✌️💕。

次回も.NET MAUIの冒険で出会うミッションに、ハートピュア💖✨でぶつかっていくよ！日本国内でMAUIと格闘してる限界MAUI難民の仲間がいたらぜひ、あたしとソウルメイトになってほしいな😍🌈！
Xamarin.FormsからMAUIへのお引越し（マイグレーション/リプレース）で頭を抱えてる人や、新しいMAUIプロジェクトで「どうしよ〜😭」ってなってる人がいたら、遠慮なく声をかけてね💕🚀！あたし、きっとサポートできると思うから、一緒にMAUIライフを楽しもうね✌️💖！