iOS
Swift

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

More than 1 year has passed since last update.


はじめに

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

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


前編でやること


  • ゴールの説明

  • 作るものの説明

  • 環境の説明

  • 1. プロジェクトを作る

  • 2. データモデルを作る

  • 3. UIを作る

  • 4. ダミーデータをUIに反映させる


ゴール


  • Swift / iOSアプリ開発の基本がわかる

  • AutoLayoutを使ったレイアウト作成の方法がわかる

  • SwiftでAPI通信からモデル生成までの流れがわかる


作るもの

こんな感じのTwitterクライアントアプリです。

自分のタイムラインを表示してみましょう!

スクリーンショット 2017-02-17 18.32.56.png

サンプルプロジェクトはこちら。→ ktanaka117/DotsSwiftTwitterClient

こちらのプロジェクトからファイルをコピーすることも適宜発生するので、事前にダウンロードしておいてください。


環境


  • Swift 3.0.2

  • Xcode 8.2.1

  • iPhone 7 iOS Simulator


1. プロジェクトを作る


1-1. プロジェクトの新規作成

LaunchpadなどからXcodeアイコンをクリックし、Xcodeを起動します。

すると以下のようなウィンドウが開くので、 Create a new Xcode project をクリックします。

1.jpg

もしくはXcodeを起動した状態でMacのツールバー上から File > New > Project を選択します。

2.jpg


1-2. 作成するプロジェクトのテンプレートを選択する

選択するテンプレートに応じて、プロジェクト作成時に作られるファイルやソースコードが切り替わります。

今回はiOSアプリを作るのでiOSタブの Single View Application という一番シンプルな構成のものを選択してプロジェクトを作成します。

3.jpg


1-3. プロジェクトに関する情報を入力する

プロジェクトを作成する時には以下の情報を入力する必要があります。


  • Product Nameは、今回は悩んだらとりあえず DotsSwiftTwitterClient と名付けましょう。

  • Languageは Swift で選択してください。

  • Devicesは、今回作るのはiPhoneアプリなので iPhone を選択してください。

4.jpg


1-4. 保存する場所を選択する

どこでもオーケーです。悩んだらデスクトップ!!!

5.jpg


1-5. プロジェクトの作成完了

プロジェクトの作成が完了したら、以下の画面が表示されます。

6.jpg


プロジェクト立ち上げ時に生成されるファイルの解説

今回扱うもののみ紹介していきます。


ViewController.swift

ViewController.jpg

プロジェクト作成時に自動的に作られるUIViewControllerのサブクラス(とそのファイル)です。Main.storyboard上のViewControllerと紐付いています。

UIViewControllerというのは、画面遷移やViewの更新などを一挙に受け持つ役割のクラスです。多くの場合はこのUIViewControllerがアプリの一画面につき一つ必要なので、最初はUIViewController = 画面と考えるとわかりやすいです。


Main.storyboard

Storyboard.jpg

StoryboardというのはUIViewController同士の画面遷移や、Viewのレイアウトを作っていく役割のファイルです。

この後の UIを作る の項でアプリのレイアウトを作る際に触れていくファイルになります。


2. データモデルを作る

今回扱うデータモデルは二つ。TweetとUserのデータモデルです。Tweetの中でUserを扱うので、Userから先に作っていきます。


2-1. 新しいファイルを作る

Command + n のショートカットを使うと新しくファイルを作成することができます。

以下の画像のように、iOSタブの Swift File を選択して Next を押します。

ファイルの新規作成.jpg

Save as にファイル名を User と指定して Create を押します。

保存.jpg

すると、作成した User.swift ファイルが追加されたのが確認されます。

ここにUserのデータモデルを定義していきましょう。

ファイルの作成完了.jpg


2-2. Userのデータモデルを作る

// User.swift

import Foundation

struct User {

// ユーザーのid
let id: String

// (@)ktanaka117
let screenName: String

// ダンボー田中
let name: String

// プロフィール画像URL
let profileImageURL: String

}

