iOS11のTwitter投稿対応（Social.framework → TwitterKit）

# **はじめに**
2017/9/20(水)にiOS11が一般公開されました
これにより、**一部のユーザーがTwitterに投稿できなくなる**という不具合をアナウンスしているのをちらほら見かけます

以下では、その原因と対応方法の一例を紹介します
「TwitterKit対応忘れてた！」という方の参考になれば幸いです

# **iOS 11で起こる問題**
結論から言うと、iOS 11ではTwitterアプリが端末にない場合、**Social.framework**または**UIActivityViewController**によるTwitterへのShareが**できなくなりました**

## **設定アプリの変化**
iOS 11で設定アプリからTwitter, Facebookなどの項目が消えました

<img src="https://qiita-image-store.s3.amazonaws.com/0/69585/79fd9b06-63c6-54c3-218c-4c7207ca8de1.png" width=500 />

これにより、Twitterのアカウント情報を端末へ登録できなくなりました

## **Social.frameworkへの影響**
### **iOS 10まで**
Social.frameworkを利用したTwitter Share機能を簡単に実装することができました

```swift:Shareの実装例
func shareWithSocialFramework(text: String, on viewController: UIViewController) {
    guard let composeVC = SLComposeViewController(forServiceType: SLServiceTypeTwitter) else {
        return
    }
    composeVC.setInitialText(text)
    viewController.present(composeVC, animated: true, completion: nil)
}
```

### **iOS 11**
上記コードを実行すると以下のような挙動になります

| Twitterアプリ無し | Twitterアプリ(v7.7.2) インストール済み |
|:-:|:-:|
| 何も表示されない | <img src="https://qiita-image-store.s3.amazonaws.com/0/69585/708399ba-2cae-54cf-9916-2adce1d60ebf.png" width=300 /> |

Twitterアプリが端末にインストールされている場合はTwitterアプリのShare Extensionで表示されますが、アプリがない場合はうんともすんとも言いません

## **iOS11から各serviceTypeがdeprecated扱い**
Twitterだけではなく、Facebookなども同様です

```swift:Social/SLServiceTypes.h
@available(iOS, introduced: 6.0, deprecated: 11.0)
public let SLServiceTypeTwitter: String
@available(iOS, introduced: 6.0, deprecated: 11.0)
public let SLServiceTypeFacebook: String
...
```

## **UIActivityViewControllerは？**
<img src="https://qiita-image-store.s3.amazonaws.com/0/69585/78c3f3a8-dd4f-db20-3120-5b09c1cbbe32.png" width=300 />

Twitterアプリが無い場合はそもそもこの一覧に表示されません🙅

# **iOS 11ではTwitterKitを使う**
これでは**Twitterアプリが入っていない場合Shareすることができない**ため、iOS11ではSocial.frameworkの代わりに**TwitterKit**を用いて実装する必要があります
TwitterKitでは端末でglobalなアカウント情報は持たず、アプリごとに管理する形になります

## **環境**

|  | version |
| --- | --- |
| TwitterKit | v3.1.1 |
| Deployment Target | iOS 9.0 ↑ |

