TwitterAPIとSwiftを使ってiOSアプリを作ろう！ - 後編 - #dotsgirls

# はじめに

こんぬづは、講師の田中です。

このQiitaは 2017/02/19 開催の[【男性参加可\!学生無料】TwitterAPIとSwiftを使ってiOSアプリを作ろう！ − presents by dots\.女子部 \#dotsgirls \- dots\.［ドッツ］](https://eventdots.jp/event/611130) のハンズオン用の教材になります。

- 前編: [TwitterAPIとSwiftを使ってiOSアプリを作ろう！ \- 前編 \- \#dotsgirls \- Qiita](http://qiita.com/ktanaka117/private/e721b076ceffd182123f)
- カスタマイズ編: [TwitterAPIとSwiftを使ってiOSアプリを作ろう！ \- カスタマイズ編 \- \#dotsgirls \- Qiita](http://qiita.com/ktanaka117/private/2246ac17523bc0b289a2)
- Xcode編: [TwitterAPIとSwiftを使ってiOSアプリを作ろう！ \- Xcode編 \- \#dotsgirls \- Qiita](http://qiita.com/akatsuki174/private/cce0d7e3d2360526e441)

# 後編でやること
- 5. Twitterへのログイン機能を作る
- 6. タイムラインの取得機能を作る
- 7. データモデルにjson用のinitを作る
- 8. パーサーを作る(事前準備)
- 9. UIにデータを流し込む

# 5. Twitterへのログイン機能を作る

## 5-1. Account.swiftとそのstructを宣言する

staticキーワードを使うことで型に紐づくプロパティを宣言することができます。これにより、故意に `nil` を代入したり、別な情報を再代入してしまわない限りはアプリが動作し続ける間はメモリ上に保持し続けることになります。
アカウント情報は今後各所で使っていくであろう情報なので、ログイン時に一度保持したあとは使い回しが効きやすいようにstaticな作りにしています。

また、ここではAccountsフレームワークをimportしています。これはTwitterにアクセスするためのCredentialやユーザーの管理を簡略化してくれるものになります。
Accountsフレームワークに関するクラスは頭に "AC" というプレフィックスがつきます。

```swift
// Account.swift

import Accounts

struct Account {
    
    static var twitterAccount: ACAccount?
    
}
```

## 5-2. 端末内のTwitterアカウントの情報を取得する

今回のアプリは端末内に登録されているTwitterアカウントを使用するので、iPhone(またはシミュレータ)の　`設定 > Twitter` からアカウントを登録しておいてください。

![スクリーンショット 2017-02-19 9.21.08.png](https://qiita-image-store.s3.amazonaws.com/0/46597/1e644198-24de-21d6-b7ae-04de86a03d99.png)

ログイン機能はLoginCommunicatorというstructを自作していきます。
先ほど説明したAccountsフレームワークの他に、Twitterが利用可能であるかを確認するためにSocialフレームワークもimportします。
Socialフレームワークに関するクラスは頭に "SL" というプレフィックスがつきます。

ログインには非同期処理が必要となるので、引数handlerをクロージャで定義しています。

```swift
// LoginCommunicator.swift

import Social
import Accounts

struct LoginCommunicator {
    // @escaping = クロージャが関数のスコープ外で保持される可能性があることを示す属性。
    func login(handler: @escaping (Bool) -> ()) {
        // Twitterが利用可能かどうかを確認する
        if !SLComposeViewController.isAvailable(forServiceType: SLServiceTypeTwitter) {
            handler(false)
            return
        }
        
        // 端末内のアカウントストアを参照
        let store = ACAccountStore()
        let type = store.accountType(withAccountTypeIdentifier: ACAccountTypeIdentifierTwitter)
        
        // Twitterのアカウント情報へのアクセスをリクエスト
        store.requestAccessToAccounts(with: type, options: nil) { granted, error in
            
            // 承認されなかった場合
            guard granted else {
                handler(false)
                return
            }
            
            // 何らかのエラーがあった場合
            if let _ = error {
                handler(false)
                return
            }
            
            let accounts = store.accounts(with: type)
            
            // 複数あるであろうアカウントの中から最初の一つを取得
            if let account = accounts?.first as? ACAccount {
                // staticなtwitterAccountに保持
                Account.twitterAccount = account
                handler(true)
            }
        }
    }
}
```

実装したLoginCommunicatorとloginメソッドをTimelineViewControllerで呼び出します。
ここでは結果のBOOL値(= isSuccess)によって成功か失敗かをハンドリングしています。

```swift
// TimelineViewController.swift

import UIKit

class TimelineViewController: UIViewController {
    @IBOutlet weak var tableView: UITableView!

    var tweets: [Tweet] = []

    override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView.delegate = self
        tableView.dataSource = self
      
        LoginCommunicator().login() { isSuccess in
            switch isSuccess {
            case false:
                print("ログイン失敗")
            case true:
                print("ログイン成功")
            }
        }
    }
}
```

### **クロージャとは？**

[あとで追記]

### @escapingとは？

escaping属性は関数に引数として渡されたクロージャが、関数のスコープの外で保持される可能性があるときにつける属性です。escaping属性をつけることによって、コンパイラがそのクロージャ内で扱う変数をキャプチャする必要があるかどうかを判定します。

今回でいうと以下のような実装例。
LoginCommunicatorのloginメソッドに渡すクロージャをTimelineViewControllerが強参照として変数で保持している「可能性がある」のでescaping属性を付ける必要があります。

```swift
// TimelineViewController.swift

// --- 省略 ---

var loginClosure: (Bool) -> () = { isSuccess in
    switch isSuccess {
    case false:
        print("ログイン失敗")
    case true:
        print("ログイン成功")
    }
}

override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView.delegate = self
        tableView.dataSource = self
        
        LoginCommunicator().login(handler: loginClosure)
    }
}

// --- 省略 ---
```

escaping属性に関しては最初は「Xcodeの補完に任せてつけるおまじないだ」くらいに覚えておいてもらっても構いません。

# 6. タイムラインの取得機能を作る

ログイン同様、タイムラインの情報をTwitter APIから取得する通信も非同期なのでクロージャを引数に取るhandlerを定義きます。結果として通信が成功したらDataを返して、失敗したらErrorを返すという実装になっています。

SLRequestというHttpリクエストを表す型に呼び出すサービスのタイプを設定し(= forServiceType)、GETで、取得先のURLを指定します。

ここで先ほどログイン時に取得したAccount.twitterAccountをリクエストにセットします。twitterAccountを宣言している型であるACAccountから認証情報などを取り出して使うためです。

作成したリクエストをperformメソッドで実行して、通信が成功すればhandlerにdata(= json)を入れて返し、失敗すればerrorを入れて返します。

```swift
// TwitterCommunicator.swift

import Social

struct TwitterCommunicator {
    func getTimeline(handler: @escaping (Data?, Error?) -> ()) {
        let request = SLRequest(
            forServiceType: SLServiceTypeTwitter,
            requestMethod: .GET,
            url: URL(string: "https://api.twitter.com/1.1/statuses/home_timeline.json"),
            parameters: nil
        )
        
        request?.account = Account.twitterAccount
        
        request?.perform { data, response, error in
            if let error = error {
                handler(nil, error)
                return
            }
            
            handler(data, error)
        }
    }
}
```

TwitterCommunicatorができたら、先ほど作成したログイン処理の成功時に呼び出すようにしましょう。

```swift
// TimelineViewController.swift

// --- 省略 ---

override func viewDidLoad() {
    super.viewDidLoad()
        
    tableView.delegate = self
    tableView.dataSource = self
        
    LoginCommunicator().login() { isSuccess in
            switch isSuccess {
            case false:
                print("ログイン失敗")
            case true:
                print("ログイン成功")
                
            TwitterCommunicator().getTimeline() { [weak self] data, error in
                    
                if let error = error {
                    print(error)
                    return
                }

                print(data)
            }
        }
    }
}
// --- 省略 ---
```



# 7. データモデルにjson用のinitを作る

## 7-1. Userにjsonからのinitを作る

### guardとは？

guardというのはOptionalの値を安全にアンラップする仕組みです。guardでアンラップしたOptionalの値はそれ以降、同一スコープ内で非Optionalな値として扱うことができます。もしguardでアンラップしようとした値がnilだった場合、elseに続くスコープ内でreturnします。

例えば `guard let dictionary = json as? [String: Any] else { return nil }` は「jsonを[String: Any]型に型変換した結果、失敗したらnilを返す」という意味になります。

```swift
// User.swift

import Foundation

struct User {
    let id: String
    
    // (@)ktanaka117
    let screenName: String
    
    // ダンボー田中
    let name: String
    
    // プロフィール画像URL
    let profileImageURL: String

    init?(json: Any) {
        guard let dictionary = json as? [String: Any] else { return nil }
        
        guard let id = dictionary["id_str"] as? String else { return nil }
        guard let screenName = dictionary["screen_name"] as? String else { return nil }
        guard let name = dictionary["name"] as? String else { return nil }
        guard let profileImageURL = dictionary["profile_image_url_https"] as? String else { return nil }
        
        self.id = id
        self.screenName = screenName
        self.name = name
        self.profileImageURL = profileImageURL
    }
}
```

同様に、Tweet型にもjsonからのinitを作成しましょう。

```swift
Tweet.swift

import Foundation

struct Tweet {
    // ツイートのid
    let id: String
    
    // ツイートの本文
    let text: String
    
    // このツイートを発言したユーザー
    let user: User
    
    init(id: String, text: String, user: User) {
        self.id = id
        self.text = text
        self.user = user
    }
    
    init?(json: Any) {
        guard let dictionary = json as? [String: Any] else { return nil }
        
        guard let id = dictionary["id_str"] as? String else { return nil }
        guard let text = dictionary["text"] as? String else { return nil }
        guard let userJSON = dictionary["user"] else { return nil }
        guard let user = User(json: userJSON) else { return nil }
        
        self.id = id
        self.text = text
        self.user = user
    }
}
```

# 8. パーサーを作る

取得したjsonはシリアライズして、データモデルに変換する必要があります。
その処理を行ってくれるTimelineParserを作りましょう。

取得したデータのシリアライズにはJSONSerializationクラスを使います。
シリアライズしたデータを[Any]型に変換し、それをflatMapを使ってTweet型に変換して戻り値として返す実装です。

```swift
// TimelineParser.swift

import Foundation

struct TimelineParser {
    func parse(data: Data) -> [Tweet] {
        let serializedData = try! JSONSerialization.jsonObject(with: data, options: .allowFragments)
        
        let json = serializedData as! [Any]
            
        let timeline: [Tweet] = json.flatMap {
            Tweet(json: $0)
        }
        
        return timeline
    }
}
```

TimelineParserが出来たらTwitterCommunicatorでdataを取得していた箇所に埋め込みましょう。

```swift
// TimelineViewController.swift

// --- 省略 ---

override func viewDidLoad() {
    super.viewDidLoad()
        
    tableView.delegate = self
    tableView.dataSource = self
        
    LoginCommunicator().login() { isSuccess in
            switch isSuccess {
            case false:
                print("ログイン失敗")
            case true:
                print("ログイン成功")
                
            TwitterCommunicator().getTimeline() { [weak self] data, error in
                    
                if let error = error {
                    print(error)
                    return
                }

                print(data)
                let timelineParser = TimelineParser()
                let tweets = timelineParser.parse(data: data!)

                print(tweets)
            }
        }
    }
}
// --- 省略 ---
```

# 9. UIにデータを流し込む

パースしたデータはTimelineViewControllerのtweets変数に代入します。
代入した後は `DispatchQueue.main.async` を使って、メインスレッドでUIの更新を行います。

```swift
// TimelineViewController.swift

// --- 省略 ---

override func viewDidLoad() {
    super.viewDidLoad()
        
    tableView.delegate = self
    tableView.dataSource = self
        
    LoginCommunicator().login() { isSuccess in
            switch isSuccess {
            case false:
                print("ログイン失敗")
            case true:
                print("ログイン成功")
                
            TwitterCommunicator().getTimeline() { [weak self] data, error in
                    
                if let error = error {
                    print(error)
                    return
                }

                print(data)
                let timelineParser = TimelineParser()
                let tweets = timelineParser.parse(data: data!)
                print(tweets)

                self?.tweets = tweets
                    
                DispatchQueue.main.async {
                    self?.tableView.reloadData()
                }
            }
        }
    }
}
// --- 省略 ---
```

## 完成!!!

![スクリーンショット 2017-02-19 3.20.37.png](https://qiita-image-store.s3.amazonaws.com/0/46597/771efc84-a619-b19a-f4dd-ec4248be5bc8.png)

# 後編の終わり

次はカスタマイズ編。
[TwitterAPIとSwiftを使ってiOSアプリを作ろう！ \- カスタマイズ編 \- \#dotsgirls \- Qiita](http://qiita.com/ktanaka117/private/2246ac17523bc0b289a2)