structはletで宣言したプロパティのみの場合はinitを書かなくてもオーケーです。自動でinitが作られて、呼び出し元で補完してくれます。

スクリーンショット 2017-02-18 11.39.14.png


2-3. Tweetのデータモデルを作る

User同様、Tweetのデータモデルも作りましょう。

// Tweet.swift

import Foundation

struct Tweet {

// Tweetのid
let id: String

// Tweetの本文
let text: String

// このTweetの主
let user: User

}


3. UIを作る

さてお待ちかね、アプリのUIを作っていきます。


3-1. TimelineViewController.swiftを作成する

まずメインとなる画面を作成します。

2-1と同じように新規ファイルを追加から、Cocoa Touch Classを選択します。

先に2段目のSubclass ofUIViewControllerと入力します。(UIViewC辺りでサジェストされるはず)

ClassはTimelineViewControllerとしましょう。

Screen Shot 2017-02-19 at 12.04.57.png


3-2. Main.storyboardのUIViewControllerにTimelineViewControllerを指定する

次にMain.storyboard上でViewControllerと指定していたUIViewControllerを、先ほど改名したTimelineViewControllerに指定し直します。

改名2.jpg


3-3. UITableViewを配置する

StoryboardのTimelineViewController上にUITableViewを探してきて、ドラッグ&ドロップで配置します。

tableview配置.jpg

配置後。

tableview配置後.jpg


3-4. UITableViewにAutoLayoutを適用する


AutoLayoutとは

AutoLayoutとその使い方の説明をします。

iPhoneには複数の端末サイズが存在します。その端末サイズ差に対応するための仕組みがAutoLayoutです。AutoLayoutではあるViewが他のViewと比較してどのくらいの位置に配置されるかを設定することができます。

AutoLayoutを設定していくときに設定するViewの相対位置のルールのことを Constraint(制約) といいます。


先ほど配置したUITableViewに制約を追加していきます。

制約を追加するには制約を追加したいViewを選択してStoryboard画面右下の右から二番目のアイコンをクリックします。

addConstraint.jpg

すると制約を追加する画面が表示されるので、ここに選択したViewに対して追加したい設定値を入力していきます。

スクリーンショット 2017-02-18 21.13.10.png

ここは少し詳しく見ていきましょう。

Spacing to nearest neighbor に、上下左右の隣接する他のViewから、選択しているViewがどれくらいのポイント数を離すかを設定します。

今回UITableViewは画面いっぱいに表示したいので、上下左右のマージンを0で設定します。

このとき注意して欲しい点は三つあります。画像を参照してください。


注意点1. 上下左右のマージンの数値は正しいか

加えたい制約の間隔を意味する、入力した数値が意図した数値になっているかどうかを確認してください。もし間違えても後から修正できるので問題はありませが、あとで数値を入力し直すのが面倒だったりするので、制約を追加するときは確認してください。

caution1.jpg


注意点2. 制約が有効になっているか

入力した数値の内側の赤い線が、点線ではなく実践になっているかを確認してください。これが点線の場合は制約が追加されないので確認してください。有効/無効の切り替えは線をクリックすることで行えます。

caution2.jpg


注意点3. Constrain to margins のチェックが外れているか

ここのチェックが入っている場合はチェックを外してください。

このチェックが入っていると、意図しないマージンが取られることがあります。

caution3.jpg

三点が確認できたら、ウィンドウの一番下にある Add 4 Constraints ボタンを押して制約を追加しましょう。

add.jpg

制約が追加できたら、TableViewの周りに黄色い線が表示されるでしょう。

これは追加した制約に対してViewの見た目が反映されていないことを意味しています。

見た目を制約通り反映させるには、Viewを選択してStoryboard画面の右下の一番左のアイコンをクリックして Update Frames します。

UpdateFrames.jpg

TableViewが画面いっぱいにマージン0で広がりました!

ここまででTableViewのAutoLayout適用は終了です。

