Goalist Developers Blog

寿司とチャットするアプリを作ってみた

こんちは。渡部です。
エンジニアの勉強会に行くとよく寿司好きの人に出会います。

f:id:watabe1028:20170531150310p:plain

なので?今回は寿司とチャットするアプリをささっと作ってみます。
ただしローカルでのみ動くものなので固定文言でやります。

チャット画面を1から作るのは大変なので
JSQMessagesViewControllerというライブラリを使って作ります。

こんな感じでやっていきます
・JSQMessagesViewControllerってなに?うまいの?
・どうやって使うのよ?
・JSQMessagesViewControllerをプロジェクトに追加する
・適当にチャット文言を用意する
・ガリガリ書く寿司だけに

れっつらゴー!

JSQMessagesViewControllerってなに?うまいの?

JSQMessagesViewController

github.com

簡単にいうとチャット画面を提供してくれるライブラリです。
なので実装は文言やアイコンなどの設定をすれば
最低限の機能が簡単に使えます。

どうやって使うのよ?

GitHubで落としてゴー!

JSQMessagesViewControllerをプロジェクトに追加する

CocoaPodsで落とします。
使い方がわからない人はこちら

プロジェクトを作ったらPodsでJSQMessagesViewControllerをinstallします。
サンプルとして「ChatTestApp」を作ります。

作ったらターミナルでアプリ直下まで行き、Podsを作成します。

pod init

作成したらJSQMessagesViewControllerを落とすため編集モードで追記し、installします。
編集モードにして、

vi Podfile

これを追記する。

  # Pods for TestApp
  pod ‘JSQMessagesViewController’
end

「:wq」で保存、終了。

インストール!

pod install

これで下準備おけ!

適当にチャット文言を用意する

まんまです。
文言をこんな感じに用意します。

var messages: [JSQMessage] = [
        JSQMessage(senderId: "sushi", displayName: "B", text: "らっしゃい♪"),
        JSQMessage(senderId: "Dummy",  displayName: "A", text: "大将!つぶ貝!"),
        JSQMessage(senderId: "sushi", displayName: "B", text: "ないよ!"),
        JSQMessage(senderId: "Dummy",  displayName: "A", text: "じゃサーモン!"),
        JSQMessage(senderId: "sushi", displayName: "B", text: "ないよ♪"),
        JSQMessage(senderId: "Dummy",  displayName: "A", text: "いか!"),
        JSQMessage(senderId: "sushi", displayName: "B", text: "ないよ♪"),
        JSQMessage(senderId: "Dummy",  displayName: "A", text: "じゃ帰る!"),
        JSQMessage(senderId: "sushi", displayName: "B", text: "ちょwww待てってwww\nマグロあるからwww")
    ]

senderIdの"sushi"が寿司アイコン(相手)、"Dummy"が自分です。
displayNameは表示名ですが、シンプルな方が好きなので
表示させないようにあとで設定します。私情です。
textはチャットで表示される文言です。

ガリガリ書く寿司だけに

コピペでできるように最低限のコードのみ記載します。
説明はコメントを見てもらえばわかるかと。

import JSQMessagesViewController // インポートする

class ViewController: JSQMessagesViewController { // ViewControllerからJSQMessagesViewControllerに変更する
    
    var messages: [JSQMessage] = [
        JSQMessage(senderId: "sushi", displayName: "B", text: "らっしゃい♪"),
        JSQMessage(senderId: "Dummy",  displayName: "A", text: "大将!つぶ貝!"),
        JSQMessage(senderId: "sushi", displayName: "B", text: "ないよ!"),
        JSQMessage(senderId: "Dummy",  displayName: "A", text: "じゃサーモン!"),
        JSQMessage(senderId: "sushi", displayName: "B", text: "ないよ♪"),
        JSQMessage(senderId: "Dummy",  displayName: "A", text: "いか!"),
        JSQMessage(senderId: "sushi", displayName: "B", text: "ないよ♪"),
        JSQMessage(senderId: "Dummy",  displayName: "A", text: "じゃ帰る!"),
        JSQMessage(senderId: "sushi", displayName: "B", text: "ちょwww待てってwww\nマグロあるからwww")
    ]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 適当につける
        senderDisplayName = "A"
        senderId = "Dummy"
    }

     //アイテムごとに参照するメッセージデータを返す
    override func collectionView(_ collectionView: JSQMessagesCollectionView!, messageDataForItemAt indexPath: IndexPath!) -> JSQMessageData! {
        return messages[indexPath.row]
    }


     //アイテムごとのMessageBubble(背景)を返す
    override func collectionView(_ collectionView: JSQMessagesCollectionView!, messageBubbleImageDataForItemAt indexPath: IndexPath!) -> JSQMessageBubbleImageDataSource! {
        if messages[indexPath.row].senderId == senderId {
            return JSQMessagesBubbleImageFactory().outgoingMessagesBubbleImage(
                with: UIColor(red: 112/255, green: 192/255, blue:  75/255, alpha: 1))
        } else {
            return JSQMessagesBubbleImageFactory().incomingMessagesBubbleImage(
                with: UIColor(red: 229/255, green: 229/255, blue: 229/255, alpha: 1))
        }
    }


     // cell for item
    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        
        let cell = super.collectionView(collectionView, cellForItemAt: indexPath) as! JSQMessagesCollectionViewCell
        
        if messages[indexPath.row].senderId == senderId {
            cell.textView?.textColor = UIColor.white
        } else {
            cell.textView?.textColor = UIColor.darkGray
        }
        return cell
    }
    
    
    // section
    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return messages.count
    }


     // image data for item
    override func collectionView(_ collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAt indexPath: IndexPath!) -> JSQMessageAvatarImageDataSource! {
        
        //senderId == 自分 だった場合表示しない
        let senderId = messages[indexPath.row].senderId
        
        if senderId == "Dummy" {
            return nil
        }
        return JSQMessagesAvatarImage.avatar(with: UIImage(named: "sushi"))
    }


     //時刻表示のための高さ調整
    override func collectionView(_ collectionView: JSQMessagesCollectionView!, attributedTextForCellTopLabelAt indexPath: IndexPath!) -> NSAttributedString! {
        
        let message = messages[indexPath.item]
        if indexPath.item == 0 {
            return JSQMessagesTimestampFormatter.shared().attributedTimestamp(for: message.date)
        }
        if indexPath.item - 1 > 0 {
            let previousMessage = messages[indexPath.item - 1]
            if message.date.timeIntervalSince(previousMessage.date) / 60 > 1 {
                return JSQMessagesTimestampFormatter.shared().attributedTimestamp(for: message.date)
            }
        }
        return nil
    }


     // 送信時刻を出すために高さを調整する
    override func collectionView(_ collectionView: JSQMessagesCollectionView!, layout collectionViewLayout: JSQMessagesCollectionViewFlowLayout!, heightForCellTopLabelAt indexPath: IndexPath!) -> CGFloat {
        
        if indexPath.item == 0 {
            return kJSQMessagesCollectionViewCellLabelHeightDefault
        }
        if indexPath.item - 1 > 0 {
            let previousMessage = messages[indexPath.item - 1]
            let message = messages[indexPath.item]
            if message.date .timeIntervalSince(previousMessage.date) / 60 > 1 {
                return kJSQMessagesCollectionViewCellLabelHeightDefault
            }
        }
        return 0.0
    }

    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

