2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Imgur APIを使ったmacOSアプリを作成する

Last updated at Posted at 2020-08-04

はじめに

  • 下記の記事でImgur APIをターミナルから使用する方法を紹介しました。
  • これをSwiftを使って、下記のようなmacOSアプリを実装したいと思います。
    • Imgur APIを使用する部分の実装はiOSでも共通ですので、iOSで実装したいという方も是非。
image

GitHub

概要

実装1 - 基本的な実装の流れ

Imgur APIの設定

  • WEB上で下記のパラメータの通り設定します。
項目
コールバックURL qiita-demo-2cdc3a06e7197c2://oauth-callback
Client ID 2cdc3a06e7197c2
Client Secret fd2cd5bda2a673213a8db56d1a79eb4f74f9a0cf

OAuth2.0認証 - ユーザ認証

CallbackURLの受け取り設定

  • ユーザ認証用のURLを作成し、認証後CallbackURLをアプリで受け取るまでまずは実装します。
  • CallbackURLによってアプリを開くために、URL Typesにスキームを登録します。

-w1141

  • AppDelegate.swiftに以下を記述します。
func applicationDidFinishLaunching(_ aNotification: Notification) {
    NSAppleEventManager.shared().setEventHandler(self, andSelector:#selector(AppDelegate.handleGetURL(event:withReplyEvent:)),
                                                        forEventClass: AEEventClass(kInternetEventClass),
                                                        andEventID: AEEventID(kAEGetURL))
}

@objc func handleGetURL(event: NSAppleEventDescriptor!, withReplyEvent: NSAppleEventDescriptor!) {
    if let urlString = event.paramDescriptor(forKeyword: AEKeyword(keyDirectObject))?.stringValue,
        let url = URL(string: urlString) {
        NotificationCenter.default.post(name: OAuthInfo.Imgur.callBackNotificationName,
                                        object: nil,
                                        userInfo:  [OAuthInfo.Imgur.UserinfoKey.callbackURL: url])
    }
}
  • なお利便性のため、定数はStructにまとめています。
public final class OAuthInfo {
    public struct Imgur {
        private enum UserDefaultsKey: String {
            case accessToken
            case expires
            case tokenType
            case refreshToken
            case accountUserName
            case accountID
        }
        
        static let clientID     = "2cdc3a06e7197c2"
        static let clientSecret = "fd2cd5bda2a673213a8db56d1a79eb4f74f9a0cf"
        static let callBackNotificationName = Notification.Name("ReceiveCallBackURLNotification")
        struct UserinfoKey {
            static let callbackURL = "callbackurl"
        }
        
       static var accessToken: String {
            get {
                return UserDefaults.standard.string(forKey: UserDefaultsKey.accessToken.rawValue) ?? ""
            }
            
            set(accessToken) {
                UserDefaults.standard.set(accessToken, forKey: UserDefaultsKey.accessToken.rawValue)
            }
        }
// (省略)
  • ではViewController側に書いていきます。
  • Notificationを受け取るようにaddObserverを行います。
    • 実際の処理handleCallbackURL(_:)は後ほど実装します。
NotificationCenter.default.addObserver(self, selector: #selector(handleCallbackURL(_:)),
                                       name: OAuthInfo.Imgur.callBackNotificationName,
                                       object: nil)

認証用URLの作成

