Swift初心者が平日の空き時間を使ってQiita Advent Calendar閲覧アプリを作ってみた話


## はじめに

やるやるといって本格的にまだ勉強してないし，Swift ではまだちゃんとしたアプリ作ってないなーと感じていた。スマートデバイス・テクノロジーも今回初めて参加したし，12/25 の枠取ったし記念にアプリ作ってみるかというのが妄想の始まり。

今の現場で週一で 2 時間Swift使ったアプリを作ろうという試みが始まって少し経つ。作業に入ったと思ったらもう時間というかなり濃い時間になっている。こんな感じでだらだらやらないで平日の空き時間に集中して作ってみようということで平日ひとりもくもく会がはじまった。


## サイクル(時間の使い方)

下記の大きく 3 種類に分けられる。

* 通勤時間(2.0h)
* 現場での昼休み(0.5~0.75h)
* 帰宅後のリラックスタイム(2.0~4.0h)


|時間|平常時|残業時|
|:--:|:--:|:--:|:--:|
|通勤時間|情報収集，妄想|妄想|
|昼休み|仮実装，アウトプット|アウトプット|
|帰宅後|実装，アウトプット|妄想|

　朝通勤時に帰宅後の作業で出た問題の調査。ないときは妄想。
　昼休みにテキストに興して，あるいは仮実装して帰宅後の私に渡す。
　帰宅後に適用(実装)。問題点が出るor次の実装の調査が必要になる。
のサイクル。

残業があった日，疲労があった日は帰宅後作業時間が取れなかった。

## 要件定義

下記を満たすこと。

* Advent Calendar の記事がみれること
* 期間は 2 週間くらい
* Swift で作ること
* 縛られてるコーディング規約がないから記法は自由

**※ 一番簡単なのは WebView 貼ること・・・(全く面白みがない)**

## 妄想後の要件定義(増えた分のみ)

* カレンダーは CollectionView で
* 記事データは Alamofire を使って取得
* HUD は BXProgressHUD を用いる
* より汎用性を意識し他の企業，カテゴリのものも閲覧可能に
* カレンダーも月ごとに適切な曜日配置に
* セルタップで記事タイトル，リンク，著者を表示
* 記事表示はWebViewで妥協
* エラー処理は出来るだけ取り組む

主に通信のとこ一度書いておきたかった。複雑じゃなさすぎるけど最初だし・・・


## 今回のコード