画面読み込みですぐにチャットが表示されているはずです。

次にチャットを送受信させて見ます。

     //Sendボタンが押された時に呼ばれる
    override func didPressSend(_ button: UIButton!, withMessageText text: String!, senderId: String!, senderDisplayName: String!, date: Date!) {
        
        //キーボードを閉じる
        self.view.endEditing(true)
        
        //メッセージを追加
        let message = JSQMessage(senderId: senderId, displayName: senderDisplayName, text: text)
        self.messages.append(message!)
        
        //送信を反映
        self.finishReceivingMessage(animated: true)
        
        //textFieldをクリアする
        self.inputToolbar.contentView.textView.text = ""
        
        //テスト返信を呼ぶ
        testRecvMessage()
    }

     //テスト用「マグロならあるよ!」を返す
    func testRecvMessage() {
        
        let message = JSQMessage(senderId: "sushi", displayName: "B", text: "マグロならあるよ!")
        self.messages.append(message!)
        self.finishReceivingMessage(animated: true)
    }

こんな感じになります。
「うに!」と入力し、Sendボタンを押下で、、、

f:id:watabe1028:20170531175132p:plain

こうなる!

f:id:watabe1028:20170531175207p:plain

f:id:watabe1028:20170531175525p:plain

まとめ

世に出てるチャットアプリの大半はJSQMessagesViewControllerを使っているんではないかなーと予想しています。
これからも楽できる便利なライブラリの紹介ができたらなと思います。

Angular CLIをアップデートしたメモ〜angular-cliから@angular/cliへ

こんにちは。イイオです。
この3月末からAngular CLIを使ってAngular2を触り始めた初心者です。

最近では新卒の技術研修の一環でAngularの環境構築サポートを担当しました。

自分で調べながらやってみてね〜と言ったもののあんまり説明不足でしたから
「angular cli 環境構築」でぐぐって上の方に出てくるブログのコマンドをそのまま使って
古い方のangular-cliをインストールしてプロジェクト作成してしまう事態が起きました。

数ヶ月前の記事がもう使えないとか諸行無常の世界だ。

正式リリースの@angular/cliの方を使いたかったのでアップデートして
プロジェクトのパッケージを変更しました。以下はその際の説明メモです。
公式に書いてある以上のことはしてないです。

github.com

stories 1.0 update · angular/angular-cli Wiki · GitHub

Angular CLIをアップデートする

手順

  1. グローバルの古いangular-cliをアンインストールする
  2. グローバルで@angular/cliをインストールする
  3. ローカルの古いangular-cliを削除し、利用パッケージから外す
  4. ローカルのnode_modules(ローカルに落としたパッケージが入ってるフォルダ)を削除する
  5. ローカルで@angular/cliを利用パッケージに登録する
  6. ローカルの利用パッケージを新しく入れ直す

npm?ローカルとかグローバル?な方はこの辺りを見たら良い気がする

勉強メモ/npmの使い方(node.js=v0.11.16, npm=2.3.0, 2015-03時点) - Qiita

1. グローバルの古いangular-cliをアンインストールする

$ npm uninstall -g angular-cli

2. グローバルで@angular/cliをインストールする

$ npm cache clean
$ npm install -g @angular/cli@latest

3. ローカルの古いangular-cliを削除し、利用パッケージから外す

cd <プロジェクトフォルダ>
npm uninstall --save-dev angular-cli

4. ローカルのnode_modules(ローカルに落としたパッケージが入ってるフォルダ)を削除する

macなら

rm -rf node_modules dist

winなら

rmdir /S/Q node_modules dist

