[iOS13]SceneDelegateでURL Schemeのリクエストを受取る(追記あり)

## 概要

URL Schemeで他アプリと連携して、callbackで自分のアプリに戻ってくる処理を実装しています。
iOS12までは`AppDelegate`の下記メソッドでリクエストを受け取っていましたが、iOS13で受け取れなくなっていました。

`func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool`

> [iOS13 application openUrl? |Apple Developer Forums](https://forums.developer.apple.com/thread/124132)

「`SceneDelegate`を代わりに使用する」ということはすぐわかったのですが、
まだSceneDelegateを導入していなかったこともあり少々手こずりました...
内容としてはあまり多くないですが備忘録としてまとめます。

## 環境

* iOS 13.2.3
* Xcode 11.2.1
* Swift5

## 実装

### iOS12まで

これまでのURL Schemeのリクエスト受取はAppDelegateで以下のように実装していました。
アプリのURLを外部からアクセスされた時に[application(_:open:options:)](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623112-application)
が実行されます。
[sourceapplication](https://developer.apple.com/documentation/uikit/uiapplication/openurloptionskey/1623128-sourceapplication)でURLリクエストを送信したアプリのBundleIdを判定して、
どの画面の処理へ遷移させるか分岐させていました。

```swift:AppDelegate.swift
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate
{
    // 省略

    func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
        guard let sourceApplication = options[.sourceApplication] as? String else {
            return false
        }

        if sourceApplication.hasPrefix("--他アプリのBundleId--") {
            // クエリパラメータを受けて色々処理
            // ...
        }

        return true
    }
}
```

### iOS13から

#### 1. SceneDelegateを呼べるように設定

SceneDelegateをそもそも用意していなかったので、自分はまずそこからやりました。
AppDelegate、Info.plistへの追記、SceneDelegateのベース作成は
下記の記事で分かりやすくまとめてありましたのでご参考ください！

> [【Xcode11】Storyboardを使わずコードだけで画面を生成する方法 - Qiita](https://qiita.com/edasan/items/68cbe9ab63d48ee71594)

#### 2. SceneDelegateに追記

SceneDelegateの[scene(_:openURLContexts:)](https://developer.apple.com/documentation/uikit/uiscenedelegate/3238059-scene)が代わりに呼ばれるメソッドということで、ここに処理を書いていきます。

```swift:SceneDelegate.swift
import UIKit

@available(iOS 13.0, *)
class SceneDelegate: UIResponder, UIWindowSceneDelegate
{
    // ↓----以下追加----↓

    func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>)
    {
        // リクエストURLの取得
        guard let url = URLContexts.first?.url else {
            return
        }

        let sourceApplication = URLContexts.first?.options.sourceApplication
        // あれ....?

    }
}
```

あれ....。
AppDelegateと同じように`sourceApplication`を取得しようとしましたがなぜかnilで返ってくる...
うまくいかない。さてどうしたものか。

#### 3. コールバックURLを識別できるものに変更

コールバックURLをユニークにすることで、レスポンスを受けた後にどの処理をするか判定できるだろう！
ということで例として`call_test`というホスト名をつけてみました。

```swift:Sample.swift
let callbackURL = URL(string: "myapp://call_test")!
```

#### 4. SceneDelegateに改めて追記

`call_test`というホストでリクエストが来ているか判定するために、
[URLComponents](https://developer.apple.com/documentation/foundation/urlcomponents)からホスト名を取得します。

```swift:SceneDelegate.swift
@available(iOS 13.0, *)
class SceneDelegate: UIResponder, UIWindowSceneDelegate
{
    func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>)
    {
        // コールバックで来たURLの取得
        guard let url = URLContexts.first?.url else {
            return
        }

        // ↓----以下追加----↓

        // ホスト名の取得
        guard let components = URLComponents(string: url.absoluteString), let host = components.host else {
            return
        }

        if host == "call_test" {
            // クエリパラメータを受けて色々処理
            // ...
        }
    }
}
```

これでURL Schemeの対応ができました！

## 参考

* [canOpenURL fails on iOS 13 |Apple Developer Forums](https://forums.developer.apple.com/thread/117704)
* [【iOS13】xcode11上でswifterを用いた際のcallback URLのSceneDelegateへの記述 - Qiita](https://qiita.com/ninoko1995/items/05de90350a4bbc12d971)

## 追記（2019.12.17)

あとでわかったこと。
AppDelegateのapplication(_:open:options:)はiOS13以降も動いておりました...
動かなくなっていたと思ったのはココ↓

```swift:AppDelegate.swift
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate
{
    // 省略

    func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
        // ↓コレ
        guard let sourceApplication = options[.sourceApplication] as? String else {
            return false
        }

        if sourceApplication.hasPrefix("--他アプリのBundleId--") {
            // クエリパラメータを受けて色々処理
            // ...
        }

        return true
    }
}
```

SceneDelegateでもそうでしたが、`sourceApplication`が受け取れなくなっていただけのようです...
理由はまだ調査しきれておりませんが。
大変失礼しました...