iOS 13 SceneDelegate で Deep Link を開く実装について(アプリが起動していないとき)

iOS の Deep Link (Custom URL Scheme) を定義して別アプリから機能を呼び出す機会があり、30分もあれば実装できるやろと思っていたら事故ったので知見を書きます。

要約

  • SceneDelegate があるときはそちらが呼ばれるから AppDelegate 側に書いた処理は動かない
  • アプリが起動していないときは scene(_:openURLContexts:) が呼ばれず、scene(_:willConnectTo:options:) を使わないといけない。このときは connectionOptions.urlContexts で url が取れる
  • 用語は正しく使おう。公式のドキュメントはしっかり読もう

事故の記録

たしか、AppDelegateに何か実装すればよかったんだよな……

    func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
        debugPrint("openUrl \(url)")
        return processLink(url)
    }

→ アプリはきちんと起動したが、何やってもメソッド application(_:open:options:) が呼ばれていない

iOS 13 から同一のアプリのウィンドウを複数開けるようになったので、その関連です。今回のアプリは SceneDelegate を持っていました。これが存在すると AppDelegate 側で呼ばれないメソッドが出てきます。

なるほど、scene(_:openURLContexts:) に書けばいいんだな

    func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
        debugPrint("scene(_:openURLContexts:)")
        guard let url = URLContexts.first?.url else {
            return
        }
        debugPrint("openUrl \(url)")
        let _ = processLink(url)
    }

実装して意図したとおりに動いて一安心した後で、Xcodeの停止ボタンからアプリを落としてからもう一度 Deep Link からの起動を試す

scene(_:openURLContexts:) が呼ばれないでそのままアプリが起動する

どうやらアプリが起動していないときの Deep Link は scene(_:openURLContexts:) を呼ばないので scene(_:willConnectTo:options:) で処理しないといけません。

scene(_:willConnectTo:options:) を使うのか……あれ? url どうやって持ってくるんだ?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        ...
        
        if let url = connectionOptions.urlContexts.first?.url {
            debugPrint("openUrl \(url)")
            let _ = processLink(url)
        }
    }

→ connectionOptions.urlContexts で取れます。

そもそもなんで事故ったのか

  • 公式のドキュメントの下のほうに書いてあったのを見落とした
  • 検索が雑だった

俗称の 『Deep Link』 で検索して出てきた記事を読むよりも、正式な名称の 『Custom URL Scheme』 で 「custom url scheme openurlcontexts not called」とやったり、「openurlcontexts not called」のようにきちんと名指ししたりすれば割と簡単に出てきました。疲れているとGoogle検索力が落ちますね……。

ここにたどり着くまでが長かった:

When the app is killed and opened by a custom URI, it doesn’t trigger func scene(_:openURLContexts:) but by the normal startup function func scene(_:willConnectTo:options) where connectionOptions contains your urlContexts.

https://developer.apple.com/forums/thread/124826

とくに何も書いてない scene(_:openURLContexts:) https://developer.apple.com/documentation/uikit/uiscenedelegate/3238059-scene

公式のドキュメント Defining a Custom URL Scheme for Your Appきちんと書いてある

 Handle Incoming URLs の SceneDelegateでの実装コードまで読んだところでそのまま閉じていたのですが、よく見るとその下に

If your app has opted into Scenes, and your app is not running, the system delivers the URL to the scene(_:willConnectTo:options:) delegate method after launch, and to scene(_:openURLContexts:) when your app opens a URL while running or suspended in memory.

https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app

と書いてありました。つらい。