Xcode 8.2, Swift 3.0でTwitterの認証を通してタイムラインを取得するまで

こんにちは :bird:

実務としてiOSエンジニアをはじめて半年が経ちSwiftやXcodeに関する知見が溜まってきたので、Objective-C時代に作っていたやつを~~（ :christmas_tree: への邪念で）~~Swift 3.0で書き換えたりしました。その際にハマったところや、そもそものクロージャまわりの知識が若干足りず右往左往したりしたところがあったので反省がてらViewController.swiftを晒していきたいと思います。

# Codes

[keisei1092/TimerTweetViewer](https://github.com/keisei1092/TimerTweetViewer/releases/tag/v0.1)

# やったこと

## `Accounts`, `Social` ライブラリをimport

```swift:ViewController.swift
import Accounts
import Social
```

## ViewControllerでグローバルに使う変数を宣言

```swift:ViewController.swift
var accountStore: ACAccountStore = ACAccountStore()
var twitterAccount: ACAccount?
var tweets: [Tweet] = []
```

## `Tweet` 構造体を宣言

```swift:Tweet.swift
import Foundation

struct Tweet {
    let text: String
    let createdAt: String
    let user: User

    var dump: String {
        get {
            return "\(text) by @\(user.screenName)"
        }
    }
}

struct User {
    let name: String
    let screenName: String
    let profileImageURLHTTPS: String
}
```

## `viewDidLoad()`

```swift:ViewController.swift
getAccounts { (accounts: [ACAccount]) -> Void in
    self.showAccountSelectSheet(accounts: accounts)
}
```

ちょっと冗長＋ここだけで`getTimeline()`をしているのがパッと見わからないのでリファクタリングしたい

## `getAccounts(callback: @escaping ([ACAccount]) -> Void)`

```swift:ViewController.swift
func getAccounts(callback: @escaping ([ACAccount]) -> Void) {
    let accountType: ACAccountType = accountStore.accountType(withAccountTypeIdentifier: ACAccountTypeIdentifierTwitter)
    accountStore.requestAccessToAccounts(with: accountType, options: nil) { (granted: Bool, error: Error?) -> Void in
        guard error == nil else {
            print("error! \(error)")
            return
        }
        guard granted else {
            print("error! Twitterアカウントの利用が許可されていません")
            return
        }

        let accounts = self.accountStore.accounts(with: accountType) as! [ACAccount]
        guard accounts.count != 0 else {
            print("error! 設定画面からアカウントを設定してください")
            return
        }
        print("アカウント取得完了")
        callback(accounts)
    }
}
```

（循環参照になってないか後で見ないと...）

## `showAccountSelectSheet(accounts: [ACAccount])`

```swift:ViewController.swift
private func showAccountSelectSheet(accounts: [ACAccount]) {
    let alert = UIAlertController(title: "Twitter", message: "Choose an account", preferredStyle: .actionSheet)

    for account in accounts {
        alert.addAction(UIAlertAction(title: account.username, style: .default, handler: { [weak self] (action) -> Void in
            if let unwrapSelf = self {
                unwrapSelf.twitterAccount = account
                unwrapSelf.getTimeline()
            }
        }))
    }

    alert.popoverPresentationController?.sourceView = self.view
    alert.popoverPresentationController?.sourceRect = CGRect(x: self.view.bounds.size.width / 2, y: self.view.bounds.size.height / 2, width: 1.0, height: 1.0)
    alert.popoverPresentationController?.permittedArrowDirections = .down
    alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
    present(alert, animated: true, completion: nil)
}
```

## `getTimeline()`

```swift:ViewController.swift
private func getTimeline() {
    let url = URL(string: "https://api.twitter.com/1.1/statuses/home_timeline.json?count=100")
    guard let request = SLRequest(forServiceType: SLServiceTypeTwitter, requestMethod: .GET, url: url, parameters: nil) else {
        return
    }
    request.account = twitterAccount
    request.perform { (responseData, response, error) -> Void in
        if error != nil {
            print(error ?? "error in performing request :[")
        } else {
            do {
                guard let responseData = responseData else {
                    return
                }
                let result = try JSONSerialization.jsonObject(with: responseData, options: .allowFragments)
                for tweet in result as! [AnyObject] { // errorsが返ってくることがある
                    guard let text = tweet["text"] as? String, let createdAt = tweet["created_at"] as? String else { // これmodel側でやるべきな感じ
                        print("failed to map tweet string from JSON")
                        return
                    }

                    let user = tweet["user"] as? [String: Any]
                    guard let userName = user?["name"] as? String, let userScreenName = user?["screen_name"] as? String, let userProfileImageURLHTTPS = user?["profile_image_url_https"] as? String else {
                        print("failed to map user string from JSON")
                        return
                    }

                    let tweetObject = Tweet(text: text, createdAt: createdAt, user: User(name: userName, screenName: userScreenName, profileImageURLHTTPS: userProfileImageURLHTTPS))
                    self.tweets.append(tweetObject)
                }
                // ここでなにかする
                // （例えば self.tableView.reloadData() とか）
            }  catch let error as NSError {
                print(error)
            }
        }
    }
}
```

# がんばったところ

## リクエスト叩いた時のエラー

Couldn't authenticate you的なのが出た。
iOSに紐付けていたTwitterアカウントが古いものだったっぽく、設定アプリからパスワードを入力し直したらいけました。

## モデルのマッピングしんどい

しんどい。
でもそれ以上に、こんだけのアプリで [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON) をインポートして依存性に縛られるのもなぁ。
とゆことで手でマッピングしてみました。
とりあえず自分が欲しかった、名前とかアイコンの画像URLとか、そこらへんを引っ張っています。他の要素が必要でしたらTweet.swiftでプロパティを増やして、getTimeline()のクロージャの中で引っ張ってくれば良いかとおもいます。

## Swiftのcomputed properties便利

詳しくは [こちら](https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Properties.html#//apple_ref/doc/uid/TP40014097-CH14-ID259) 。

ツイートをprint()で確認したいときにとりあえずcomputed propertiesを叩けばいいから便利。 `"\(tweet.text) by \(tweet.user.name)"` とか毎回打つのは大変なので。

# 参考

* http://t-higashi.com/twitter-api-one
* http://qiita.com/kwst/items/2ca7937a67a4f8ff59cb
* https://gist.github.com/SatoshiKawabata/c73542cb164d0abfe2a1
* http://qiita.com/nnsnodnb/items/d85235bca74f58c71b3f
* http://qiita.com/teradonburi/items/f196b58d51372f7a3aab