(以下の画像のように表示がおかしい場合がありますが、これはおそらくXcodeのバグ。ファイルを開き直すと正しい状態に直ります。)

updatedframes.jpg


3-5. TableViewをTimelineViewController.swiftにIBOutletで紐付ける

このTableViewをソースコード上で扱えるようにするためにIBOutletでTableViewとTimelineViewControllerの紐付けを行います。IBOutletというのは、Storyboard上の要素をソースコード上で扱うための機能になります。

紐付けを行うにはStoryboard上でTimelineViewControllerを選択して、Assistant Editorを開きます。

Assistant Editorというのは画面を分割する機能のことで、開き方はXcode右上の6つ並んだアイコンの左から二番目をクリックします。

assistanteditor.jpg

Storyboard上でTimelineViewControllerを選択した状態でAssistant Editorを開くと、TimelineViewController.swiftを開くことができます。

なお、下の画像ではXcode内に要素が多くなってきたので、Utilitiesを非表示にしています。

hideutilities.jpg

IBOutletの紐付けの方法は、Storyboard上のTableViewを選択して、その上で control + ドラッグ して線を引きます。引いた線はソースコード上のクラス内でドロップします。ドロップする場所は class TimelineViewController: UIViewController { のすぐ下の行がよいでしょう。

スクリーンショット 2017-02-18 22.38.12.png

ドロップするとポップアップが表示されます。

ここではTableViewをどのようにソースコード上に紐付けるかの設定を行います。以下の画像の通りに入力して、 Connect ボタンを押しましょう。

outletoption.jpg

Storyboardと紐づいたことを意味する黒丸と、tableViewの宣言が追加されれば完了です。

didIBOutlet.jpg

Storyboard上のtableViewの設定はここまで!


3-6. UITableViewCellを配置する

UITableViewCellを作っていく前に、Standard EditorとUtilitiesを表示してXcodeの表示を元に戻しましょう。

backtoorigin.jpg

Xcodeが元の表示に戻ったら、UITableViewの時と同様にUITableViewCellを検索して、それをUITableView上に配置します。

tableviewcell配置.jpg

配置後。

tableviewcell配置後.jpg


3-7. UITableViewCellにAutolayoutを適用する

今回実装していくレイアウトは以下のようなものです。

Cell.jpg

要素を分解すると以下の四つの要素が含まれています。


  1. アイコンを表示するためのUIImageView

  2. 名前(ここでは「ダンボー田中フレンズ」)を表示するためのUILabel

  3. スクリーンネーム(ここでは「@ktanaka117」)を表示するためのUILabel

  4. ツイート本文を表示するためのUILabel

それぞれのレイアウトは画像の通りですが、文字に起こすと以下の通りです。


  1. 上と左にそれぞれ8ptずつの余白を持ち、サイズが50 * 50ptとなっています。

  2. 上に0ptと右に8ptの余白、そしてアイコンのUIImageViewに対して8ptの余白を持ちます。

  3. 名前のUILabelに対して8pt、 アイコンのUIImageViewに対して8pt、右に8ptの余白を持ちます。

  4. 上下と右にそれぞれ8ptの余白、そしてアイコンのUIImageViewに対して8ptの余白を持ちます。

さきほどUITableViewに対して行ったように、制約を追加していきましょう。順番は上の数字の通り、1, 2, 3, 4の順で進めましょう。

すると3つ目のラベルの制約追加を終えたときにエラーが表示されるはずです。

赤丸矢印をクリックしてエラーの内容を見てみましょう。

error.jpg

エラーの詳細が表示されました。

スクリーンショット 2017-02-18 23.39.04.png

エラーを見てみると、 vertical hugging prioritycompression registance priority という二つのキーワードが見受けられます。今回修正するのは3つ目のツイート本文を表示するUILabelの vertical hugging priority (= Content Hugging Priority)

このエラーが起こるのは「セルの高さが未確定だから」です。Twitterの公式クライアントアプリを見ればわかる通り、ツイートのセルの高さは本文の長さによって可変です。それに対し、三つ並んだラベルがある中でどのラベルの高さを優先して高くしたらいいのかがわからないことでこのエラーが起こっています。(= Content Hugging Priorityの曖昧さ)


Content Hugging Priorityとは

Content Hugging Priorityというのは、Viewの「大きくなりにくさ」を指定する数値です。この数値が大きければ大きいほどViewは大きくなりにくくなります。逆に優先して大きくしたい場合は数値を小さくしてあげればOKです。

今回優先して大きくしたいのは3つ目のラベルです。以下の画像に従って3つ目のラベルの水平方向のContent Hugging Priority (= vertical hugging priority) の数値を小さくしましょう。

opensizeinspector.jpg

fixhugging.jpg

Content Hugging Priorityの数値を修正すれば、エラーは無くなるはずです。

配置したUIの各要素にUpdate Framesを行えば、UITableViewCellに対するAutoLayoutの適用は終了です。

スクリーンショット 2017-02-19 0.18.11.png


UILabelを設定する

ついでなのでUILabelにデフォルトの文字列を入力する設定と、ツイート本文のラベルを可変にする設定を行いましょう。

Attributes inspector を表示して設定を行いたいラベルを選択します。

openattributesinspector.jpg

以下の画像の該当箇所にデフォルトに設定したい文字列を入力します。すると、Storyboard上のUILabelにもその変更が反映されます。

setText.jpg

同じ調子で他のUILabelにも文字列を入力していきましょう。

↓な感じ。

スクリーンショット 2017-02-19 0.44.17.png

一定以上の文字数を超えて入力するとツイート本文を入力するラベルが見切れてしまいました。本来ここは "..." となるのではなく高さが変わって欲しいところ。

UILabelの高さを可変にするには Lines の項目を1から0に設定する必要があります。

Lines はそのUILabelが表示する行数を表しています。0に設定することで行数の指定を無くし、高さを可変にすることができます。

lines.jpg

もしかするとこの時、エラーが表示されるかもしれません。

これは表示上のUITableViewCellの高さが足りないことが原因で起こるエラーです。

heighterror.jpg

UITableViewCellの下端をドラッグ&ドロップして表示上の高さを調節してあげれば解決します。

cellheight.jpg

その他フォント、フォントサイズ、フォントカラー、などはカスタマイズ編で紹介します。


3-8. TweetTableViewCell.swiftとそのクラスを作成し、IBOutletで紐付ける

データモデルを作ったときと同様に、Swiftファイルを作成してUITableViewCellを継承したTweetTableViewCellクラスを宣言します。

// TweetTableViewCell.swift

import UIKit

class TweetTableViewCell: UITableViewCell {

}

そして先ほどまで作成していたMain.storyboardのUITableViewCellのclass指定に、今作成したTweetTableViewCellを指定します。

SpecifyTweetTableViewCellClass.jpg

ついでに後ほど描画設定の際に使うことになるのでIdentifierも指定しておきます。

Specifyidentifier.jpg

次にTimelineViewControllerのときと同様に、Assistant Editorを開いてTweetTableViewCellにUI要素をソースファイルに紐付けていきます。

Assistant Editorで開くファイルを指定するには、以下の画像のように Automatic をクリックして、

Automatic > Manual > DotsSwiftTwitterClient > DotsSwiftTwitterClient > TweetTableViewCell.swift

と階層を潜って指定します。

Automatic.jpg


スクリーンショット 2017-02-19 1.28.26.png

Assistant EditorでTweetTableViewCell.swiftを開けたら、 control + ドラッグ&ドロップ でIBOutletを紐付けていきます。

変数名は以下の画像の通り。

cellOutlet.jpg

// TweetTableViewCell.swift

import UIKit

class TweetTableViewCell: UITableViewCell {

// アイコンを表示するUIImageView
@IBOutlet weak var iconImageView: UIImageView!

// 名前(ダンボー田中フレンズ)を表示するUILabel
@IBOutlet weak var nameLabel: UILabel!

// スクリーンネーム(@ktanaka117)を表示するUILabel
@IBOutlet weak var screenNameLabel: UILabel!

// ツイート本文を表示するUILabel
@IBOutlet weak var textContentLabel: UILabel!

}


3-9. Delegateを設定する


Delegateとは?

Delegateは日本語で「委譲」という意味を持ちます。

Delegateというのはprotocolで実装されていて、そのprotocolに宣言されている処理をどこで実行するかをプログラマが指定することのできる仕組みです。

これをどこで使うかというと、例えば「UITableViewのセルがタップされたのを検知してなにかしたいとき」などです。

UIViewControllerにはもともと、UITableViewがタップされたのを検知する機能はありませんが、Delegateを使って処理の委譲先をUIViewControllerに指定してあげればそれができるようになります。

コードで書くとこんな風に指定します。

class TimelineViewController: UIViewController {

@IBOutlet weak var tableView: UITableView!

override func viewDidLoad() {
super.viewDidLoad()

// delegateの指定を自分自身(self = TimelineViewController)に設定
tableView.delegate = self
}
}

extension TimelineViewController: UITableViewDelegate {

// cellがタップされたのを検知したときに実行する処理
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("セルがタップされたよ!")
}

}