  • 続いて認証用URLを作成します。
  • 具体的なURLは下記の通りです。
https://api.imgur.com/oauth2/authorize
?client_id=2cdc3a06e7197c2
&response_type=token
&state=hoge
  • SwiftにはURLを扱うのに便利なクラスURLComponentsURLQueryItemが用意されているのでこちらを使用します。
  • 勿論Stringでも実装可能ですが、後でクラス分けするときに使い勝手が悪いのでオススメはしません。
  • クエリ・フラグメントの知識は以下を参照。
  • イメージとしては、URLComponentsのオブジェクトに、クエリやフラグメント情報を渡して、最終的にURLを発行してもらう、という感じです。
let baseURL           = URL(string: "https://api.imgur.com")!
let relativePath      = "oauth2/authorize"
let authenticationURL = baseURL.appendingPathComponent(relativePath)
var components = URLComponents(url: authenticationURL , resolvingAgainstBaseURL: true)  // URL構成要素を表現するクラス。URLも楽に書き出せて便利。

let queryItems: [URLQueryItem] = [URLQueryItem(name: "client_id",     value: OAuthInfo.Imgur.clientID),
                                  URLQueryItem(name: "response_type", value: "token"),
                                  URLQueryItem(name: "state",         value: "sample")]

components?.queryItems = queryItems

guard let openingURL = components?.url else {
    return
}

// ブラウザで認証ページを開く
if !NSWorkspace.shared.open(openingURL) {
    fatalError()
}
  • 以上で認証URLを作成しブラウザに表示することができました。

CallbackURLの受け取り後の処理

  • では残しておいたCallback URL取得後の処理を実装します。
  • Callback URLが例えば以下の通りに取得されます。
imgur-e6d8ea34904ab93://oauth-callback?state=sample
# access_token=37b3888f2801db013d35e4bec3a8f103b43344aa
&expires_in=315360000
&token_type=bearer
&refresh_token=ba2bb6ea7df5c0a3ecca48c5412b1d06e6135a3b
&account_username=IKEH1024
&account_id=104356397
  • フラグメントの情報取得をします。
  • まずcallbackURL.fragmentを使って取り出し、一旦クエリに保存します。
  • その後URLComponentsからクエリとして取り出します。
  • また実際には安全性のためUserDefaultsではなくKeyChainに保存するべきですね。
@objc func handleCallbackURL(_ notification: Notification) {
    NotificationCenter.default.removeObserver(self)
    
    guard let callbackURL = notification.userInfo?[OAuthInfo.Imgur.UserinfoKey.callbackURL] as? URL else {
        return
    }
    
    var components = URLComponents(url: callbackURL , resolvingAgainstBaseURL: true)  // フラグメント部分は無視されるようです
    components?.query = callbackURL.fragment  // フラグメントをクエリとして保存する
    guard let queryItems = components?.queryItems else {
        return
    }
    
    var oauthInfo = OAuthInfo.Imgur()
    for queryItem in queryItems {
        oauthInfo.update(for: queryItem)
    }
}
  • URLQueryItemの情報を受け取って保存するメソッドを用意しておきます。
public final class OAuthInfo {
    public struct Imgur {
// (省略)
        mutating func update(for queryItem: URLQueryItem) {

            guard let queryValue = queryItem.value else {
                return
            }
            
            switch queryItem.name {
            case "access_token":
                Self.accessToken = queryValue
            case "expires_in":
                Self.expires = Int(queryValue) ?? 0
            case "token_type":
                Self.tokenType = queryValue
            case "refresh_token":
                Self.refreshToken = queryValue
            case "account_username":
                Self.accountUserName = queryValue
            case "account_id":
                Self.accountID = queryValue
            default:
                break
            }
        }
    }
}

Access Tokenの更新

