SwiftUIでASWebAuthenticationSession

## 2023/11/28 追記
* しばらく気がつきませんでしたが、SwiftUIでASWebAuthenticationSessionを利用するAPIが追加されていました
* 利用可能なOSは限られていますが、可能であればこちらのAPIを使う方が良いでしょう

## webAuthenticationSession
* [本家ドキュメント](https://developer.apple.com/documentation/swiftui/environmentvalues/webauthenticationsession)
* iOS16.4~
* `@Environment`で利用する
* 扱うのはASWebAuthenticationSessionではなく、[`WebAuthenticationSession`](https://developer.apple.com/documentation/authenticationservices/webauthenticationsession)

## 利用方法
* SwiftUI用にチューニングされていて、Concurrency対応していたり既存の方法よりかなり使い勝手が良くなっています
* 以下はApple公式の例をちょっと変えたものです

```swift
struct WebAuthenticationSessionExample: View {
    @Environment(\.webAuthenticationSession) private var webAuthenticationSession

    var body: some View {
        Button("Sign In") {
            Task {
                do {
                    // authenticateメソッドの戻り値でコールバックURLをもらえる
                    let urlWithToken = try await webAuthenticationSession.authenticate(
                        using: URL(string: "https://www.example.com")!,
                        callbackURLScheme: "x-example-app"
                    )
                    // URLComponentsで必要なパラメータを取り出す
                    let queryItems = URLComponents(string: urlWithToken.absoluteString)?.queryItems
                    // OAuth2だったらここからトークン交換をしたりする
                } catch {
                    print(error)
                }
            }
        }
    }
}
```

# ここから追記前記事

### 導入
新卒でSwiftはじめて３ヶ月が経ちました。あっという間です…
今回はSwiftUIでTwitter連携させたかったとき悩んだところが解消できたのでせっかくなので記事にしたいと思います。エンジニアっぽくなってきて楽しいです:relaxed:
## したかったこと
* `SwiftUI`のViewで作ったボタンをポチったら`ASWebAuthenticationSession`でTwitterとか連携するあの画面が出てきてサインインしたら閉じるやつが作りたい(語彙力)

## できたやつ
AuthPresentationContextProver クラスは[こちらの記事](https://qiita.com/simorgh3196/items/644ea63e99d23c4983c3)から拝借しました。ありがとうございます:clap:

```swift
import SwiftUI
import AuthenticationServices

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! ?? ASPresentationAnchor()
    }
}

struct RenkeiButton: View {
    @State var session: ASWebAuthenticationSession?
    @State var presentationContextProvider: AuthPresentationContextProver?
    // リクエストトークンもらうマン
    func postOAuthRequestToken(success: 成功した時のHandler, failure: 失敗した時)
    // リクエストトークンから連携画面表示するマン
    func authStart(requestToken: とーくん) {
        let queryUrl = tokenで作る
        let callbackURLScheme = "ちょめちょめ"

        self.session = ASWebAuthenticationSession(url: queryUrl, callbackURLScheme: callbackURLScheme, completionHandler: { (url, error) in
            // callbackURLScheme + parameters のURLが帰ってくるのでちょめちょめする
            // キャンセルの時はエラーになる
        })
        let presentationContextProvider = AuthPresentationContextProver(viewController: UIHostingController(rootView: self))
        self.session?.presentationContextProvider = presentationContextProvider
        self.presentationContextProvider = presentationContextProvider
        self.session?.start()
    }

    var body: some View {
        Button(action: {
            self.postOAuthRequestToken(
                success: { (requestToken, response) in
                    self.authStart(token: requestToken)
                }, failure: { (error) in
                })
        }, label: {
            Text("連携や!!")
        })
    }
}
```

## 悩んでたとこ
iOS13では`presentationContextProvider`に`ASWebAuthenticationPresentationContextProviding`とかいうProtocolを満たすNSObjectを設定をしていないと連携画面のモーダルが出ません…
Controllerベースで作れば簡単にできるけど…ってなってた時に@simorgh3196さんの記事を発見して、もしやUIHostingControllerで渡してやれば同じようにできる…？となってうまくいった次第です

## UIHostingControllerで渡してやらなくても…
要は現在のwindowが知りたいので@shizさんの[こちらの記事](https://qiita.com/shiz/items/3b829b1521f9723aa875)を参考に…

```swift
class AuthPresentationContextProver: NSObject, ASWebAuthenticationPresentationContextProviding {
    @Environment(\.window) var window

    func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
        return window ?? ASPresentationAnchor()
    }
}
```
とすることもできます。(この書き方いいんでしょうか…)

### おわりに
SwiftUI、iOS13　まだまだBeta段階ですのでこの書き方もいずれ使えなくなるかもなので注意です！

SwiftUIたのしい！