今回のコードは Github に上げました。
Github：[リンク](https://github.com/MilanistaDev/SDT_AdventCalendar2015_day25)

なお，今回は CocoaPods も用いているので git clone だけじゃ動きません。pod install して CalendarTest.xcworkspace を開いてください。

```bash:ターミナル
$ cd Downloads/
$ git clone git@github.com:MilanistaDev/SDT_AdventCalendar2015_day25.git

$ cd SDT_AdventCalendar2015_day25/
$ pod install
```

いろいろ勉強したいので間違い，もっとこうした方がいいなどご指摘お願いいたします。デザインは時間なかったしそこまで凝って作ったつもりないので勘弁してください。ファイルに書きすぎかもしれないです。

画面は3つでこんな感じです。
<a href="http://2.bp.blogspot.com/-BZayjfxd0Dk/Vn2CO6jkfVI/AAAAAAAADSs/c_4RVFJaj10/s1600/%25E5%2590%258D%25E7%25A7%25B0%25E6%259C%25AA%25E8%25A8%25AD%25E5%25AE%259A-1.gif" imageanchor="1" ><img border="0" src="http://2.bp.blogspot.com/-BZayjfxd0Dk/Vn2CO6jkfVI/AAAAAAAADSs/c_4RVFJaj10/s1600/%25E5%2590%258D%25E7%25A7%25B0%25E6%259C%25AA%25E8%25A8%25AD%25E5%25AE%259A-1.gif" /></a>

## Advent Calendar のデータ取得

Qiita API を当初は使う予定だったが，問い合わせたところ Advent Calendar 用の API は用意されてないとの返信が来た。

早速ツミかと思ったけど Feed があるじゃないかということで Feed を JSON 形式で返してくれる Google Feed API を用いることにした。実際のリンクがこんな感じ。

https://ajax.googleapis.com/ajax/services/feed/load?q=http://qiita.com/advent-calendar/2015/sdt/feed&num=25&hl=ja&v=1.0

25 個以上フィードが返ってくることはないので固定にしています。

実際に叩いてみるとこういうレスポンス(内容は一部省略)。

```json:JSON
{
    "responseData": {
        "feed": {
            "author": "", 
            "description": "", 
            "entries": [
                {
                    "author": "", 
                    "categories": [], 
                    "content": "", 
                    "contentSnippet": "", 
                    "link": "", 
                    "publishedDate": "", 
                    "title": ""
                },
                ...(記事がある分だけ繰り返し)
                }
            ], 
            "feedUrl": "http://qiita.com/advent-calendar/2015/sdt/feed", 
            "link": "http://qiita.com", 
            "title": "スマートデバイス・テクノロジー Advent Calendarの投稿 - Qiita", 
            "type": "atom10"
        }
    }, 
    "responseDetails": null, 
    "responseStatus": 200
}
```
entries の中に各投稿者の記事内容が並ぶ感じです。実際に叩いてみればわかりますが，本文や記事の断片(contentSnippet)もあるので結構長いレスポンスになります。

使う予定の値としては下記のとおりです。

| 使用予定値 | 該当パラメータ |
|:--|:--|
|カレンダーのタイトル|"feed" の "title"|
|著者名|"feed" の "entries" の "author"|
|記事タイトル|"feed" の "entries" の "title"|
|記事リンク|"feed" の "entries" の "link"|
|何日目に書いたという情報|な，ない・・・|

見ての通り最後の Day3 みたいな情報は取得できていない。
これと Array の配列番号，collectionView の indexPath.row を対応させるつもりだったのだがこれは妥協点。参加登録されていない，キャンセルになった，参加予定で失踪した人がいた場合，配列が詰められて日付と対応できない。日付と配列の何番目が対応できる，いい考えをお持ちの方がいればご指摘お願いいたします。

## 通信周り(Alamofire)

Alamofire でのデータ取得自体はとてもシンプルでした。Top 画面から送ったカテゴリ名で Advent Calendar が存在しない場合は Google Feed API 側で返してくれるのでそれを利用。JSON の解析が頭では難しいなら 1 個ずつ要素を取り出してやればいい感じ。

```Swift:Swift
// MARK:- Provate method
    // MARK: Network method

    /**
     * 通信してfeedのデータ(JSON)をArrayに入れる
     * 
     * @param keyword Top画面で入力したカテゴリ名
     */
    func getFeedJSONData(keyword:String) {
    
        // Top画面で入力したカテゴリ名を元にfeedを取得
        // feed を JSON で取得するために Google API を使用
        let feedURL = kAdventCalendarURLPrefix + keyword + kAdventCalendarURLSuffix
        
        // 通信処理
        Alamofire.request(.GET,feedURL).responseJSON { response in
            switch response.result {
            case .Success(let value):
                if let jsonDic = value as? NSDictionary {
                    
                    let statusCode = jsonDic["responseStatus"] as! NSInteger
                    if (statusCode == 400) {
                        self.HUD!.hide()
                        let weakSelf = self
                        weakSelf .showAlertAtReturnTop("Advent Calendar が存在しないようです")
                        return
                    }
                    if (statusCode != 200 && statusCode != 400) {
                        self.HUD!.hide()
                        let weakSelf = self
                        weakSelf .showAlertAtReturnTop("エラーです")
                        return
                    }
                    // Feed の JSON データが入る
                    let responseData = jsonDic["responseData"] as! NSDictionary
                    
                    // Advent Calendar の情報が入る
                    self.feedDic = responseData["feed"] as! NSDictionary
                    
                    // みんなのQiitaの記事の情報が入る
                    self.articleDataArray = self.feedDic["entries"] as! NSArray
                    
                    // 最新の記事から入るのでわかりやすいように古い方を最初に入れ替える
                    self.articleDataArray = self.articleDataArray.reverseObjectEnumerator().allObjects
                    
                    self.subTitleLabel.text = self.feedDic["title"] as? String
                    // インジケータ隠す
                    self.HUD!.hide()
                    BXProgressHUD.Builder(forView: self.view).mode(.Checkmark).text("Completed!!").show().hide(afterDelay: 2)
                }
            case .Failure(let error):
                // TODO:通信失敗時のエラーハンドリング
                print(error)
                self.HUD!.hide()
                let weakSelf = self
                weakSelf .showAlertAtReturnTop("ネットワークエラーのようです")
            }
        }
    }
```

## Top 画面について
![top.png](https://qiita-image-store.s3.amazonaws.com/0/88266/95add7e1-b2f5-ef99-1c49-66ac8a6bd39c.png "top.png")

特にない。入力された文字列を次のカレンダー画面に渡している。一応から文字判定だけはしているが，スペースなどいれるとアプリが落ちる。落ちないように絵文字判定，記号判定など Validate をしっかりしないといけない。この辺のエラー処理しっかりやっておくと今後も使い回しが効くと思うのでこれは後でやります。

## カレンダー部分作成について
![calendar.png](https://qiita-image-store.s3.amazonaws.com/0/88266/e1540b9c-2207-4ccc-bf5f-e2bfe7bc2a45.png "calendar.png")

CollectionView の基本的な実装になっているので詳細は書きませんが，セルの大きさは端末の画面サイズで計算しています。詳しくは下記の記事を参照してください。

* UICollectionViewCell の大きさについて
  - http://qiita.com/MilanistaDev/items/fbf5fb890d9a3a7180cd

AutoLayout でカレンダー : 下の UIView = 7:3 になっています。
通信時にオフライン，該当 Advent Calendar なしのエラー起こした場合は Top 画面に戻す処理をアラートのボタン押下処理に仕込んであります。

### カレンダーの日付セルについて

正直汎用性を持たせようとして無理くりです。時間あるときに見直そうと思います。ただ今回の場合は12月のみなので12/26-31もなくすべきだったかもしれない。12/1 が始まるのは何曜日か(weekDay)を取得して最初のセルに値を与えないようにしました。このあたりはこの記事をご覧ください。

* Swift カレンダー，日付系備忘録
  - http://qiita.com/MilanistaDev/items/0422e72f9bfabc773848


セル押下で対応する記事データを下のデータ View に表示させ，NSDictionary にリンク，タイトル，著者名を格納します。記事がないところはエラー処理入れました。日付に対応してないので12/1から詰められて残りはエラー処理が走ります。本当にもやもやする。

```Swift:Swift
/// セル選択時の遷移処理
    func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
    
        // indexPath.row を操作(カレンダー次第で変わる)
        let index : NSInteger = indexPath.row-weekDay+1
        
        // 記事がないところはエラーアラート表示
        if (index < 0 || index > self.articleDataArray.count-1) {
            self .showNormalAlert("記事がありません")
            return
        }
        
        // 該当記事データを抜き取る
        let authorName = (self.articleDataArray[index])["author"] as! String
        let articleTitleName = (self.articleDataArray[index])["title"] as! String
        let accessLinkURL = (self.articleDataArray[index])["link"] as! String

        // 次の画面に渡す用のデータDictionaryにデータを入れる
        self.selectedArticleDataDic = [kAuthorName : authorName,
                                       kArticleTitleName : articleTitleName,
                                       kAccessLinkURL : accessLinkURL]
        // 表示部分に表示
        self.authorNameLabel.text = self.selectedArticleDataDic[kAuthorName] as? String
        let viewWidth = UIScreen.mainScreen().bounds.width * 0.9
        let articleTitleText = self.selectedArticleDataDic[kArticleTitleName] as? String
        let textSize = articleTitleText!.getTextSize(UIFont.systemFontOfSize(17),viewWidth: viewWidth, padding: 8)
        self.articleTitleLabel.text = articleTitleText
        self.heightConstraints.constant = textSize.height
        self.viewArticleButton.enabled = true
    }
```


記事を見るボタンを押下すると次の画面にリンク，タイトル，著者名を格納したNSDictionaryを次の画面の property に渡して遷移します。

このボタンは @IBDesignable を用いています。詳しくは下記の記事をご覧ください。

* @IBDesignable使ってXcodeのAttributes Inspectorで設定を変更，リアルタイムでデザインを確認できるUIButtonを作る
  - http://qiita.com/MilanistaDev/items/ba85a0bc557681b8041e


なお，下のデータ View の記事タイトルの部分は高さ可変にできるようにしました。下記記事がとても参考になりました。ほぼそのまま使わさせていただきました。

* UILabelの高さを文字数によって動的に変更する
  - http://qiita.com/ekushida/items/e574b30fee811c297ea9


## BXProgressHUD

* [BXProgressHUD](https://github.com/banxi1988/BXProgressHUD)

Objective-C だと SVProgressHUD とかよく使うんだけど，気分転換に使ってみた。最初はもっと楽に書けないのかなぁーと思ったけど中をたどるといろいろ書いてあった。勉強になった。

```Swift:BXProgressHUD_使い方例
BXProgressHUD.Builder(forView:self.view).mode(.Checkmark).text("Completed!!").show().hide(afterDelay: 2)
```

テキストを伴って現れて・・・2秒後に隠れるのかータイミングじゃなくて好きな時に出したいんだがで少し時間食った。

```Swift:BXProgressHUD_使い方例2
var HUD:BXProgressHUD? // property
self.HUD =  BXProgressHUD.Builder(forView:self.view).text("Loading") .create()
self.HUD!.show() // こういうのが欲しかった
self.HUD!.hide() // こういうのが欲しかった
```
なんでこういうのをすぐ思いつけなかったのか。


## 記事閲覧画面
![viewer.png](https://qiita-image-store.s3.amazonaws.com/0/88266/e8d44e4b-a465-795a-a4ea-bd3ea5cdef7b.png "viewer.png")

受け取ったリンクを NSURL に変換してリクエスト投げているだけです。コンテンツも Feed で取得できますが，画像もありますしレイアウトを崩してまで表示させるまでもないかなということで WebView に任せました。一応これだけだと寂しいので Tweet をできるようにしてみました。このためにリンクだけ送るのではなく NSDictionary で送った感じです。結構楽に実装できるんだなと改めて感じた。

```Swift:twitter
import Social

// Tweet 機能
func tweetButtonTapped() {

    myComposeView = SLComposeViewController(forServiceType: SLServiceTypeTwitter)
        
    // 投稿テキスト
    let tweetContentsText = String(hoge)
        
     // myComposeViewの画面遷移.
    self.presentViewController(myComposeView, animated: true, completion: nil)
}

```


## 問題点

* 参加登録した日に対応しない時がある
  - 参加登録がない、参加者が失踪してカレンダーが埋まらない場合詰められる
  - これは取得する記事データに日付情報がないため
  - 投稿時間は取得できるが早めに投稿されていたりするので必ずしも投稿順になるかわからない
  - ソートに使うには不向き。

---
* 記事閲覧画面のオフライン処理
  - Reachability とかで
  - Reachability ってSwiftにもあるのかな？

---
* エラー処理の漏れ
  - textFeild 系
  - CollectionView 系(セルも)

---
* デザイン，機能性
  - 一覧ではなくセルを押さないと内容がわからない
  - 地味

## おわりに

今回はSwift初心者の私が平日の空き時間を使って作った Qiita Advent Calendar 記事閲覧アプリの紹介と時間の使い方について書いた。見ての通り複雑な処理はほとんどないしこれくらいのアプリなら楽しくあまり詰まることもなく作ることができるのがわかりました。レベル的にはスマートデバイス・テクノロジーの個人研修くらいかなーって感じ。空き時間をどう使うかは大事だなーと改めて実感した次第であります。Swift とも少しお友達になれた気がします。文法は少しずつやりつつ実際に実装目標を決めて積極的にお付き合いしていきたいと思います。

ここまでご覧いただきありがとうございます。実際に git clone してアプリを実機で見てみて私ならこうすべきだとか，ここはよかったとかありましたらコメントにてお願いいたします。今後改修する予定はないですが，来年の Advent Calendar でもう一回違うコンセプトで作ってみると面白いかもなとか思ったりしました。Qiita API に Advent Calendar API が来年は入ってるといいなー(一応 Increments さんのエンジニアには伝えたとのこと)



## 参考
* Google Feed APIを使ってみたよ(JSON)
  - http://chuross.hateblo.jp/entry/2012/12/20/024240

* UICollectionViewCell の大きさについて
  - http://qiita.com/MilanistaDev/items/fbf5fb890d9a3a7180cd

* Swift カレンダー，日付系備忘録
  - http://qiita.com/MilanistaDev/items/0422e72f9bfabc773848

* @IBDesignable使ってXcodeのAttributes Inspectorで設定を変更，リアルタイムでデザインを確認できるUIButtonを作る
  - http://qiita.com/MilanistaDev/items/ba85a0bc557681b8041e

* UILabelの高さを文字数によって動的に変更する
  - http://qiita.com/ekushida/items/e574b30fee811c297ea9

* BXProgressHUD
  - https://github.com/banxi1988/BXProgressHUD

* Alamofire
  - https://github.com/Alamofire/Alamofire