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

GitHub
概要
- 今回2つのプロジェクトを作りました。以下、実装1,実装2と呼びます。
- 前者は一つのクラスにつらつらと書きました。
- How to実装を示すためのものです。
- 後者は前者を参考に、クラス分けをしながらそれなりの形のmacOSアプリにしています。
- 最終的な成果物はこちらです。
- ※記事に
Client ID
・Client Secret
の直値を載せていますが、試す際は自身のものを使用してください。- 載せているものは失効させているので使用できません。
実装1 - 基本的な実装の流れ
Imgur APIの設定
- WEB上で下記のパラメータの通り設定します。
項目 | 値 |
---|---|
コールバックURL | qiita-demo-2cdc3a06e7197c2://oauth-callback |
Client ID | 2cdc3a06e7197c2 |
Client Secret | fd2cd5bda2a673213a8db56d1a79eb4f74f9a0cf |
OAuth2.0認証 - ユーザ認証
CallbackURLの受け取り設定
- ユーザ認証用のURLを作成し、認証後CallbackURLをアプリで受け取るまでまずは実装します。
- CallbackURLによってアプリを開くために、
URL Types
にスキームを登録します。
-
AppDelegate.swift
に以下を記述します。- setEventHandler(_:andSelector:forEventClass:andEventID:)
- コールバックURLを受け取る設定、およびその際の動作を実装します。
- 今回は
Notification
を使用してViewController
にコールバックURLを伝えるようにします。
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は下記の通りです。
- 参考:OAuth2.0認証
https://api.imgur.com/oauth2/authorize
?client_id=2cdc3a06e7197c2
&response_type=token
&state=hoge
- SwiftにはURLを扱うのに便利なクラス
URLComponents
やURLQueryItem
が用意されているのでこちらを使用します。 - 勿論
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
で通信を行うために、下記にチェックをします。
- 以下の通り、Swiftで実装していきます。
- HTTPリクエストのbodyの設定方法
- How to make HTTP Post request with JSON body in Swift
- また上記回答のコメントにヘッダに関して言及があるので採用しています。(無いとAPIからレスポンスエラーが返ってくる)
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
の更新の実装は完了です。
認証の画像アップロード
-
Access Tokenの更新
と同様の流れで、APIのドキュメントに沿って実装します。
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クライアントを作ろう」を主に参考にして実装しました。
- (複雑で難しい所ですね…。何日もかけてトライアンドエラーで実装しました。)
- 下記の通りクラス分けしています。
- 詳しくはGitHubを参照くださいませ。
- 実装の流れはほぼ本の通りです。

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