  • 続いてAccess Tokenの更新処理を実装します。
  • まず、URLSessionで通信を行うために、下記にチェックをします。

-w534

func updateAccessToken() {
    let baseURL      = URL(string: "https://api.imgur.com")!
    let relativePath = "oauth2/token"
    let url          = baseURL.appendingPathComponent(relativePath)
    
    var urlRequest = URLRequest(url: url, timeoutInterval: Double.infinity)
    urlRequest.httpMethod = "POST"
    
    let bodyJSON = [
        "refresh_token" : OAuthInfo.Imgur.refreshToken,
        "client_id"     : OAuthInfo.Imgur.clientID,
        "client_secret" : OAuthInfo.Imgur.clientSecret,
        "grant_type"    : "refresh_token",
    ]
    
    guard let bodyData = try? JSONSerialization.data(withJSONObject: bodyJSON) else {
        return
    }
    urlRequest.httpBody = bodyData
    
    
    urlRequest.addValue("\(bodyData.count)", forHTTPHeaderField: "Content-Length")
    urlRequest.addValue("application/json",  forHTTPHeaderField: "Content-Type")
    
    let session = URLSession.shared
    let task = session.dataTask(with: urlRequest) { data, urlResponse, error in
        if let urlResponse = urlResponse as? HTTPURLResponse {
            if urlResponse.statusCode != 200 {
                print("アクセストークンのアップデートに失敗しました。")
                return
            }
            
            let dic = try? JSONSerialization.jsonObject(with: data ?? Data(), options: []) as? [String: Any]
            if let dic = dic {
                for (key, value) in dic {
                    print("\(key):\(value)")
                }
                
                if let accessToken = dic["access_token"] as? String {
                    OAuthInfo.Imgur.accessToken = accessToken
                }
                
                if let refreshToken =  dic["refresh_token"] as? String {
                    OAuthInfo.Imgur.refreshToken = refreshToken
                }
            }
        }
    }
    
    task.resume()
}
  • 以上でAccess Tokenの更新の実装は完了です。

認証の画像アップロード

func uploadImage() {
    
    guard let image = readImageFromClipboard() else {
        return
    }
    
    guard let imageData = image.tiffRepresentation else {
        return
    }

    let baseURL      = URL(string: "https://api.imgur.com")!
    let relativePath = "/3/image"
    let url          = baseURL.appendingPathComponent(relativePath)
    
    var urlRequest = URLRequest(url: url, timeoutInterval: Double.infinity)
    urlRequest.httpMethod = "POST"
    
    let base64 = imageData.base64EncodedString()
    let bodyJSON = [
        "image" : base64,
    ]
    
    guard let bodyData = try? JSONSerialization.data(withJSONObject: bodyJSON) else {
        return
    }
    
    urlRequest.httpBody = bodyData
    
    urlRequest.addValue("application/json",                      forHTTPHeaderField: "Content-Type")
    urlRequest.addValue("Bearer \(OAuthInfo.Imgur.accessToken)", forHTTPHeaderField: "Authorization")
    
    let session = URLSession.shared
    let task = session.dataTask(with: urlRequest) { data, urlResponse, error in
        if let urlResponse = urlResponse as? HTTPURLResponse {
            if urlResponse.statusCode != 200 {
                if let errorString = String(data: data ?? Data(), encoding: .utf8) {
                    print(errorString)
                }
                print("画像のアップロードに失敗しました。")
                return
            }
            
            // アップロード結果の確認
            let dic = try? JSONSerialization.jsonObject(with: data ?? Data(), options: []) as? [String: Any]
            if let dic = dic {
                for (key, value) in dic {
                    print("\(key):\(value)")
                }
            }
        }
    }
    
    task.resume()
}
  • アップロードに成功すると以下のようなレスポンスが返ってきます。
data:{
    "account_id" = 104356397;
    (中略)
    datetime = 1596414978;
    deletehash = 60zvjsIbzBl8aHK;
    link = "https://i.imgur.com/yjU8yCh.png";
    type = "image/png";
}
status:200
success:1
  • 以上で基本的な動作をSwiftで実装することができました。

実装2 - クラス分けとUnitTestsの実装

  • 残りの作業として下記を行います。
    • 重複している冗長な部分が多いので、通信部分やデータモデルへのクラス分けをします
    • UnitTests・スタブの実装
    • UI部分の実装
  • 増補改訂第3版 Swift実践入門
    • 「第18章 実践的なSwiftアプリケーション ── Web APIクライアントを作ろう」を主に参考にして実装しました。
    • (複雑で難しい所ですね…。何日もかけてトライアンドエラーで実装しました。)
  • 下記の通りクラス分けしています。
image

おわりに

  • 以上SwiftによるOAuth2.0認証やAPI利用の実装方法でした。
  • 「Swift実践入門」を参考に、Imgur APIを使って構造的にアプリの作成を行うことができました。
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?