【Swift】Social Frameworkを使ってTwitterに動画付きツイートをする

# はじめに
個人的に開発を続けている[こちらのアプリ](https://itunes.apple.com/jp/app/guan-cha-ri-ji-mei-rino-cuo/id1063102538?mt=8)の機能として動画つきのツイートをtwitterに投稿できるようにしたいと思い、いろいろ試行錯誤しましたが、思いのほか手こずったので書き残します。

今回はSocial Framework と Accounts Framework を使って実装していきますが、
動画のアップロードに関することを主題として扱うため、アカウントの取得方法やSocial Frameworkの詳しい使い方などは省略しています。

# Twitter APIについて

Swiftのコードを見る前に、まずはじめに私がつまずいたTwitterのAPIの仕様について見てみましょう。

## エンドポイント

今回は動画のアップロード用のエンドポイントとツイートをポストするエンドポイントの２つを使います。
https://upload.twitter.com/1.1/media/upload.json
https://api.twitter.com/1.1/statuses/update.json

## APIリクエストの手順

Twitterでは、APIから動画のアップロードを行うのに**合計3回のAPIリクエスト**を投げることになります。
（これを知らずに画像のアップロードと同じようにAPIを叩いたりしていきなりつまずきました。。）

ざっくり説明していきますが、この辺りは以下を参考にしていただいた方がわかりやすいと思います。
https://dev.twitter.com/rest/public/uploading-media#chunkedupload
https://syncer.jp/twitter-api-matome/post/media/upload_chunked

また、３度のリクエストで動画をアップロードした後、さらにその結果をもってツイートをポストするので、合わせて４回のAPIリクエストを行います。

では、３度のリクエストの流れをざっくりと見ていきましょう。

### INIT リクエスト

動画アップロードの1度目のリクエストでは、「こういうファイルを送りますよ」という準備的なリクエストを送ります。

パラメータは以下

- command
- media_type（例：`video/mp4`）
- total_bytes（例：`4430752`）

commandパラメータは3回のリクエスト全てに出てきますが、何度目のリクエスト（どの工程のリクエスト）かを判別するものになります。
1度目のリクエストの場合はcommandの値はINITになります。

送信されるパラメータ例：
`"command=INIT&media_type=video/mp4&total_bytes=4430752"`

成功すると以下のようなレスポンスが返ってきます。

```
{
  "media_id": 601413451156586496,
  "media_id_string": "601413451156586496",
  "expires_after_secs": 3599
}
```

### APPEND リクエスト

2度目のリクエストでは実際に動画ファイルをアップロードするリクエストになります。

パラメータは以下

- command
- media_id
- segment_index

`media_id`はINITリクエストのレスポンスとして得られた値を使用します。

また、上記パラメータに加えて、実際にアップロードするファイルをつけます。

パラメータの詳細は前述の参考URLをご参照ください。笑

送信されるパラメータ例：
`"command=APPEND&media_id=601413451156586496&segment_index=0"`

このリクエストは、成功しても特にレスポンスデータはありません。
200番台のステータスコードが返ってきたら成功と判断します。

### FINALIZE リクエスト

最後のリクエストです。
このリクエストでアップロードした動画が有効になります。

パラメータは以下

- command
- media_id

こちらの`media_id`もAPPENDリクエストの時と同様にINITリクエストのレスポンスで得られた値を使用します。

こちらは成功すると以下のようなレスポンスが返ってきます。
ようやくアップロードした動画をつけてツイートする準備が整いました。

```
{
  "media_id": 601413451156586496,
  "media_id_string": "601413451156586496",
  "size": 4430752,
  "expires_after_secs": 3600,
  "video": {
    "video_type": "video/mp4"
  }
}
```

### 動画をつけてツイートする

前述の工程で得られた`media_id`をツイートをポストする際の`media_ids`パラメータに設定した上でAPIを叩けばOKです。


# Swiftで書いてみる

前述の説明ではかなり端折った部分が多いので、実際にコードを見た方がわかるかもしれません。

※冒頭で記述したアプリ内で使う予定のコードを改変したものなので、若干適当な部分がありますがご容赦ください。 :raised_hands: 
また、自分が使う都合なので、今回のコードはfileManagerでのファイルパスからの動画ファイル取得に限っています。


```swift:Twitter.swift
import UIKit
import Social
import Accounts
import SwiftyJSON

struct Twitter {
    
    var account: ACAccount
    let fileManager = NSFileManager.defaultManager()

    init(account: ACAccount) {
        self.account = account
    }
    
    let uploadURL = NSURL(string: "https://upload.twitter.com/1.1/media/upload.json")
    let statusURL = NSURL(string: "https://api.twitter.com/1.1/statuses/update.json")
    
    func postWithMovie(tweet: String, filePath: String, success: (responseData: NSData!, urlResponse: NSHTTPURLResponse!) -> Void, failure: ((error: NSError!) -> Void)?) {
        guard let mediaData = NSData(contentsOfFile: filePath) else {
            return
        }
        do {
            let fileAttr = try fileManager.attributesOfItemAtPath(filePath)
            if let fileSize = fileAttr[NSFileSize] as? Int {
                postMedia(tweet, mediaData: mediaData, fileSize: String(fileSize), success: success, failure: failure)
            }
        } catch {
            return
        }
    }
    
    // このメソッドが前述までの３回のリクエスト＋動画付きツイートを行う部分
    private func postMedia(tweet: String, mediaData: NSData, fileSize: String, success: (responseData: NSData!, urlResponse: NSHTTPURLResponse!) -> Void, failure: ((error: NSError!) -> Void)?) {

        // INIT リクエスト
        uploadVideoInitRequest(fileSize, success: { (responseData) -> () in
            let json = JSON(data: responseData)
            // レスポンスから media_id_string を取得して APPEND リクエストに利用
            let mediaIdString = json["media_id_string"].stringValue

            // APPEND リクエスト
            self.uploadVideoAppendRequest(mediaData, mediaIdString: mediaIdString, success: { () -> () in

                // FINALIZE リクエスト
                self.uploadVideoFinalizeRequest(mediaIdString, success: { (responseData) -> () in
                    
                    let statusKey: NSString = "status"
                    let mediaIDKey: NSString = "media_ids"
                    
                    let statusRequest = SLRequest(forServiceType: SLServiceTypeTwitter, requestMethod: .POST, URL: self.statusURL, parameters: [statusKey : tweet, mediaIDKey : mediaIdString])
                    
                    statusRequest.account = self.account
                    
                    // 動画をつけてツイート
                    statusRequest.performRequestWithHandler { (responseData: NSData!, urlResponse: NSHTTPURLResponse!, error: NSError!) -> Void in
                        if let error = error {
                            failure?(error: error)
                        }
                        // 成功したらなんかする
                        success(responseData: responseData, urlResponse: urlResponse)
                    }
                    
                    }, failure: { (error) -> () in
                        failure?(error: error)
                })
                }, failure: { (error) -> () in
                    failure?(error: error)
            })
            }) { (error) -> () in
                failure?(error: error)
        }
    }

    // INIT リクエスト
    private func uploadVideoInitRequest(fileSize: String, success: (responseData: NSData) -> (), failure: ((error: NSError) -> ())?) {
        let commandKey: NSString = "command"
        let mediaTypeKey: NSString = "media_type"
        let totalBytesKey: NSString = "total_bytes"
        let initParams: [NSString: AnyObject] = [commandKey: "INIT", mediaTypeKey: "video/mp4", totalBytesKey: fileSize]
        let initRequest = SLRequest(forServiceType: SLServiceTypeTwitter, requestMethod: .POST, URL: self.uploadURL, parameters: initParams)
        initRequest.account = account
        initRequest.performRequestWithHandler { (responseData: NSData!, urlResponse: NSHTTPURLResponse!, error: NSError!) -> Void in
            if let error = error {
                failure?(error: error)
                return
            }
            success(responseData: responseData)
        }
    }
    
    // APPEND リクエスト
    private func uploadVideoAppendRequest(mediaData: NSData, mediaIdString: String, success: () -> (), failure: ((error: NSError) -> ())?) {
        let commandKey: NSString = "command"
        let mediaIdKey: NSString = "media_id"
        let segmentIndexKey: NSString = "segment_index"
        let appendParam: [NSString: AnyObject] = [commandKey: "APPEND", mediaIdKey: mediaIdString, segmentIndexKey: "0"]
        let appendRequest = SLRequest(forServiceType: SLServiceTypeTwitter, requestMethod: .POST, URL: self.uploadURL, parameters: appendParam)
        appendRequest.addMultipartData(mediaData, withName: "media", type: "video/mp4", filename: nil)
        appendRequest.account = account
        appendRequest.performRequestWithHandler { (responseData: NSData!, urlResponse: NSHTTPURLResponse!, error: NSError!) -> Void in
            if let error = error {
                failure?(error: error)
                return
            }
            if urlResponse.statusCode < 300 && urlResponse.statusCode >= 200 {
                success()
            }
        }
    }
    
    // FINALIZE リクエスト
    private func uploadVideoFinalizeRequest(mediaIdString: String, success: (responseData: NSData) -> (), failure: ((error: NSError) -> ())?) {
        let commandKey: NSString = "command"
        let mediaIdKey: NSString = "media_id"
        let finalizeParam: [NSString: AnyObject] = [commandKey: "FINALIZE", mediaIdKey: mediaIdString]
        let finalizeRequest = SLRequest(forServiceType: SLServiceTypeTwitter, requestMethod: .POST, URL: self.uploadURL, parameters: finalizeParam)
        finalizeRequest.account = account
        finalizeRequest.performRequestWithHandler { (responseData: NSData!, urlResponse: NSHTTPURLResponse!, error: NSError!) -> Void in
            if let error = error {
                failure?(error: error)
                return
            }
            success(responseData: responseData)
        }
    }

}

```

これで以下のように使えます。

```swift
let twitter = Twitter(account: account) // accountは 事前に取得したACAccount
twitter.postWithMovie("ツイートの文章",
    filePath: "動画ファイルのパス",
    success: { (responseData, urlResponse) -> Void in
        // 成功した時になんかする
        // 「ツイートしました」ってアラート出したり？
    }) { (error) -> Void in
        // エラー時になんかする
}
```

# おわりに

Social Frameworkでさらっとできるかと思っていたのですが、思いの外苦戦してしまいました。
なんだかすごく面倒ですね、、知らないだけでもっと簡単な方法があるんでしょうか。。
ご存知の方いらっしゃったら教えてください。 :pray: