はじめに

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

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

iOS 11で起こる問題

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

設定アプリの変化

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

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

Social.frameworkへの影響

iOS 10まで

Social.frameworkを利用したTwitter Share機能を簡単に実装することができました

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) インストール済み
何も表示されない

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

iOS11から各serviceTypeがdeprecated扱い

Twitterだけではなく、Facebookなども同様です

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は?

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

iOS 11ではTwitterKitを使う

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

環境

version
TwitterKit v3.1.1
Deployment Target iOS 9.0 ↑

TwitterKitの導入

基本的なことは公式(Getting Started)にまとまっています
Social.frameworkからのマイグレーションという観点では公式(Migrating from iOS Social Framework)やこちらの記事にまとまっています

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

  1. TwitterKitを直接、もしくはCocoaPods経由で導入(Carthage非対応 😱)
  2. Twitter Application Managementでappを作成
  3. info.plistにCFBundleURLTypesLSApplicationQueriesSchemesを追加
  4. AppDelegate#application(_:open:options:) にハンドリング処理を追加
  5. アプリ起動時に Twitter.sharedInstance().start(withConsumerKey: key, consumerSecret: secret) する

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

1アカウントの連携で良い場合

投稿

基本的に以下の実装だけでTwitterKitがよしなにしてくれます(ログイン周りも含め)

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

Desktop applications only support the oauth_callback value 'oob' というエラーが出る場合はApplication ManagementでCallback URLを設定することで回避できます

投稿の様子
(Twitter App有)
簡単なフロー
01_auth_and_tweet.gif Single_account.png

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

ログアウト

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

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

複数アカウントの連携をする場合

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

実装例を元にした躓きポイントとその対処方法

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

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

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

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

02_account_management.gif

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

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

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

2. APIを叩いて複数アカウントの情報を一度に取得する

TwitterKitはアカウントを連携してもuserIDしか保存しません(正確にはauthTokenauthTokenSecretも)
name, screenName等の情報は自分で取得する必要があります

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

GET users/lookup

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

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

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

複数ユーザ取得の実装例
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を用いる場合は最後に連携したアカウントで固定となっています

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

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

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

アカウント変更処理の実装例
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)
    }
}
アカウント切り替えの様子
(コードによる切り替え + 手動切り替え)
03_change_default_account.gif

全体の流れ

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

Multi_account

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

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

その他

連携画面のプライバシーポリシーと利用規約

これらのリンクはTwitter Application Managementで以下2つのURLを設定することで有効になります

Application_Details.png

投稿に失敗するケース

TwitterKit v3.1.1時点で再現するバグとして、^<>\|`など、一部文字列を含む投稿ができないという問題があります
Twitter DevelopersのForumsにTopicはあるようですが、こちらはまだ未解決のようです

投稿完了のcompletionについて

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

※ 2年ほど前に同様のTopicが上がっているようですが、未解決のままCloseされてしまっているようです

端末の時間がずれているとエラーが返ってくる

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

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

サンプルアプリ

iOS11-TwitterShareSample [2017/12/06 更新]

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

※ TwitterUserデータをキャッシュするためにRealmを使っています

まとめ

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

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

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