5. ローカルで@angular/cliを利用パッケージに登録する

npm install --save-dev @angular/cli@latest

6. ローカルの利用パッケージを新しく入れ直す

npm install

ng -vでバージョンを確認してみたら

$ ng -v
    _                      _                 ____ _     ___
   / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
  / △ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |    | |
 / ___ \| | | | (_| | |_| | | (_| | |      | |___| |___ | |
/_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
               |___/
@angular/cli: 1.0.0
node: 6.10.2
os: darwin x64
@angular/common: 2.4.10
@angular/compiler: 2.4.10
@angular/core: 2.4.10
@angular/forms: 2.4.10
@angular/http: 2.4.10
@angular/platform-browser: 2.4.10
@angular/platform-browser-dynamic: 2.4.10
@angular/router: 3.4.10
@angular/cli: 1.0.0
@angular/compiler-cli: 2.4.10

無事にアップデートできました。めでてえ〜
何も中身を作り込んでないのであまり問題もなく終わりました。

新卒なのにいきなりAngularをやるみなさんはがんばってくれ!

Angular4でangular-google-mapsを使う

こんにちわ、 ゴーリスト開発のモリツグです。
Angular2もAngular4になってしばらく経ちますが、皆さん元気でしょうか。
RC5のNgModule が導入された時のような事態にはならず安心しました。

今回はAngular4の環境でangular-google-mapsの使い方を紹介したいとおもいます。
私が担当する回では必ずFlexを懐かしんできましたが、今回は残念ながらFlexを懐かしめるのはChangeDetectorRef.detectChanges()がvalidateNow()に似ている点ぐらいでした。

開発環境
・angular 4.1.2
・agm 1.0.0-beta.0
・typescript 2.2.0

インストール

詳しくはこちらになります。
とりあえずインストールします。 古い方のangular2-google-mapsを入れないように注意です。

npm install @agm/core --save

新旧バージョンのselectorの比較などはCHANGELOG.mdにあります

使ってみる

plunkerのDEMOはこちら。
場所を入力⇒表示された候補をクリック⇒地図の表示変更 という感じで動きます。
f:id:t-moritsugu:20170518123817p:plain
GoogleのAPIキー無しでやっているので、いつエラーになるのかは不明です。
ただちゃんと動くのでローカルにコピれば大丈夫です。(agm公式のデモもplunkerでキー無しです)
ローカル環境ではangular-cliを使って作成しましたが、plunkerではsystemjsを利用しています。
app/app.module.ts の以下の部分にAPI_KEYを入れればローカルでも動きます。

app/app.module.ts

@NgModule({
 ...
    AgmCoreModule.forRoot({ // Google Maps JavaScript 
      /*apiKey: 'Your API KEY HERE'*/
    })
 ...
})
export class AppModule { }

Google Maps JavaScript APIを利用できるように設定するのを忘れると以下のようなエラーで怒られます。

Google Maps API error: ApiNotActivatedMapError

あと直接は関係ないのですが、微妙に困ったところはAngularが変更を検知できない現象でした。
このリンク の3番目の解決方法で解決しました。
コード中でchangeDetectorRefを利用しているのはそのためです。

まとめ

angular-google-mapsを使えばあっさりできました。オススメです。
changeDetectorRefはFlexでいうところのvalidateNow()に見えて懐かしかったです。

CloudFront の署名付き URL で S3 にアクセスしてみる

こんにちは、ゴーリストのエンジニアのJPです。
最近 SaaS サービスの開発プロジェクトでインフラ構成の調査を行っているため、その中で話題に上がった AWS のサービスを利用したファイル配信方法を紹介したいと思います。

目次

ターゲット

以下のような疑問をお持ちの方

  • S3 へのアクセスを制限したい
    ( S3 の URL 知ってれば誰でもアクセスできちゃうよね?)
  • Lambda のレスポンスサイズの上限に引っかかる
  • 署名付き URL と Cookie どっちがいいの?

  ※具体的な設定方法は次回以降の記事で記載しますので、ご注意ください。

調査の背景

  • 新規サービスにはファイルをアップロード・ダウンロードする機能がある
  • アプリケーションのユーザーによりダウンロードできるファイルを制限したい
  • バックエンドは AWS の Lambda を使用
  • Lambda にはレスポンスのサイズに 6MB という制限がある → 足りないから DB じゃなくて AWS S3 にファイルを置こう
  • S3 からファイルをダウンロードする際に、ユーザーの権限判定ロジックを埋め込みたい
    どうする?

解決策

下図のように AWS の CloudFront の署名付き URL を利用して S3 のアクセスを制限し、取得者の権限チェックロジックも埋め込もう。

f:id:j-itoh:20170511124644j:plain

署名付き URL についての概要
Amazon の公式ドキュメント:CloudFront を使用してプライベートコンテンツを供給する

インターネットを通じてコンテンツを配信する多くの企業が、選ばれたユーザー(料金を支払っているユーザーなど)のドキュメント、ビジネスデータ、メディアストリーム、またはコンテンツに対して、アクセスを制限する必要があると考えています。CloudFront を使用してこのプライベートコンテンツを安全に供給するには、以下の方法を使用できます。 特別な CloudFront 署名付き URL または署名付き Cookie を使用してプライベートコンテンツにアクセスするようユーザーに要求します。 Amazon S3 コンテンツへのアクセスにユーザーが Amazon S3 URL ではなく CloudFront URL を使用するよう要求します。CloudFront URL を要求することは必須ではありませんが、ユーザーが署名付き URL や署名付き Cookie で指定された制限をバイパスすることを防ぐため、この方法をお勧めします。

CloudFront でできること

S3URL による S3 へのアクセスを無効にする
CloudFront の Distribution のドメイン名を含む URL でしか S3 のファイルにアクセスできない
例)https://sample.cloudfront.net/usagi.png

署名付き URL でのアクセスしか受け付けないようにできる
署名付き URL:公開鍵認証方式により暗号化された文字列を URL のクエリ文字列に含んだもの
例)

