iOS13(beta)でASWebAuthenticationSessionの変更点

注：本記事は `Xcode 11 beta 6` (2019/08/20)時点での変更内容での話になります

## iOS13でログイン画面が開かない

開発中のアプリで`ASWebAuthenticationSession`を利用してOAuthログインする機能があり、iOS13(beta)で動作確認をしたところログインボタンを押してログイン画面を出そうとしても出てくれない状態でした。

ログを見てみると以下のエラーの出力がありました。

```
Cannot start ASWebAuthenticationSession without providing presentation context. Set presentationContextProvider before calling -start.
```

どうやらiOS13からは`start()`を呼び出す前に`presentationContextProvider`をセットする必要があるようです。

## ASWebAuthenticationPresentationContextProviding

`presentationContextProvider`は`ASWebAuthenticationPresentationContextProviding`([document](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationpresentationcontextproviding))というprotocolに適合した`NSObject`のサブクラスのインスタンスのようです。

定義は以下のようになっています。

```swift
/** @abstract Provides context to target where in an application's UI the authorization view should be shown.
 */
@available(iOS 13.0, *)
public protocol ASWebAuthenticationPresentationContextProviding : NSObjectProtocol {

    
    /** @abstract Return the ASPresentationAnchor in the closest proximity to where a user interacted with your app to trigger
     authentication. If starting an ASWebAuthenticationSession on first launch, use the application's main window.
     @param session The session requesting a presentation anchor.
     @result The ASPresentationAnchor most closely associated with the UI used to trigger authentication.
     */
    func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor
}
```

## ASPresentationAnchor

なるほど、そのprotocolのインスタンスを用意すればいいことはわかったけど、`ASPresentationAnchor`ってなんだ？と思ったら`UIWindow(iOS, Mac Catalyst, tvOS)` or `NSWindow(macOS)`のtypealiasでした。

## 直してみる

シンプルに書くと多分こんな感じに実装すれば良さそう

```swift
import AuthenticationServices
import UIKit

class ViewController: UIViewController {

    private var authenticationSession: ASWebAuthenticationSession?

    // ログインボタンを押した後に呼ばれる
    func loginStart() {
        let authenticationSession = ASWebAuthenticationSession(
            url: URL(string: "https://...")!,
            callbackURLScheme: "<url-scheme>",
            completionHandler: loginCompletionHandler(url:error:))
        if #available(iOS 13.0, *) {
            // 
            authenticationSession.presentationContextProvider = self
        }
        authenticationSession.start()

        // authenticationSessionが解放されないようにプロパティに入れておく
        self.authenticationSession = authenticationSession
    }

    // ログインの結果のハンドリング
    func loginCompletionHandler(url: URL?, error: Error?) {
        authenticationSession = nil
        ...
    }

    ...
}

@available(iOS 13.0, *)
extension ViewController: ASWebAuthenticationPresentationContextProviding {

    func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
            return view.window!
    }
}
```

ちなみに今開発中のアプリではiOS11も対応しており、`ASWebAuthenticationSession`と`SFAuthenticationSession`をラップしたクラスを定義していたため、`ASWebAuthenticationPresentationContextProviding`を適合させたprivateなクラスを用意して実装しました。
このように実装する場合、`presentationContextProvider`は弱参照なので解放されてしまいます。そのため`ASWebAuthenticationPresentationContextProviding`を適合したオブジェクトのインスタンスもプロパティとして保持しないといけないことに注意してください。

```swift
class AuthenticationSession {

    ...
    private var authenticationSession: Any?
    private var presentationContextProvider: Any?

    func loginStart(from viewController: UIViewController) {
        if #available(iOS 12.0, *) {
            let authenticationSession = ASWebAuthenticationSession(
                url: loginURL,
                callbackURLScheme: "<url-scheme>",
                completionHandler: loginCompletionHandler(url:error:))
            if #available(iOS 13.0, *) {
                let presentationContextProvider = AuthPresentationContextProver(viewController: viewController)
                authenticationSession.presentationContextProvider = presentationContextProvider
                self.presentationContextProvider = presentationContextProvider
            }
            authenticationSession.start()
            self.authenticationSession = authenticationSession
        } else { // iOS 11.x
            let authenticationSession = SFAuthenticationSession(
                url: loginURL,
                callbackURLScheme: "<url-scheme>",
                completionHandler: loginCompletionHandler(url:error:))
            authenticationSession.start()
            self.authenticationSession = authenticationSession
        }
    }

    func loginCompletionHandler(url: URL?, error: Error?) {
        authenticationSession = nil
        presentationContextProvider = nil
        ...
    }

    ...

    @available(iOS 13.0, *)
    private class AuthPresentationContextProver: NSObject, ASWebAuthenticationPresentationContextProviding {

        private weak var viewController: UIViewController!

        init(viewController: UIViewController) {
            self.viewController = viewController
            super.init()
        }

        func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
            return viewController?.view.window!
        }
    }
}
```

## おわり

`ASWebAuthenticationSession`を使っているアプリはiOS13が出るまでに対応しないとログインが出来なくなってしまう可能性があるので注意しましょう。