Swifterの使い方

はじめに

iOSアプリでTwitterAPIを使う場合、使用するAPI全てのリクエストやエンティティを用意するのは大変です。
その辛さを軽減してくれるSwifterというライブラリを紹介します。

実行環境

  • Xcode11.6
  • Swift5
  • Swifter2.4.0

Swifterって何?

SwifterとはA Twitter Framework witten in Swiftという説明そのままですが、Swift製のTwitterAPIクライアントフレームワークです。

公式github:https://github.com/mattdonnelly/Swifter

何が嬉しいの?

SwifterはTwitterAPIでできることが大体できます。
1番嬉しいのは利用するAPIのリクエストやエンティティを用意しなくても良い点です。
自分で1からTwitterAPIとの通信を実装する場合、利用したいリクエスト、エンティティを自分で用意していかなければなりません。APIのドキュメントを見ていただけるとわかりますが、Twitterのエンティティは結構な量があってネストも深いので、自分で全て書いていくのは大変です。

TwitterAPIdocs:https://developer.twitter.com/ja/docs

Swifterは辞書型ライクにJSONの中身にアクセスできる独自の型を用意しているため、エンティティを用意してdecodeせずとも、指定したキーに対応する値を簡単に取得できます。

if let statusText = statuses[0]["text"].string {
    // ...
}

何が不満?

SwifterがラップしたJSON型から値を取得する時に値が全てオプショナルになるのは少し不便を感じます。

インストール

Carthegeの場合、Cartfileに以下を記述します。

git "https://github.com/mattdonnelly/Swifter.git"

Cocoa podsの場合、podファイルに以下を記述します。

pod 'Swifter', :git => 'https://github.com/mattdonnelly/Swifter.git'

認証

Swifterの操作の際はSwifterオブジェクトを利用します。
認証には3種類方法があると公式Readmeには書かれていますが、ACAccountを使う方法はサンプルを見る限りdepricatedになっているようだったので、2つ目と3つ目のconsumerTokenconsumerSecretを使う方法を紹介します。

self.alert(title: "Deprecated", message: "ACAccountStore was deprecated on iOS 11.0, please use the OAuth flow instead")

consumerTokenconsumerSecretでSwifterオブジェクトを初期化すれば、そのオブジェクト経由で認証のリクエストを送ることができます。

// init
public init(consumerKey: String, consumerSecret: String, appOnly: Bool = false)

// usage
swifter.authorize(
    withCallback: AppResource.Twitter.redirectUrl, // 認証後に自分のアプリに戻ってくるためのurl
    presentingFrom: self, // 認証リクエスト画面をpresentする元となるViewController
    success: { [weak self] accessToken, _ in

        guard let accessToken = accessToken else { return }
        guard let self = self else { return }

        self.saveAccessTokens(accessToken: accessToken)
    }, failure: loginFauilureHandler(error:)
)

consumerTokencomsumerSecretはTwitterの開発者サイトに登録することで発行できます。

> OAuth Consumer Tokens
In Twitter REST API v1.1, each client application must authenticate itself with consumer key and consumer secret tokens. You can request consumer tokens for your app on Twitter’s dev website

キー発行までの詳しい流れはこちらの記事を参考にさせていただきました。
Twitter API 登録 (アカウント申請方法) から承認されるまでの手順まとめ ※2019年8月時点の情報

上記のauthorizeで認証のリクエストを送ると、Safariの認証用のページを開きます。

注意
この時authorizeのpresentatioFrom:に指定するViewCotrollerをSafariServicesDelegateに準拠させることで、外部のsafariではなくSFSafariViewControllerで認証画面をモーダル表示できます。
この設定をしていないとa poor user experienceという理由でリジェクトされるとのことなので注意です。

SafariServicesDelegateに準拠して認証のリクエストをした時はfinishを検知してモーダルを閉じれるように以下の処理を実装しておきます。

func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
    controller.dismiss(animated: true)
}