https://sample.cloudfront.net/usagi.png?Expires=1493274265&Signature=P-Mz9vHOGzL5HFwUpwoEwaPFDJIqd3T5rCoenGd3JFu50FtmTC9BM~o22rUf3gBn6wLECFgt6b-9bxJKUBc32uzGzy4M0KLBhW9W3CF4G79~sOHGJUKl0HmuLsFn1ZFy606~z5bBcdhp6hI-fJXd2dS4VEP1S~Z~q2rzTR75l3V1WnHnCTxLYWl5smhFavm57WdIan6jnvCRXnVWzWv7pbSz5VAmzx1N5MgQ0fflI1GaYMNel7sErr40BGeTSSoJpdFClWHfuOhclXFYUbq-dTVD4XRp0o4rQHM7uk-jFLTyi0v~jYFGfrYp1p825xwkiM1UYw~kasH5VHJM5Njs7g__&Key-Pair-Id=APKAJRIMZTJQZZQ42AVA

署名付き URL は以下のセキュリティーポリシーを追加することができる

  • 有効期限(開始日時、終了日時)
  • アクセスを許可する IP アドレス

署名付き URL は Java、Node.js、PHP 等の AWS ライブラリで作成する必要がある
具体的な作成方法は省略します。

認証の仕組み

署名付き URL の構造

https:// { Distribution } / { S3 Key } ?Expires= { 有効期限 }&Signature= { 暗号化情報 } &Key-Pair-Id= { アクセスキー ID }
パラメータ 説明
Distribution CloudFront の S3 接続用の Distribution のドメイン名
S3 Key S3 上の目的のファイルまでのパス
有効期限 URLが使えなくなる時刻(UTCで指定しなければならない)
暗号化情報 秘密鍵により暗号化かつ Base64 エンコードされた文字列(※)
アクセスキー ID AWSのルートアカウントにより作成された秘密鍵と公開鍵のペアの ID(認証に使用するキーペアを指定)

※暗号化情報には上表のパラメータが全て含まれている

認証手順

順序 実行サービス 処理内容
1 クライアント 署名付き URL により CloudFront にアクセス
2 CloudFront Distribution でアクセスが許可されている AWS ルートアカウントがアクセスキー ID を保持しているか判定
3 CloudFront 保持している場合、2の ID の公開鍵で Signature を復号化
4 CloudFront 復号化した内容とパラメータを比較( URL の改ざんチェック)
5 CloudFront 4の比較の結果、合わない場合アクセスを拒否(エラーを返す)
6 CloudFront 5が合う場合、有効期限などのポリシーをチェック
7 S3 有効期限内であればファイルを取得

URL の再利用への対策

  • 秘密鍵がなければ Signature を復号化できないため、有効期限等の暗号化情報を改ざんすることはできない
    (公開鍵で暗号化したものは3の公開鍵での復号化はできない)
  • 有効期限内であれば再利用可能(最適な有効期限を検討する必要)

どちらにするかの検討段階で、署名付き URL の以下のメリットが大きいかなと思い、正直 Cookie の方は調べていません。
Cookie のメリットはこれから勉強します。

  • 画像ファイルの URL をそのまま HTML タグのプロパティに書ける
<img src=" { 署名付き URL } "/>

Cookie だとフロント側でロジックが必要になりそう。

活用例

ファイルダウンロード機能において、アプリケーションのユーザー権限によるアクセス制限を行う
※ Lambda では 6MB という制限があるため、ファイルは一律 S3 から転送するようにする

順序 実行サービス 処理内容
1 クライアント ファイルダウンロードリクエストを送信
2 Lambda 権限チェック
3 Lambda 2で権限がある場合、署名付き URL を作成してレスポンスを送信
4 クライアント 署名付き URL にアクセス
5 CloudFront URL の改ざん、有効期限をチェック
6 CloudFront キャッシュの確認・あれば返信(7は実行しない)
7 S3 ファイルを送信

他の実現方法

他の案として Lambda@Edge でセッションをチェックするという方法もありましたが、以下の懸念があったため採用しませんでした。
※ Lambda@Edge とは CloudFront のリクエストと S3 の間に Lambda Function を処理をかませる機能です。

  • Lambda Function の実行時間の上限が0.05秒しかない
  • Node.js でしかロジックを書けない(ランタイムが Node.js にしか対応していない)

感想

署名付き URL でもアプリケーションのユーザー権限によるセキュリティを保つことができることが分かりました。
また、CloudFront を使用することで、キャッシュからの高速なファイル転送が可能となりました( SaaS だったら関係ないかも)。
Lambda のレスポンスサイズの上限を気にする必要がなく、ファイルを DB から取得する方法と、CloudFront 経由のファイル取得、どちらの方が早いのかは気になるところです。

課題としては、ベストな有効期間を検討することが残っています(動かしてみるしかない)。