cellの高さを自動計算させる

UITableViewDelegateにはセルタップなどのユーザーイベントを検知する機能の他にも、UITableViewの見た目や挙動に関する機能も含まれています。

見た目の中にはセルの高さの指定も含まれています。今回はツイート本文の量に応じてUILabelの高さを自動的にCellに反映させたいので、 estimatedHeightForRowUITableViewAutomaticDimension の値を設定します。

// TimelineViewController.swift

--- 省略 ---

extension TimelineViewController: UITableViewDelegate {

// cellがタップされたのを検知したときに実行する処理
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("セルがタップされたよ!")
}

// セルの見積もりの高さを指定する処理
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return 200
}

// セルの高さ指定をする処理
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
// UITableViewCellの高さを自動で取得する値
return UITableViewAutomaticDimension
}
}


3-10. TimelineViewControllerにTweetの配列をプロパティとしてもつ

このあと説明するdataSourceを設定するために、UITableViewを表示するためのデータソースとしてTweetの配列を宣言しておきます。

// TimelineViewController.swift

import UIKit

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

// テーブル表示用のデータソース
var tweets: [Tweet] = []

override func viewDidLoad() {
super.viewDidLoad()

tableView.delegate = self
}
}


3-11. dataSourceを設定する


DataSourceとは?