※ v3.2.1では投稿画面において[スクロール周りのバグ](https://twittercommunity.com/t/twtrcomposerviewcontroller-with-initialtext-and-image-broken-layout/98085)があります

## **TwitterKitの導入**
基本的なことは公式（[Installation](https://github.com/twitter/twitter-kit-ios/wiki/Installation)）にまとまっています
Social.frameworkからのマイグレーションという観点では公式（[Migrating from iOS Social Framework](https://github.com/twitter/twitter-kit-ios/wiki/Migrating-from-iOS-Social-Framework)）や[こちらの記事](http://qiita.com/Takumi_Mori/items/1b27c46114cc839e01e5)にまとまっています

導入手順をざっくりとまとめると以下のとおり

1. TwitterKitを[直接](https://github.com/twitter/twitter-kit-ios/wiki/Installation#install-twitter-kit-manually)、もしくは[CocoaPods](https://github.com/twitter/twitter-kit-ios/wiki/Installation#install-twitter-kit-using-cocoapods)/[Carthage](https://github.com/twitter/twitter-kit-ios/wiki/Installation#install-twitter-kit-using-carthage)経由で導入
1. [Twitter Application Management](https://apps.twitter.com/)でappを作成
1. info.plistに**CFBundleURLTypes**と**LSApplicationQueriesSchemes**を追加
1. `AppDelegate#application(_:open:options:)` にハンドリング処理を追加
1. アプリ起動時に `Twitter.sharedInstance().start(withConsumerKey: key, consumerSecret: secret)` する

3.と4.はアカウント連携時の **[自分のアプリ ⇔ Twitterアプリ]** 間遷移に必要になります

## Callback URLsの設定 **（2018/08/06 追記）**
[Twitter Application Management](https://apps.twitter.com/) -> app -> Settings -> Callback URLs に `twitterkit-{CONSUMERKEY}://` （`{CONSUMERKEY}`は導入時に付与されるConsumer key）を設定する必要があります
この設定が漏れると、一部条件下でログインに失敗するようになりました（2018/06/13〜）

詳細 - **[Callback URLs](https://developer.twitter.com/en/docs/basics/developer-portal/guides/callback-urls)**

## **1アカウントの連携で良い場合**
### **投稿**
基本的に以下の実装だけでTwitterKitがよしなにしてくれます（ログイン周りも含め）

```swift
let composer = TWTRComposer()
composer.setText(text)
composer.show(from: viewController, completion: { result in ... })
```

| 投稿の様子<br>（Twitter App有） | 簡単なフロー |
| :---: | :---: |
| ![01_auth_and_tweet.gif](https://qiita-image-store.s3.amazonaws.com/0/69585/f144a88e-d2a1-f0bc-35a9-62b4926f5dd1.gif) | <img width="700" alt="Single_account.png" src="https://qiita-image-store.s3.amazonaws.com/0/69585/7bd4fbb7-85a4-fc79-53e5-c8522635ae2f.png" /> |

session情報は保持されるため、一度認証すればそれ以降は直接投稿画面が表示されます

### **ログアウト**

```swift
// sessionを管理するTWTRSessionStoreを取得
let sessionStore = Twitter.sharedInstance().sessionStore

// アクティブなアカウントのsessionを取得
if let session = sessionStore.session() {
    // userIDでログアウト
    sessionStore.logOutUserID(session.userID)
}
```

## **複数アカウントの連携をする場合**
ユーザーがその時々によってshareするアカウントを変えたいという欲求に答えるためには複数アカウントへの対応をする必要があります
1アカウントの連携で良い場合に対し、こちらでいくつかの制御を行う必要があります

### **実装例を元にした躓きポイントとその対処方法**
TwitterKitには現状いくつかの躓きポイントがあります（少なくとも自分は躓きました）
以下では、複数アカウントの連携を実現する1つの実装例を元に、対処方法を述べていきます

#### **1. アプリ内で複数アカウントを管理する必要がある**

**iOS10まで**
設定アプリにある以下の画面でアカウントの**追加連携**と**連携解除**ができました

<img src="https://qiita-image-store.s3.amazonaws.com/0/69585/1d22c4cb-7c59-e735-eeea-6a2cce4082c1.png"  width=300 />

**iOS11(TwitterKit)**
自前でアカウントを管理する必要があります
今回は自前の管理画面を用意しました

![02_account_management.gif](https://qiita-image-store.s3.amazonaws.com/0/69585/756fffa4-e102-821c-e014-87eb026eeebd.gif)

Add Accountで追加連携、セル削除で連携解除を行います

[追加連携]
`Twitter#logIn(with:completion)`で明示的に連携処理を走らせることができます

[連携解除]
`TWTRSessionStore#logOutUserID(_:)`で指定した`userID`に対応するsessionを破棄できます

#### **2. APIを叩いて複数アカウントの情報を一度に取得する**
TwitterKitはアカウントを連携しても**`userID`しか保存しません**（正確には`authToken`と`authTokenSecret`も）
`name`, `screenName`等の情報は自分で取得する必要があります

1アカウントのみの取得であれば `TWTRAPIClient#loadUser(userID:completion:)` で取ることができます
ただし、今回はアカウント管理画面表示時に最新のアカウント情報を複数同時に取りたいため、以下のAPIを叩く必要があります

[GET users/lookup](https://dev.twitter.com/rest/reference/get/users/lookup)

`TWTRAPIClient`にはAPIのendpointを指定してリクエストするmethodが用意されています

```swift:TwitterKit.TWTRAPIClient
func urlRequest(withMethod method: String, url URLString: String, parameters: [AnyHashable : Any]?, error: NSErrorPointer) -> URLRequest
func sendTwitterRequest(_ request: URLRequest, completion: @escaping TWTRNetworkCompletion) -> Progress
```

こちらを利用することで同時に複数ユーザの情報を取得できます

```swift:複数ユーザ取得の実装例
func fetchUsers(withUserIDs userIDs: [String]) {
    let client = TWTRAPIClient()
    let userIDsString = userIDs.joined(separator: ",")
    let params = ["user_id": userIDsString]
    var clientError: NSError?

    let url = "https://api.twitter.com/1.1/users/lookup.json"
    let request = client.urlRequest(withMethod: "GET", url: url, parameters: params, error: &clientError)
    client.sendTwitterRequest(request) { (response, data, connectionError) in
        // エラーハンドリングとParse処理
    }
}
```

#### **3. ツイートするアカウントを切り替える**
複数アカウントを連携するとSocial.frameworkと同様にアカウント切り替えのUIが表示されます
Social.frameworkでは、これを切り替えることでそのアカウントが次回以降も保持されていました
しかし、TwitterKitを用いる場合は**最後に連携したアカウントで固定**となっています

<img src="https://qiita-image-store.s3.amazonaws.com/0/69585/6d8bda54-f053-b6a0-5c58-6f1e9621c0fc.png" width=300 />

変更する為の専用methodは存在しなさそうですが、以下のmethodでsessionを上書きすることで変更できます

```swift:TwitterCore.TWTRSessionStore
func save(_ session: TWTRAuthSession, completion: @escaping TWTRSessionStoreSaveCompletion)
```

save処理が非同期なので、直後にツイート画面を表示する場合はcompletionに処理を書く必要があります

```swift:アカウント変更処理の実装例
func changeDefaultUser(toUserID userID: String, completion: @escaping (_ success: Bool) -> Void) {
    let sessionStore = Twitter.sharedInstance().sessionStore
    guard let session = sessionStore.session(forUserID: userID) else {
        completion(false)
        return
    }
    sessionStore.save(session) { (_, _) in
        completion(true)
    }
}
```

| アカウント切り替えの様子<br>（コードによる切り替え + 手動切り替え） |
| :---: |
| ![03_change_default_account.gif](https://qiita-image-store.s3.amazonaws.com/0/69585/730eb5f9-3cb6-3e9c-8319-6b6f009b81c6.gif) |

### **全体の流れ**
これらの点を踏まえ、以下のような形で実装しました（あくまで一例です）
Sessionが無い時のフローはほぼ「1アカウントの連携で良い場合」と同じになるため省略しています

<img width="1127" alt="Multi_account" src="https://qiita-image-store.s3.amazonaws.com/0/69585/ba5cd6b0-b83e-746f-658f-aad312b757b6.png">

最低限ツイートできるようにするだけであれば一部UIを削ることもできますが、なるべくユーザーが迷わないような形を取っています
また、アプリ内の設定等にTwitterアカウント管理画面を用意するのも良いと思います

どこに何を置くかは、アプリ構造やTwitter連携をログインに使っているかどうかなどとの兼ね合いで決めるべきかと思います

# **その他**
### **連携画面のプライバシーポリシーと利用規約**
<img width=300 src="https://qiita-image-store.s3.amazonaws.com/0/69585/4056cfe4-dca1-9a12-aaef-23299973329f.png" />

これらのリンクは[Twitter Application Management](https://apps.twitter.com/)で以下２つのURLを設定することで有効になります

<img width="674" alt="Application_Details.png" src="https://qiita-image-store.s3.amazonaws.com/0/69585/8070b4a6-1789-c09e-ff89-2c66dd02d2ee.png">

### **投稿に失敗するケース**
TwitterKit v3.1.1〜 で再現するバグとして、`` ^<>\|` ``など、一部文字列を含む投稿ができないという問題があります
Twitter DevelopersのForumsに[Topic](https://twittercommunity.com/t/tweets-error/73153)はあるようですが、こちらはまだ未解決のようです

### **投稿完了のcompletionについて**
`TWTRComposer#show(from:completion:)`による投稿が成功した際、投稿画面のmodalが閉じます
ただし、この時**completionが呼ばれるタイミングとしてはmodalが完全に閉じ切るより前になります**
その為、投稿完了後別のVCをmodalとしてpresentする場合には、delayを付けるなどの対応が必要になります

※ 2年ほど前に[同様のTopic](https://twittercommunity.com/t/bug-twitterkit-ios-completion-handler-is-being-called-too-early/55672)が上がっているようですが、未解決のままCloseされてしまっているようです

### **端末の時間がずれているとエラーが返ってくる**
端末の時間がずれた状態でTwitterのAPIを叩くと以下のようなエラーが返り失敗することがあります
`{"errors":[{"code":135,"message":"Timestamp out of bounds."}]}`

必要に応じてユーザーにその旨を伝えるようエラーハンドリングをすると良いかと思います

# **サンプルアプリ**
**[iOS11-TwitterShareSample](https://github.com/naoto0n2/iOS11-TwitterShareSample)** [2017/12/06 更新]

ここまでの内容を元に複数アカウント対応したものをサンプルアプリとしてgithubに上げてあります
正常系の流れを追いやすいよう、エラーハンドリングや細かい処理は省いています
あくまで実装の一例なので、参考程度に見ることをおすすめします

※ TwitterUserデータをキャッシュするために[Realm](https://realm.io/)を使っています

# **まとめ**
1つの実装例をベースにTwitterKitの使い方を紹介してきました

Social.frameworkのときに比べ実装量が一気に増え、必要なら画面を追加する場合もあり、現状維持にしては思ったより対応に工数が取られる印象でした
また、実際にTwitterKitを導入してみたところ、まだまだ不安定で痒いところに手が届かないなと身にしみて感じたので、今後のアップデートで改善されていくことを切に願っています

少し長くなりましたが、TwitterKit導入の参考になれば幸いです