認証に成功したら
認証に成功したらレスポンスからアクセストークンを取得できます。
このアクセストークンにはkeyとsecretが入っており、この2つを使用してswifterインスタンスを生成すると認証ユーザーとしてTwitterAPIを利用できるようになります。
セキュアな情報のため、私は認証に成功した時点でKeychainに保存しています。

/// トークンを保存します。
/// - Parameter accessToken: アクセストークン
private func saveAccessTokens(accessToken: Credential.OAuthAccessToken) {
    KeychainManager.twitterAccessTokenKey = accessToken.key
    KeychainManager.twitterAccessTokenSecret = accessToken.secret
    swifter = Swifter(consumerKey: AppResource.Twitter.consumerKey,
                      consumerSecret: AppResource.Twitter.consumerSecret,
                      oauthToken: KeychainManager.twitterAccessTokenKey,
                      oauthTokenSecret: KeychainManager.twitterAccessTokenSecret)
}

タイムラインの取得

タイムラインを取得する際には各種用意されたGet用の関数を使用します。

ユーザー指定でタイムラインを取得する際は以下のgetTimeLineを使用できます。

public func getTimeline(for userTag: SwifteriOS.UserTag, customParam: [String : Any] = [:], count: Int? = nil, sinceID: String? = nil, maxID: String? = nil, trimUser: Bool? = nil, excludeReplies: Bool? = nil, includeRetweets: Bool? = nil, contributorDetails: Bool? = nil, includeEntities: Bool? = nil, tweetMode: SwifteriOS.TweetMode = .default, success: SwifteriOS.Swifter.SuccessHandler? = nil, failure: SwifteriOS.Swifter.FailureHandler? = nil)

public func getHomeTimeline(count: Int? = nil, sinceID: String? = nil, maxID: String? = nil, trimUser: Bool? = nil, contributorDetails: Bool? = nil, includeEntities: Bool? = nil, tweetMode: SwifteriOS.TweetMode = TweetMode.default, success: SwifteriOS.Swifter.SuccessHandler? = nil, failure: SwifteriOS.Swifter.FailureHandler? = nil)

ユーザー単位ではなくハッシュタグなどで検索してツイートを取得したい場合はsearchTweetが使用できます。

public func searchTweet(using query: String, geocode: String? = nil, lang: String? = nil, locale: String? = nil, resultType: String? = nil, count: Int? = nil, until: String? = nil, sinceID: String? = nil, maxID: String? = nil, includeEntities: Bool? = nil, callback: String? = nil, tweetMode: SwifteriOS.TweetMode = TweetMode.default, success: SwifteriOS.Swifter.SearchResultHandler? = nil, failure: @escaping SwifteriOS.Swifter.FailureHandler)

どんな単位でツイートを取得するかはSwifterの各種GetメソッドとTwitterAPIを参照していただけると分かります。

利用時のTopic

  • Tweetを取得する時のtweetModeパラメータはextendedを指定しておくことをオススメします。
    このtweetModeによって取得できるTweetオブジェクトの中身が変わって来るのですが、extendedを指定していないとTweetのテキストを全文返してくれません。
    参考:https://qiita.com/hitsumabushi845/items/f7fd87106381fc65fc86
  • Tweetにはentitieextended_entitiesが存在しますが、Tweetに含む画像を全て取得したい場合はextended_entitiesを使う必要がありました。(entitiesには最初の一枚しか入っていないため)

ツイートを取得したら
ツイートオブジェクトを取得したら中のデータにアクセスします。
上述したようにSwifterを使って取得したレスポンスはJSONという型としてラップされており、キー名と型名を指定することで取得できるようになっています。

tweetId = tweet["id_str"].string ?? ""

ネストしている場合は[]を複数指定することでアクセス可能です。

screenName = tweet["user"]["screen_name"].string ?? ""

結果がarrayになっている場合は.arrayを指定します。
.objectをすることでキー名とセットの辞書型として取得することも可能です。

public var object: [String : SwifteriOS.JSON]? { get }
public var array: [SwifteriOS.JSON]? { get }