DataSourceもDelegateと仕組みは同じで、protocolで実装されたものになります。DataSourceの役割はViewを描画するのに必要な情報を渡す処理の実装先を決めて、実装することです。例えば「どんなcellを描画するか」、「cellは何個描画するか」、「セクションの数は何個にするか」などです。

コードで書くとこんな感じ。

// TimelineViewController.swift

import UIKit

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

var tweets: [Tweet] = []

override func viewDidLoad() {
super.viewDidLoad()

tableView.delegate = self
// dataSourceの指定を自分自身(self = TimelineViewController)に設定
tableView.dataSource = self
}
}

// --- UITableViewDelegateを省略 ---

extension TimelineViewController: UITableViewDataSource {
// 何個のcellを生成するかを設定する関数
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// tweetsの配列内の要素数分を指定
return tweets.count
}

// 描画するcellを設定する関数
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// TweetTableViewCellを表示したいので、TweetTableViewCellを取得
let cell = tableView.dequeueReusableCell(withIdentifier: "TweetTableViewCell") as! TweetTableViewCell

return cell
}
}


tableView.dequeueReusableCell(withIdentifier:) とは?

Storyboardから指定したIdentifierのセルを取得する処理です。先ほどTweetTableViewCellをStoryboardで作成した際に指定したIdentifierの文字列がこの関数の引数に渡す文字列と一致している必要があります。

