はじめに
こんぬづは、講師の田中です。
このQiitaは 2017/02/19 開催の【男性参加可!学生無料】TwitterAPIとSwiftを使ってiOSアプリを作ろう! − presents by dots.女子部 #dotsgirls - dots.[ドッツ] のハンズオン用の教材になります。
- 前編: TwitterAPIとSwiftを使ってiOSアプリを作ろう! - 前編 - #dotsgirls - Qiita
- カスタマイズ編: TwitterAPIとSwiftを使ってiOSアプリを作ろう! - カスタマイズ編 - #dotsgirls - Qiita
- Xcode編: TwitterAPIとSwiftを使ってiOSアプリを作ろう! - Xcode編 - #dotsgirls - Qiita
後編でやること
-
- Twitterへのログイン機能を作る
-
- タイムラインの取得機能を作る
-
- データモデルにjson用のinitを作る
-
- パーサーを作る(事前準備)
-
- UIにデータを流し込む
5. Twitterへのログイン機能を作る
5-1. Account.swiftとそのstructを宣言する
staticキーワードを使うことで型に紐づくプロパティを宣言することができます。これにより、故意に nil
を代入したり、別な情報を再代入してしまわない限りはアプリが動作し続ける間はメモリ上に保持し続けることになります。
アカウント情報は今後各所で使っていくであろう情報なので、ログイン時に一度保持したあとは使い回しが効きやすいようにstaticな作りにしています。
また、ここではAccountsフレームワークをimportしています。これはTwitterにアクセスするためのCredentialやユーザーの管理を簡略化してくれるものになります。
Accountsフレームワークに関するクラスは頭に "AC" というプレフィックスがつきます。
// Account.swift
import Accounts
struct Account {
static var twitterAccount: ACAccount?
}
5-2. 端末内のTwitterアカウントの情報を取得する
今回のアプリは端末内に登録されているTwitterアカウントを使用するので、iPhone(またはシミュレータ)の 設定 > Twitter
からアカウントを登録しておいてください。
ログイン機能はLoginCommunicatorというstructを自作していきます。
先ほど説明したAccountsフレームワークの他に、Twitterが利用可能であるかを確認するためにSocialフレームワークもimportします。
Socialフレームワークに関するクラスは頭に "SL" というプレフィックスがつきます。
ログインには非同期処理が必要となるので、引数handlerをクロージャで定義しています。
// 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)によって成功か失敗かをハンドリングしています。
// 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属性を付ける必要があります。
// 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を入れて返します。
// 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ができたら、先ほど作成したログイン処理の成功時に呼び出すようにしましょう。
// 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を返す」という意味になります。
// 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を作成しましょう。
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型に変換して戻り値として返す実装です。
// 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を取得していた箇所に埋め込みましょう。
// 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の更新を行います。
// 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()
}
}
}
}
}
// --- 省略 ---
完成!!!
後編の終わり
次はカスタマイズ編。
TwitterAPIとSwiftを使ってiOSアプリを作ろう! - カスタマイズ編 - #dotsgirls - Qiita