機会があれば続きとして以下のようなことも書いていきたいです。

  • AWS コンソール上での設定方法
  • Java での署名付き URL の作成方法
  • CloudFront を経由して一括で複数のファイルをダウンロードする際の最も早い方法

XLPagerTabScriptの使い方

こんちは。渡部です。
最近はOSSのライブラリが豊富でありがたいですよね。
UIの参考にもなるし、実際に使用すると工数の削減になります。

今回はXLPagerTabScriptというライブラリの使い方を紹介します。
どんどこやっていきます。(パクリ)

f:id:watabe1028:20170430235401p:plain

こんな感じでやっていきます
・XLPagerTabScriptってなに?うまいの?
・どうやって使うのよ?
・XLPagerTabScriptをプロジェクトに追加する
・遷移するViewControllerを用意する
・Storyboardを設定する
・ガリガリ書く

れっつらゴー!

XLPagerTabScriptってなに?うまいの?

XLPagerTabScript github.com

一言で例えると、「カスタマイズしやすいタブ遷移ライブラリ」です。
カスタマイズしやすい点としてはUIViewControllerがそのまま使えることです。
よくライブラリを使うと、よくわからないクラスを継承していて
結局Wiki通りに使うことしかできないものが多々あります。(未熟なので)

また、今も頻繁に更新されているのでOSが上がった際にも安心です。

どうやって使うのよ?

GitHubで落としてゴー!

XLPagerTabScriptをプロジェクトに追加する

CocoaPodsで落とします。
使い方がわからない人はこちら

プロジェクトを作ったらPodsでXLPagerTabScriptをinstallします。
サンプルとして「TabTestApp」を作ります。

作ったらターミナルでアプリ直下まで行き、Podsを作成します。

pod init

作成したらXLPagerTabScriptを落とすため編集モードで追記し、installします。
編集モードにして、

vi Podfile

これを追記する。

  # Pods for TestApp
  pod ‘XLPagerTabStrip’
end

「:wq」で保存、終了。

インストール!

pod install

これで下準備おけ!

遷移するViewControllerを用意する

まんまです。
UIViewControllerを3つ追加します。
今回はわかりやすく
「RedViewController」
「BlueViewController」
「GreenViewController」
の3つを追加し、それぞれviewに色を設定します。
Storyboardにも追加し、関連付けてください。

Storyboardを設定する

まず、ベースとなる(デフォルトで入っている)ViewControllerに
Collection ViewとScroll Viewを追加します。
ついでにAutoLayoutもつけちゃってください。

Scroll VIewを設定します。
ます、追加したScroll Viewの上で右クリックし、
「 New Referencing Outlet」をViewControllerに繋ぎます。
その際に「containerView」と「view」の選択肢が出るので、「containerView」を選択します。

Collection Viewはclassを「ButtonBarView」、Moduleを「XLPagerTabStrip」を選択します。

f:id:watabe1028:20170430234311p:plain

その後、ScrollViewと同様に
「 New Referencing Outlet」をViewControllerに繋ぎ、
出てくる選択肢の「buttonBarView」を選択します。

これで準備ができました。

ガリガリ書く

もちろん、ソース無しでは動きません。
が、結構簡単にかけます。コピペでできるように最低限のコードのみ記載します。

まずはベースとなるViewControllerに以下を記述します。

import XLPagerTabStrip // インポートする

class ViewController: ButtonBarPagerTabStripViewController {    // 「ButtonBarPagerTabStripViewController」に変更する

    override func viewDidLoad() {
        
        // もしタブのカスタマイズをするなら
        // ここに追記する

        super.viewDidLoad()
    }
    
    /// ViewControllers
    override func viewControllers(for pagerTabStripController: PagerTabStripViewController) -> [UIViewController] {
        
        // 追加したViewControllerを指定
        let firstVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "redViewController")
        let secondVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "blueViewController")
        let thirdVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "greenViewController")
        
        // ViewControllersに入れる
        let childViewControllers:[UIViewController] = [firstVC, secondVC, thirdVC]
        return childViewControllers
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

次に追加した各ViewControllerに以下を記述します。
RedViewControllerを例にしてます。

import XLPagerTabStrip  //インポートして

class RedViewController: UIViewController, IndicatorInfoProvider {    // 「IndicatorInfoProvider」を追加
    
    // タブのタイトルを設定
    var itemInfo: IndicatorInfo = "RED"

    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    // これはお約束。コピペでおけ!
    func indicatorInfo(for pagerTabStripController: PagerTabStripViewController) -> IndicatorInfo {
        return itemInfo
    }
}

これで実行してください。

タブでスルスル遷移します。
スワイプでも遷移するので試してみてください。

まとめ

少ないコードで劇的に工数が減らせる最高のライブラリですね。
これからも楽をするためにバンバンライブラリを取り入れて行きたいと思います!

はじめてのAngular TODOアプリをつくる③ ルーティング編

はじめてAngular2を触ってみた初心者のメモ書きです。

公式チュートリアルをチラ見しながらTODOアプリを作っています。
今回は第三回のルーティング編です。
前回に引き続き、appコンポーネントをドコドコ分割していきます。

やりたいこと

  • ダッシュボードを作る
  • ダッシュボード、タスク一覧、タスク詳細のビューを切り替える
  • それぞれのビューにURLパスを割り振る
  • TODOアプリとしての体裁を整える

チュートリアルで言うとこのあたりです。

Angular Docs

今回もAngular CLIの恩恵にあずかってまいります。

appコンポーネントを分割する

新しい子コンポーネントとしてTasksComponentを作ります。

ng g component tasks