投稿

投稿する際は以下の関数を利用します。

public func postTweet(status: String, inReplyToStatusID: String? = nil, coordinate: (lat: Double, long: Double)? = nil, autoPopulateReplyMetadata: Bool? = nil, excludeReplyUserIds: Bool? = nil, placeID: Double? = nil, displayCoordinates: Bool? = nil, trimUser: Bool? = nil, mediaIDs: [String] = [], attachmentURL: URL? = nil, tweetMode: SwifteriOS.TweetMode = TweetMode.default, success: SwifteriOS.Swifter.SuccessHandler? = nil, failure: SwifteriOS.Swifter.FailureHandler? = nil)

public func postTweet(status: String, media: Data, inReplyToStatusID: String? = nil, autoPopulateReplyMetadata: Bool? = nil, excludeReplyUserIds: Bool? = nil, coordinate: (lat: Double, long: Double)? = nil, placeID: Double? = nil, displayCoordinates: Bool? = nil, trimUser: Bool? = nil, tweetMode: SwifteriOS.TweetMode = TweetMode.default, success: SwifteriOS.Swifter.SuccessHandler? = nil, failure: SwifteriOS.Swifter.FailureHandler? = nil)

public func postTweetWithGif(attachmentUrl: URL, text: String, success: SwifteriOS.Swifter.SuccessHandler? = nil, failure: SwifteriOS.Swifter.FailureHandler? = nil)

public func postMedia(_ media: Data, additionalOwners: SwifteriOS.UsersTag? = nil, success: SwifteriOS.Swifter.SuccessHandler? = nil, failure: SwifteriOS.Swifter.FailureHandler? = nil)

利用時のトピック
Tweetのテキストは全角であれば140文字、半角であれば280文字まで指定できます。
Tweetの文字数を判定して「投稿ボタン」をdisableにしたい場合などは、全角なら1文字、半角なら0.5文字としてカウントすると良さそうです。

// 半角英数字は0.5文字でカウントします。(半角英数字は280文字まで入力してツイートできるため)
let singleByteString = textView.text.filter { $0.isASCII }
let doubleByteString = textView.text.filter { !$0.isASCII }

let wordCount = CGFloat(140) - (CGFloat(singleByteString.count) * 0.5 + CGFloat(doubleByteString.count))
let isWordCountOver = wordCount < 0
// 初期状態か文字数オーバーの時は投稿ボタンをdisableにします。
postTweetBarButtonItem.isEnabled = !isWordCountOver

エラーハンドリング

Swifterを使用して発生したエラーはError型として返されます。
Swifterを利用した際のエラーはSwifterErrorというLocalizedErrorにキャストすることでケースを分けたハンドリングが可能です。

switch error {
case let swifterError as SwifterError:    
    switch swifterError.kind {  
    case .badOAuthResponse, .invalidAppOnlyBearerToken:
        logout(  

    case .responseError(code: let code):
         ...    

    case .urlResponseError(status: let status, headers: _, errorCode: let errorCode):
        // UnAuthorized
        if case 401 = status {
             ...
        }                
        switch errorCode {
        case 186: // "文字数制限をオーバーしています。"
            ...
        case 187: // "このツイートは既に送信済みです。"
            ...    
        default:
            ...
        }

    case .cancelled: return
        ...     
    default:
        ...
    }

case let urlError as URLError:
   ... 
default:
   ...   
}

https://developer.twitter.com/ja/docs/basics/response-codes

上記のコード一覧を見て、個別にハンドリングしたいエラーがあればurlResponseErrorからerrorCodeを受け取ってハンドリングしていけば良さそうです。

おわりに

Swifterの使い方を紹介しました。
TwitterKitのサポートは終了していますが、Swifterは2020/05/10に最新バージョンをリリースしており、今もメンテされているみたいです。
使い方もシンプルで使いやすいので、もしTwitterAPIをアプリで利用する際は導入を検討してみて欲しいです。