Swifterの使い方

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

## 実行環境
- Xcode11.6
- Swift5
- Swifter2.4.0

## Swifterって何？
<img src="https://github.com/mattdonnelly/Swifter/blob/master/swifter_logo.png?raw=true" width="300">

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

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

## 何が嬉しいの？

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

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

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

```swift
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オブジェクトを利用します。
認証には３種類方法があると公式Readmeには書かれていますが、ACAccountを使う方法はサンプルを見る限りdepricatedになっているようだったので、２つ目と３つ目の`consumerToken`、`consumerSecret`を使う方法を紹介します。

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


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

```swift
// 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:)
)
```

`consumerToken`と`comsumerSecret`は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月時点の情報
](https://qiita.com/kngsym2018/items/2524d21455aac111cdee)

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

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

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

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

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

```swift

/// トークンを保存します。
/// - 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を使用できます。

```swift

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が使用できます。

```swift
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には`entitie`と`extended_entities`が存在しますが、Tweetに含む画像を全て取得したい場合は`extended_entities`を使う必要がありました。（`entities`には最初の一枚しか入っていないため）

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

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

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

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

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

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

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

```swift
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にしたい場合などは、全角なら１文字、半角なら0.5文字としてカウントすると良さそうです。

```swift
// 半角英数字は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にキャストすることでケースを分けたハンドリングが可能です。

```swift
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をアプリで利用する際は導入を検討してみて欲しいです。