で、今までappコンポーネントにそのまま書いてたタスク一覧と詳細部分を子側に切り取って貼っちゃいます。

app.component.ts

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})

export class AppComponent {
  private title = 'todo app';

}

app.component.html

<h1>{{title}}</h1>
<app-tasks></app-tasks>

だいぶすっきりしました。

ルーティングを追加する

クライアント側でルーティングまでやってしまうんですね~

index.htmlのbase hrefを変更

<base href="/">にする、ということでしたが、
Angular CLIでプロジェクト作ったのでデフォルトでそうなってました。

app.module.tsにRouterModuleをインポート

で、パスと呼び出すコンポーネントを宣言。

app.module.ts

import { RouterModule } from '@angular/router';
・・・
@NgModule({
  ・・・
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    RouterModule.forRoot([
      {
        path: 'list',
        component: TasksComponent
      }
    ])
  ],
  ・・・
})

リンクを作る

さっき宣言したパスへのリンクを貼ります。
呼び出されたコンポーネントが<router-outlet></router-outlet>に表示されます。

app.component.html

<h1>{{title}}</h1>
<a routerLink="/list">task list</a>
<router-outlet></router-outlet>

トップページはこうなって

f:id:y-iio:20170411180658p:plain

/listでタスク一覧が表示されるようになりました。

f:id:y-iio:20170411180717p:plain

ダッシュボードを作る

コンポーネント追加

いつものようにng g componentでコンポーネント追加します。

ng g component dashboard

ルーティング追加

先ほど同様、app.module.tsにルーティングを追加します。
初期表示時にリダイレクトさせたいのでリダイレクトルートも追加。

app.module.ts

RouterModule.forRoot([
  {
    path: 'list',
    component: TasksComponent
  },
  {
    path: 'dashboard',
    component: DashboardComponent
  },
  {
    path: '',
    redirectTo: '/dashboard',
    pathMatch: 'full'
  },
])

これで初期表示(/にアクセス)時に/dashboardがリダイレクト表示されるようになりました。

ナビゲーションを作る

app.component.htmlにもリンクを貼り、ナビゲーション完成です。

app.component.html

<h1>{{title}}</h1>
<nav>
  <a routerLink="/dashboard">Dashboard</a>
  <a routerLink="/list">Task List</a>
</nav>
<router-outlet></router-outlet>

ダッシュボードの中身を作る

チュートリアルだとダッシュボードに表示されるHeroはid若い順に5人という謎な感じですから、
このTODOアプリでは重要なタスクだけ表示してみようとします。

Taskクラスに重要度を追加して、
ついでにTODOアプリとしての体裁のために完了フラグも追加して、

task.ts

export class Task {
  public id: number;
  public name: string;
  public importance: string;
  public isDone: boolean = false;
}

モックデータにも重要度、完了フラグを付けます。

mock-tasks.ts

import { Task } from './task';

export const TASKS: Task[] = [
  { id: 11, name: '企画ロードマップ作成', importance: 'high', isDone: false },
  { id: 12, name: '山田さんにメール返信', importance: 'high', isDone: false },
  { id: 13, name: 'Angular2キャッチアップ', importance: 'mid', isDone: false },
  { id: 14, name: 'ブログ更新', importance: 'low', isDone: false },
  { id: 15, name: '新卒技術研修', importance: 'low', isDone: false }
];

タスク詳細画面で重要度を編集できるようにして…
(もっと良いやり方ありそうですがとりあえずこれで許して)

task-detail.component.html

<div>
  <label>importance: </label>
  <select [(ngModel)]="task.importance">
    <!-- options = ['low', 'mid', 'high'] -->
    <option *ngFor="let option of options" [value]="option">{{option}}</option>
  </select>
</div>

ダッシュボードは重要度の高いタスクのみフィルタして表示します。

dashboard.component.ts

export class DashboardComponent implements OnInit {
  private tasks: Task[];

  constructor(private taskService: TaskService) { }

  private getTasks(): void {
    this.taskService.getTasks().then(retTasks => {
      this.tasks = retTasks.filter(function(retTask) {
        return retTask.importance === 'high';
      });
    });
  }

  ngOnInit() {
    this.getTasks();
  }
}

dashboard.component.html

<h3>Important Tasks</h3>
<div class="grid grid-pad">
  <a *ngFor="let task of tasks" [routerLink]="['/detail', task.id]" class="col-1-4">
    <div class="module task">
      <h4>{{task.name}}</h4>
    </div>
  </a>
</div>

CSSもチュートリアルからパクってきて適当に入れました。

f:id:y-iio:20170411180802p:plain

それらしくなってきました。

TODOアプリっぽくする

TODOアプリとしての体裁のために完了フラグをねじこんだのですが、
とりあえずtask-detailから変更できるようにしてみました。

task-detail.component.html

<h2>
 {{task.name}}
  <span *ngIf="!task.isDone">details</span>
  <span *ngIf="task.isDone">is DONE!!!</span>
</h2>
・・・
<button class="btn-primary" (click)="done()">done!!!</button>

task-detail.component.ts

public done(): void {
  this.task.isDone = true;
}

サービスで完了したものはフィルタします。

task.service.ts

public getTasks(): Promise<Task[]> {
  return Promise.resolve(TASKS.filter(function(task) {
     return !task.isDone;
  }));
}

これで完了ボタンを押すと

f:id:y-iio:20170424185755p:plain

リストで非表示になります。

f:id:y-iio:20170424185817p:plain

タスク詳細ビューもルーティング