そしてこの関数の戻り値はUITableViewCellなので、 TweetTableViewCellとして扱うために as! TweetTableViewCell としてキャストしています。

そしてUITableViewで利用されているqueueという仕組みについても軽く説明します。

例えばUITableViewに1000件のデータソースがあったとして、その件数に応じてセルを生成するとしたらメモリをとても圧迫してしまいます。それを解決するために使われているのがUITableViewCellの描画におけるqueueです。

1画面に描画できるセルの数は、画面に表示できるだけしかありません。UITableViewのスクロールに合わせて、消えていくセルとこれから表示されるセルがありますが、生成されたセルはそのままに、セルに表示するための中身のデータだけを入れ替えて描画しているというのがこのqueueの仕組みになります。

[画像]


4. ダミーデータをUIに反映させる


4-1. TweetTableViewCellのUIにTweetの値をセットする関数を作る

TweetTableViewCellに、渡されたTweetの値をUIに反映させる func fill(tweet: Tweet) という関数を定義します。

let downloadTask = URLSession.shared.dataTask(with: URL(string: tweet.user.profileImageURL)!) は渡されたURLをもとに通信を行います。

DispatchQueue.main.async[weak self] などについてはとりあえず今は「おまじない」として覚えておいてください。また後ほど登場するのでその時紹介します。

// TweetTableViewCell.swift

import UIKit

class TweetTableViewCell: UITableViewCell {
@IBOutlet weak var iconImageView: UIImageView!
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var screenNameLabel: UILabel!
@IBOutlet weak var textContentLabel: UILabel!

func fill(tweet: Tweet) {
// profileImageURLから画像をダウンロードしてくる処理
let downloadTask = URLSession.shared.dataTask(with: URL(string: tweet.user.profileImageURL)!) { [weak self] data, response, error in
if let error = error {
print(error)
return
}

DispatchQueue.main.async {
// iconImageViewにダウンロードしてきた画像を代入する処理
self?.iconImageView.image = UIImage(data: data!)
}
}
downloadTask.resume()

// tweetから値を取り出して、UIにセットする
nameLabel.text = tweet.user.name
textContentLabel.text = tweet.text
// screenNameには "@" が含まれていないので、頭に "@" を入れてあげる必要がある
screenNameLabel.text = "@" + tweet.user.screenName
}
}


4-2. ダミーデータを作成してTableViewのデータソースにする

ダミーデータの内容はなんでもOKです!

self.tweet に配列に格納したダミーデータを代入したら、 tableView.reloadData() を呼ぶことでtableViewを再度描画(更新)させることができます。

indexPath というのはそのセルの行を表した型になります。 indexPath.row からはInt型の行番号を取り出すことができるので、配列の中からindexPath.rowで指定したindexのTweetを取得することができます。

// TimelineViewController.swift

import UIKit

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

var tweets: [Tweet] = []

override func viewDidLoad() {
super.viewDidLoad()

// ダミーデータの生成
let user = User(id: "1", screenName: "ktanaka117", name: "ダンボー田中", profileImageURL: "https://pbs.twimg.com/profile_images/832034247414206464/PCKoQRPD.jpg")
let tweet = Tweet(id: "01", text: "Twitterクライアント作成なう", user: user)

let tweets = [tweet]
self.tweets = tweets

// tableViewのリロード
tableView.reloadData()
}
}

extension TimelineViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("セルがタップされたよ!")
}

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return 200
}

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
}

extension TimelineViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tweets.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TweetTableViewCell") as! TweetTableViewCell

// TweetTableViewCellの描画内容となるtweetを渡す
cell.fill(tweet: tweets[indexPath.row])

return cell
}
}


4-3. 実行して確認する

Command + R のショートカットでプロジェクトを実行(Run)することができます。

スクリーンショット 2017-02-16 18.10.22.png


前編の終わり

ここで前編は終わりです。休憩しましょう。

次は後編。

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