Swift：OAuthSwiftを使ってQiita API v2をいじる

**macOSでOAuthSwiftを使ってQiita APIにOAuth 2.0で認証する方法**

- [OAuthSwift](https://github.com/OAuthSwift/OAuthSwift)
- [Qiita API v2の仕様](https://qiita.com/api/v2/docs)

# 1. Qiita側の準備
Qiitaの設定ページへ行き，新規にアプリケーションを登録して，Client IDとClient Secretを入手します．
リダイレクト先のURLは`[任意の文字列] + ://oauth-callback`にして，メモしておきます．
![qiita.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67153/e9ae70e0-1426-60d5-5296-6d8bb27a4273.png)

# 2. プロジェクトの下準備
URL Typeを追加します．URL Schemesに先ほどのリダイレクト先の任意の文字列を入力します．
![スクリーンショット 2020-01-15 17.02.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67153/b66d17a8-748c-2520-8ff3-e51ad7042704.png)

# 3. コーディング
OAuth認証をするためのWebViewController

```swift:OAuthWebVC.swift
import Foundation
import WebKit
import OAuthSwift

class OAuthWebVC: OAuthWebViewController, WKNavigationDelegate {
    
    var targetURL: URL?
    let webView = WKWebView()
    var cancelBtn: NSButton!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        webView.navigationDelegate = self
        webView.translatesAutoresizingMaskIntoConstraints = false
        self.view.addSubview(webView)
        cancelBtn = NSButton(title: "Cancel", target: self, action: #selector(cancel))
        cancelBtn!.translatesAutoresizingMaskIntoConstraints = false
        self.view.addSubview(cancelBtn)
        
        webView.leadingAnchor.constraint(equalTo:  self.view.leadingAnchor).isActive = true
        webView.trailingAnchor.constraint(equalTo:  self.view.trailingAnchor).isActive = true
        webView.topAnchor.constraint(equalTo:  self.view.topAnchor).isActive = true
        webView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -40.0).isActive = true
        cancelBtn!.widthAnchor.constraint(equalToConstant: 82.0).isActive = true
        cancelBtn!.heightAnchor.constraint(equalToConstant: 32.0).isActive = true
        cancelBtn!.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 20.0).isActive = true
        cancelBtn!.centerYAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -20.0).isActive = true
    }
    
    override func handle(_ url: URL) {
        targetURL = url
        super.handle(url)
        self.loadAddressURL()
    }
    
    func loadAddressURL() {
        guard let url = targetURL else {
            return
        }
        let req = URLRequest(url: url)
        webView.load(req)
    }
    
    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        if let url = navigationAction.request.url {
            if url.host == "oauth-callback" {
                OAuthSwift.handle(url: url)
                decisionHandler(WKNavigationActionPolicy.cancel)
                self.dismissWebViewController()
                return
            }
        }
        decisionHandler(WKNavigationActionPolicy.allow)
    }
    
    func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
        Swift.print(error.localizedDescription)
        self.dismissWebViewController()
    }
    
    @objc func cancel() {
        self.dismissWebViewController()
    }
    
}
```

認証とユーザ情報の取得を促すベースのViewController

```swift:ViewController.swift
import Cocoa
import OAuthSwift

class ViewController: NSViewController, OAuthWebViewControllerDelegate {
    
    var oauthswift: OAuth2Swift?
    var client: OAuthSwiftClient?
    lazy var webVC: OAuthWebVC = {
        let controller = OAuthWebVC()
        controller.view = NSView(frame: NSRect(x: 0, y: 0, width: 600, height: 400))
        controller.delegate = self
        controller.viewDidLoad()
        return controller
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBAction func register(_ sender: Any) {
        let _ = webVC.webView
        oauthswift = OAuth2Swift(consumerKey: "Client ID",
                                 consumerSecret: "Client Secret",
                                 authorizeUrl: "https://qiita.com/api/v2/oauth/authorize",
                                 accessTokenUrl: "https://qiita.com/api/v2/access_tokens",
                                 responseType: "code")
        oauthswift?.authorizeURLHandler = getURLHandler()
        oauthswift?.allowMissingStateCheck = true
        let _ = oauthswift?.authorize(
            withCallbackURL: URL(string: "oauth-qiita-station://oauth-callback")!,
            scope: "read_qiita write_qiita",
            state: "",
            headers: ["Content-Type" : "application/json"],
            completionHandler: { (result) in
                switch result {
                case .success(let (credential, _, _)):
                    Swift.print("token", credential.oauthToken)
                    self.client = OAuthSwiftClient(credential: credential)
                case .failure(let error):
                    Swift.print(error.description)
                }
                
        })
    }
    
    func getURLHandler() -> OAuthSwiftURLHandlerType {
        if webVC.parent == nil {
            self.presentAsSheet(webVC)
        }
        return webVC
    }
    
    @IBAction func getUserInfo(_ sender: Any) {
        let userID: String = "Kyome"
        client?.get(
            URL(string: "https://qiita.com/api/v2/users/\(userID)")!,
            completionHandler: { (result) in
                switch result {
                case .success(let response):
                    if let json = try? response.jsonObject(options: .allowFragments) as? [String : Any] {
                        Swift.print(json)
                    }
                case .failure(let error):
                    Swift.print(error.description)
                }
        })
    }
    
    func oauthWebViewControllerWillAppear() {
    }
    
    func oauthWebViewControllerDidAppear() {
    }
    
    func oauthWebViewControllerWillDisappear() {
    }
    
    func oauthWebViewControllerDidDisappear() {
        oauthswift?.cancel()
    }
    
}
```

# 参考
[Qiita APIのOAuth認証は一筋縄ではいかない](https://qiita.com/takumi-n/items/795e7687db988dc9e2e5)