ルーティング追加

先ほど同様、app.module.tsにルーティングを追加します。
:idでパラメータ送れるみたいな?

app.module.ts

{
  path: 'detail/:id',
  component: TaskDetailComponent
}

サービスにメソッド追加

ID指定でタスクひとつを返却するメソッドを追加します。

task.service.ts

public getTask(id: number): Promise<Task> {
  return this.getTasks().then(tasks => tasks.find(task => task.id === id));
}

タスク詳細でパラメータ取得できるようにする

どんどこimportしていきます。

task-detail.component.html

import { Component, OnInit, Input } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router'; // 追加
import { Location } from '@angular/common'; // 追加
import 'rxjs/add/operator/switchMap'; // 追加

import { Task } from '../task';
import { TaskService } from '../task.service'; // 追加

const OPTIONS: string[] = ['low', 'mid', 'high'];

@Component({
  selector: 'app-task-detail',
  templateUrl: './task-detail.component.html',
  styleUrls: ['./task-detail.component.scss']
})
export class TaskDetailComponent implements OnInit {
  @Input() task: Task;
  options = OPTIONS;

  // コンストラクタで呼び出し
  constructor(
    private taskService: TaskService,
    private route: ActivatedRoute,
    private location: Location
  ) { }

  // コンポーネント初期表示時に実行
  ngOnInit(): void {
    // パラメータの値を取得して、タスク詳細を呼び出す
    this.route.params
      .switchMap((params: Params) => this.taskService.getTask(+params['id']))
      .subscribe(task => this.task = task);
  }

}

ちょっとこの部分

this.route.params
  .switchMap((params: Params) => this.taskService.getTask(+params['id']))
  .subscribe(task => this.task = task);

のわかんなさですが、非同期でgetTaskしてきて?
subscribeのときに結果を代入してる?

ブラウザバック実装

詳細ビューにはダッシュボードか一覧に戻れるように戻るボタンを付けてあげます。

メソッドは

public goBack(): void {
  this.location.back();
}

ダッシュボードからも一覧からも詳細が見られるようになりました~

f:id:y-iio:20170411180930p:plain

ルーターを別モジュールにまとめる

いつものng generateでモジュールも作れます。

ng g module app-routing

AppRoutingモジュールの中身はapp.module.tsから取ってきてこんなかんじに

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Routes } from '@angular/router';

import { DashboardComponent } from '../dashboard/dashboard.component';
import { TaskDetailComponent } from '../task-detail/task-detail.component';
import { TasksComponent } from '../tasks/tasks.component';

const routes: Routes = [
  { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
  { path: 'dashboard', component: DashboardComponent },
  { path: 'list', component: TasksComponent },
  { path: 'detail/:id', component: TaskDetailComponent },
];

@NgModule({
  imports: [
    CommonModule,
    RouterModule.forRoot(routes)
  ],
  exports: [ RouterModule ],
  declarations: []
})
export class AppRoutingModule { }

これをAppModule側にインポートして、
importsにAppRoutingModuleを追加してあげれば分離完了です。
大変な道のりでした。

後は全体的にCSSをば調整して…こんな感じになりました。

f:id:y-iio:20170411180955p:plain

f:id:y-iio:20170411181045p:plain

f:id:y-iio:20170411181006p:plain

感想

ちょっと本当に今回は長くて大変でした。
チュートリアルに沿っているだけで、モジュール?コンポーネント?ハハハ
まあいつか分かればいいです。

次回感動の最終回、TODOアプリ④ HTTP編を刮目して見よ!
GW明けに投稿予定です。

はじめてのAngular TODOアプリをつくる② コンポーネントとサービス編

はじめてAngular2を触ってみた初心者のメモ書きです。

前回の投稿から、公式チュートリアルをチラ見しながらTODOアプリを作っています。
今回は、前回作ったappコンポーネントをサクサクと分割していきたいと思います。
チュートリアルで言うとこのあたりです。

Angular Docs

今回もAngular CLIの恩恵を享受していきます。

目次

コンポーネントを分割する

コンポーネントを作成(ng generate component)

ファイル名はケバブケースでコンポーネント作成します。(最初キャメルにしたらAngular CLIに直された)

ng g component task-detail

作成したコンポーネントをインポート

前回同様Taskクラスをインポートします。import { Task } from './task';
app.component.ts、task-detail.component.tsどちらにもインポート文が必要です。

親コンポーネントのプロパティをバインド

また子コンポーネントのプロパティに@Input()アノテーションを付けることで、
親コンポーネントの値がバインドされるようになります。
よくわかんないけど親側での変更が反映されるんだな~と思っておきます。

task-detail.component.ts

//ここでInputアノテーションが使えるようにInputをインポートしてます
import { Component, OnInit, Input } from '@angular/core';

import { Task } from './task';

@Component({
  selector: 'app-task-detail',
  templateUrl: './task-detail.component.html',
  styleUrls: ['./task-detail.component.scss']
})
export class TaskDetailComponent implements OnInit {
  @Input() task: Task;

  constructor() { }

  ngOnInit() {
  }

}

ディレクティブを置き換えてコンポーネント表示

task-detail.component.html

<div *ngIf="task">
  <h2>{{task.name}} details!</h2>
  <div><label>id: </label>{{task.id}}</div>
  <div>
    <label>name: </label>
    <input [(ngModel)]="task.name" placeholder="name"/>
  </div>
</div>

今まで↑のディレクティブが書いてあった部分を今作ったコンポーネントに置き換えます。
selectedTaskをtaskというプロパティ名で子コンポーネントに送ってバインドしています。

app.component.html

<app-task-detail [task]="selectedTask"></app-task-detail>

ヌゥ…なんでエラー

ERROR in C:/angularWork/my-new-app/src/app/task-detail/task-detail.component.ts (3,22): Cannot find module './task'.)

ERROR in ./src/app/task-detail/task-detail.component.ts
Module not found: Error: Can't resolve './task' in 'C:\angularWork\my-new-app\src\app\task-detail'
 @ ./src/app/task-detail/task-detail.component.ts 11:0-30
 @ ./src/app/app.module.ts
 @ ./src/main.ts
 @ multi webpack-dev-server/client?http://localhost:4200/ ./src/main.ts

appコンポーネントにも同じインポート文書いてるのにな?と思ったら
task-detail.component.tsはapp/task-detail/下にあったのでした。
import { Task } from './task';じゃなくて
import { Task } from '../task';でした。
変なところで時間使ってしまった。

これでコンポーネント分割できました~見た目は全く変わってませんが。

f:id:y-iio:20170410164651p:plain

サービスを使う

気を取り直して、サービスを作っていきます。
チュートリアルで言うとこのあたりです。

Angular Docs

サービスを作る(ng generate service)

新しいコマンドですが、いつものようにng g

ng g service task
  installing service
    create src\app\task.service.spec.ts
    create src\app\task.service.ts
    WARNING Service is generated but not provided, it must be provided to be used

not provided?ということは何かやらないと使えないのか…

app下にtask.service.tsが作られてました。
中身はこう。

import { Injectable } from '@angular/core';

@Injectable()
export class TaskService {

  constructor() { }

}

@Injectable()アノテーションは最初からつけておいてくれるんですね。

モックデータを移動する

今までapp.component.tsで宣言していた配列を別ファイルに分離させます。
Taskクラスも忘れずにインポート。

src/app/mock-tasks.ts

import { Task } from './task';

export const TASKS: Task[] = [
  { id: 11, name: '企画ロードマップ作成' },
  { id: 12, name: '山田さんにメール返信' },
  { id: 13, name: 'Angular2キャッチアップ' },
  { id: 14, name: 'ブログ更新' },
  { id: 15, name: '新卒技術研修' }
];

親コンポーネントのプロパティであるところのtasksは初期化しないように変更します。

app.component.ts

export class AppComponent {
  private title = 'todo app';
  private tasks: Task[];// ここです
  private selectedTask: Task;
}

task.service.tsでモックデータを受け取って、返却するようにします。

import { Injectable } from '@angular/core';

import { Task } from './task';
import { TASKS } from './mock-tasks';

@Injectable()
export class TaskService {
  public getTasks(): Task[] {
    return TASKS;
  }

  constructor() { }

}

TaskServiceをインポートする

作ったサービスをAppComponentで使えるようにしていきます。
いつものようにインポート文を加えて

import { TaskService } from './task.service';

コンストラクタでサービスインスタンスを取得します。newして呼び出してはいけないそうです。

export class AppComponent {
  private title = 'todo app';
  private tasks: Task[];
  private selectedTask: Task;

  constructor(private taskService: TaskService) {
  }

  public onSelect(task: Task): void {
    this.selectedTask = task;
  }
}

で保存したらエラー

f:id:y-iio:20170410202740p:plain

そういえばさっきnot providedって言われてました。
app.module.tsでprovidersを宣言しないといけないようです。

app.module.ts

@NgModule({
 ・・・
  providers: [TaskService], 
 ・・・
})

これでエラーが消えました。一安心。
サービスのgetTasks()を宣言しなおします。

app.component.ts

private getTasks(): void {
  this.tasks = this.taskService.getTasks();
}

ngOnInitというメソッドが、コンポーネントの生成後に呼ばれるそうなので、
ここでさっき作ったgetTasks()を呼んでもらいます。

app.component.ts

import { Component, OnInit } from '@angular/core'; // OnInitもインポート
・・・
export class AppComponent implements OnInit { // implementsでOnInitを使えるようにして
  private title = 'todo app';
  private tasks: Task[];
  private selectedTask: Task;

  constructor(private taskService: TaskService) { }

  private getTasks(): void {
    this.taskService.getTasks().then(tasks => this.tasks = tasks);
  }

  // ここで使う
  ngOnInit() {
    this.getTasks();
  }
}

これでモックデータを呼んでくることができるようになりました~
見た目は全く変わってませんが。

f:id:y-iio:20170410164440p:plain

非同期でサービスを使う

promise使って非同期でモックデータを持ってきてみます。

TaskServiceのgetTasks()メソッドでTasksを返していたところを、
Promise<Tasks
>を返すように変更します。

task.service.ts

public getTasks(): Promise<Task[]> {
  return Promise.resolve(TASKS);
}

tasks.component.ts

private getTasks(): void {
  this.taskService.getTasks().then(tasks => this.tasks = tasks);
}

アロー関数、初めて使いました。
thisを別名で宣言しなくて良いんですね。

これで非同期でデータを取ってくることができるようになりました~
見た目は全く変わってませんが。

f:id:y-iio:20170410164440p:plain

ソースコードまとめ

感想

なんだか大変だったわりに見た目は全く変わっていません。
何にもやってないみたいじゃないか、いやいや学びはありました。
次回でもう少しは体裁を整えられるのか、TODOアプリを作る③ ルーティング編、乞うご期待!