Angular 2 Componentのnode moduleを最小限の構成で作る

モチベーション

Angular2のコンポーネントを共有したい。

具体的には共有したいコンポーネントを@NgModuleで包んでnode moduleにして、いろんなプロジェクトからnpm installして使えるようにしたい。 最低限の構成でこれを実現するnode moduleプロジェクトmydatelibと、mydatelibを利用するプロジェクトconsumerを作った。

構成

root // 作業ディレクトリ
├─mydatelib
│  ├─dest
│  ├─src
│  │  ├─index.ts
│  │  ├─mydatelib.module.ts
│  │  └─datetime.component.ts
│  ├─tsconfig.json
│  └─package.json
└─ consumer //angular2-seedからゆるく作る。

mydatelib

作業スペースrootを作る。

mkdir root
cd root

mydatelibプロジェクトを作る。

mkdir mydatelib
cd mydatelib
mkdir src

npm initしてAngular2としての最低限のライブラリと、ngcをインストール。 最低限すぎるのでyarnさえ使わない。

npm init -y
npm i -S @angular/core rxjs zone.js
npm i -D typescript @angular/compiler @angular/compiler-cli

packageとtsのコンパイル設定

package.jsonを編集して、配布ディレクトリを指定。ngcスクリプトに登録。

"main": "dest/index.js",
"files": ["dest"],
"scripts": {
  "ngc": "ngc"
},

tsc --initしてtsconfig.jsonを作る。

./node_modules/.bin/tsc --init

tsconfig.jsonを編集。

{
    "compilerOptions": {
        "module": "commonjs",
        "target": "es5",
        "noImplicitAny": false,
        "sourceMap": true,
        "declaration": true,
        "outDir": "dest",
        "experimentalDecorators": true,
        "moduleResolution": "node",
        "lib": ["es6", "dom"]
    }
}

コンポーネントとモジュールをかく

時刻表示コンポーネント

datetime.component.ts

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

@Component({
  selector: 'datetime',
  template: '<p>{{date}}</p>'
})
export class DateTimeComponent {
  date: string = new Date().toString();
}

モジュール

mydatelib.module.ts

import { NgModule } from '@angular/core';
import { DateTimeComponent } from './datetime.component';

@NgModule({
  declarations: [
    DateTimeComponent
  ],
  exports: [
    DateTimeComponent
  ]
})
export class MyDateLibModule {}

index

index.ts

export * from './mydatelib.module';

ビルドする

npm run ngc

エラーが出なかった完了

mydatelibを利用するconsumer

angular2-seedをベースにrootディレクトリにconsumerプロジェクトを作る。

cd .. // rootに移動
git clone https://github.com/angular/angular2-seed.git consumer
cd consumer

angular2-seedデフォルトのnode moduleをインストール

npm install

一旦動かしてみる

npm start

http://localhost:3000 にアクセスするとこんなんが出る。

f:id:chikulla:20161229050744p:plain

Angular 2 Seedの下あたりにDateTimeComponentを挿入してみることにする

mydatelibインストール

npm i -S ../mydatelib 

ここはgitからインストールするか、プライベートnpmリポジトリからインストールできるように変えたい

DateTimeComponentが表示されるように関連ソースを更新

モジュール

src/app/にあるapp.module.ts

import文を書く。読み込むのはモジュールだけ。

import { MyDateLibModule } from 'mydatelib/dest';

@NgModuleの中にもMyDateLibModuleを書く。

  imports: [
    BrowserModule,
    FormsModule,
    ReactiveFormsModule,
    HttpModule,
    RouterModule.forRoot(rootRouterConfig, { useHash: true }),
    MyDateLibModule // これ!
  ],

AppComponentのテンプレートを編集

src/appにあるapp.component.html

</h3><nav>の間にDateTimeComponentのselectorを書く。

</h3>
<datetime></datetime>
<nav>

動作確認

npm start

http://localhost:3000/ を開く。

f:id:chikulla:20161229050723p:plain

やったね。

国旗の使い方2

これの記事です。12/21日分

www.adventar.org

前回作った255枚の国旗のPDFをCocoaアプリケーションから使えるようにします。

PDFのロード

適当にiOSアプリケーションプロジェクトを作ります。Single View Applicationで。

f:id:chikulla:20161221194720p:plain

使う

全部の.pdfファイルをAssets.xcassetsにドロップします。私のMacBook Airはここで固まります。

f:id:chikulla:20161221195707g:plain

pdfを配置できたので、ImageViewを適当にViewに配置しましょう

f:id:chikulla:20161221200129p:plain

コントローラとImageViewを関連づけます。

f:id:chikulla:20161221200210g:plain

とりあえずセントビンセント及びグレナディーン諸島でも出しておきます。 ViewControllerのviewDidLoad()でImageViewにセントビンセント及びグレナディーン諸島を設定します。 Assetsにあるイメージはコード保管でサムネイル出てきます。素敵ですね。

f:id:chikulla:20161221201111g:plain

ビルドしてSVGからPDFに変えたセントビンセント及びグレナディーン諸島がImageViewでちゃんと表示できるか確認します。 軽いとはいえAssetが255もあるのでビルドにそれなりに時間がかかります。

f:id:chikulla:20161221201352p:plain

はいセントビンセント及びグレナディーン諸島

enumで扱う

大量の画像があるので、全部表示したい!みたいな時はAssetの名前指定ではなく、enumで扱うと楽です。 imageViewにセットする時はAssetの名前をname:パラメータをString指定しなければならないので、enumの要素に対してStringが返ってくると楽です。

Swiftだとこんなふうにかけます

enum CountryKey: String {
    case JP = "Japan"
}
print(CountryKey.JP.rawValue) // "Japan\n"

いちいち文字列割り当てなくても、何も書かなければ要素のキーの文字列が返ってきます。 要素のキーは大文字開始が推奨されてます。

enum CountryKey: String {
    case JP
}
print(CountryKey.JP.rawValue) // "JP\n"

国旗が沢山あるのでこっちの方が楽そうでいいですね。 enumのcase文はPDFを作った時のrubyスクリプトgenerate.rbで作ってもらうことにしましょう。 さらに、swiftのenumは全要素取得(Javaで言う所のvalues)もありません。こんなふうに実装します。

enum CountryKey: String{
    case AD
    //国名全部
    case ZW

    static func fromIndex(at: Int) -> CountryKey  {
        switch at {
        case 0:
            return .AD
        case 1: 
            //全部実装
        default:
            return .ZW
        }
    }

    static func length() -> Int {
        return CountryKey.ZW.hashValue + 1
    }
    
    static func values() -> [CountryKey] {
        var natural: [Int] = []
        for i in 0...CountryKey.length() - 1 {
            natural.append(i)
        }
        var values: [CountryKey] = []
        repeat {
            values.insert(fromIndex(at: natural.popLast()!), at:0)
        } while natural.count > 0
        return values
    }

国旗が255もあるので、fromIndex(at:)の中身もenumの要素のキーcaseと同じようにgenerate.rbに作ってもらうことにします。

ただし、Swiftのenumの要素に-ハイフンは使えません。 また、今生成できているPDFはSVGの元のファイル名に従って小文字の名前を持ってます。enum要素名の推奨は大文字始まりです。 ということで、今のPDFのAssetの名前を直接enumのcaseにするのはだめっぽいので、

f:id:chikulla:20161221203459p:plain

rubyでAssetの名前を大文字、ハイフンをアンダースコアに変えます。 generate.rbをこんな風に変えました。ゲロみたいにネストしてますがAdventCalendarの投稿タイムリミットが迫っているのでご勘弁を

require "prawn"
require "prawn-svg"
src = "./node_modules/flag-icon-css/flags/4x3/"
dist = "./dist/"
swift = "./swift/"

Dir.mkdir(dist) unless File.exists?(dist)
Dir.mkdir(swift) unless File.exists?(swift)

i = 0
open(swift + "enum.swift", "w") { |e|
  open(swift + "switch.swift", "w") { |s|
    Dir.entries(src).select { |f|
      if File.extname(src + f)==".svg"
        name = File.basename(src + f, ".svg").upcase.gsub(/-/,"_")
        e.puts("case " + name)
        s.puts("case " + i.to_s + ":")
        s.puts("    return ." + name) 
        i += 1
        Prawn::Document.generate(dist + name + ".pdf", page_size: [600, 800], page_layout: :landscape) do
          svg IO.read(src + f), position: :center, vposition: :center, width: 800, height: 600
        end
      end
    }
  }
}

綺麗に生成できました。真面目にswiftのenum生成してもいいのですが、投稿タイムリミットが迫ってるので普通にコピペします。

f:id:chikulla:20161221211301p:plain

CollectionViewで見せる

投稿のタイムr ので適当にUICollectionViewをレイアウトします。

f:id:chikulla:20161221222711p:plain

ViewControllerを書きます。CollectionViewのnumberOfItemsInSectionCountryKey.length()を返します。 cellForItemAtCountryKey.fromIndexがいきてきます。

import UIKit

class ViewController: UIViewController {

    @IBOutlet var collectionView: UICollectionView!
    override func viewDidLoad() {
        super.viewDidLoad()
        self.collectionView.dataSource = self
    }
}

extension ViewController: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return CountryKey.length()
    }
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
        let imageView = cell.contentView.viewWithTag(1) as! UIImageView
        let image = UIImage(named: CountryKey.fromIndex(at: indexPath.row).rawValue)
        imageView.image = image
        return cell
    }
}

extension ViewController: UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let width = self.view.frame.width / 2
        let height = width / 4 * 3
        return CGSize(width: width, height: height)
    }
}

collectionViewLayoutで国旗の比率に合わせたCGSizeを返すと良しです。

f:id:chikulla:20161221223744g:plain

ああ間に合った。

国旗の使い方1

これです

モチベーション

国旗って眺めてるだけで面白くないですか?アルゼンチンの

アルゼンチンの国旗

真ん中のやつとかなかなか良い表情をしてます。

f:id:chikulla:20161212103839p:plain




じゃあこれはだーれだ?

f:id:chikulla:20161212104802p:plain




はいウルグアイでしたー

Flag of Uruguay.svg




じゃあ朴訥な彼は?

f:id:chikulla:20161212104954p:plain




え?エクアドルっすよ!

エクアドルの国旗

こういう国旗の画像を集めてCocoaとかから使いたい。

収集

国連加盟国だけでも193あるので、一枚にかけるサイズは抑えつつ、国旗に細かく描かれていることを表現してもらいたい。ので、ベクターイメージを集めることにする。WikipediaFlag_of_国名.svgのファイルを引っ張ってくるんでもいいが、ググったら一発目で

f:id:chikulla:20161212111855p:plain

見つけちゃった

github.com

これを使うことにします。

CocoaでのSVGの扱い

Cocoa ApplicationでSVGを直に使うことはできません。残念でした。SVGを扱えるようにするライブラリは色々あるのですが、

https://github.com/SVGKit/SVGKit https://github.com/mchoe/SwiftSVG

SVGを使えるようにする程度ならそんなにめんどくさくないので自分で加工します。具体的にはSVGをPDFにしちゃえばいいだけです。PDF変更後のファイルサイズは最悪SVGの2倍くらいになっちゃうのですが、元の国旗のSVGはでかくても数十kbなのであんまり気にしない。

npm install

git cloneでもいいんですが、ここでは後々のためにnpmで管理します。 package.jsonを書いて

{
  "dependencies": {
    "flag-icon-css": "^2.8.0"
  }
}

npm installしましょう

npm install

はい

SVG > PDF

node_modules/flagsの中に1x1と4x3の比率のsvgが転がってます。 国旗の形とかサイズはその国が勝手に規定してるものなんですが、オリンピックとかになると4x3に統一されたりするらしいです。 ネパールとかいう異端児もいますが、

ネパールの国旗

ここでは我慢して長方形の枠の中に収まっていただきたい。

SVGからPDFへの変換は、精度が良かったprawn-svgを使います。rubyです。

github.com

package.jsonと同じところにgenerate.rbを作ります。

require "prawn"
require "prawn-svg"
src = "./node_modules/flag-icon-css/flags/4x3/"
dist = "./dist/"
Dir.mkdir(dist) unless File.exists?(dist)
Dir.entries(src).select { |f|
  if File.extname(src + f)==".svg"
    name = File.basename(src + f, ".svg")
    Prawn::Document.generate(dist + name + ".pdf", page_size: [600, 800], page_layout: :landscape) do
      svg IO.read(src + f), position: :center, vposition: :center, width: 800, height: 600
    end
  end
}

ついでにpackage.jsonスクリプトも追加して

{
  "scripts": {
    "gem:install": "gem install prawn-svg",
    "pdf": "ruby generate.rb"
  },
  "dependencies": {
    "flag-icon-css": "^2.8.0"
  }
}

svgを丸ごとpdfにします。

npm run gem:install
npm run pdf

はいできた

f:id:chikulla:20161212234105p:plain

ここまでできたらCocoaのAssetを追加してドラッグ&ドロップするだけですが、255ファイルあるんでいちいち追加するのは辛いです。次回は全部の国旗をCocoaからごそっと丸ごと使えるようにします。

Kolodaの使い方

アドベントカレンダー用の投稿

モチベーション

CocoaTinderみたいなViewを提供するライブラリがある。

github.com

これを使いたい。使う。

Carthageの準備

Cocoaライブラリのパッケージマネージャとして、だいたいみんなCocoaPodsかCarthageを使う。 ここではシンプルで軽量なCarthageを使う。

github.com

CarthageはHomebrewというパッケージマネージャでインストールできる。 Homebrewが入ってなかったら入れる。入れない理由がない。

$/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

brewが入ったらcarthageを入れる。

$brew install carthage

はいこんにちは

$carthage version

プロジェクトの準備

XcodeでSingle View Applicationプロジェクトを作る。

f:id:chikulla:20161129180441p:plain

作ったプロジェクトのルートディレクトリ(*.xcodeprojがあるとこ)にCartfileという名前のファイルを作る。

f:id:chikulla:20161129211646p:plain

今回はSwift3+Xcode8.1でiOSターゲットのサンプル作るが、 2016年11月時点でKolodaのmasterブランチはSwift3対応できてないので、Swift3ブランチをCarthageから使う。どんなライブラリを使うか、ライブラリのどのブランチを使うかは、Cartfileで指定できる。 Cartfileの中身はこんなん。

github "Yalantis/Koloda" "swift-3"

書き終わったら、ターゲットをiOSにするオプションをつけてcarthage updateする。

$carthage update --platform iOS

プロジェクトの設定

carthage updateが成功したら、Koloda.frameworkpop.frameworkがCarthageディレクトリ以下にできてる。

f:id:chikulla:20161129215122p:plain

Xcodeを開いて、プロジェクトルートのGeneralタブの一番下Linked Frameworks and Librariesに2つの.frameworkをドロップする。

f:id:chikulla:20161129215552p:plain

Build Phasesタブを開いて、左の+ボタン(Add a new build phase)を押し、New Run Script Phaseを選択する。

f:id:chikulla:20161129220059p:plain

Run Scriptのコマンドに

/usr/local/bin/carthage copy-frameworks

を書く。さらにInput Filesに

$(SRCROOT)/Carthage/Build/iOS/pop.framework
$(SRCROOT)/Carthage/Build/iOS/Koloda.framework

を書く。popがKolodaの前提ライブラリなので、先頭に持ってこないとビルド失敗する。こんな感じになるはず。

f:id:chikulla:20161129221502p:plain

なんでこんなスクリプトを設定しなきゃいけないのかというと、App Store のサブミッションのバグの対策らしい。 cmd+bでビルドして、エラーがでなかったらライブラリのロードは完了。

Viewを作る

デフォルトのstoryboardとコントローラをそのまま使う。 Main.storyboardを開く。右下のObject LibraryからViewを選択して、左ツリーのView Controller Scene > View Controller > Viewの下に配置する。 配置したViewをKolodaViewとして扱うために、右上のIdentity InspectorからClassをKolodaViewにする。ModuleをKolodaにする。

f:id:chikulla:20161130120624p:plain

配置したKolodaViewをViewの真ん中らへんにレイアウトする。

f:id:chikulla:20161129222753p:plain

ControllerとViewを繋げる

Main.storyboardを開きながら、右上の円が二つ重なってるアイコンを押して、ViewController.swiftを表示する。ViewController.swiftにimport Kolodaを書いてKolodaをインポートする。

f:id:chikulla:20161129223359p:plain

Main.storyboardのツリーからctrlを押しながらKoloda Viewをクリックして、ViewController.swiftのviewDidLoad()の上らへんにドラッグ&ドロップする。

f:id:chikulla:20161129223730p:plain

ドロップしたらNameをkolodaViewにして、Connectを押す。

f:id:chikulla:20161129223845p:plain

ViewControllerを書く

KolodaViewには最低限KolodaViewDatasourceを設定する必要がある。Datasourceは表示するカードの情報源となるプロトコルプロトコルJavaで言う所のInterfaceとだいたいおなじ。ここではViewControllerをDatasourceにする。

viewDidLoad()はWeb APIでいうところのwindow.onload。ここでkolodaViewのdataSourceをcontroller自身にする。 kolodaNumberOfCardsでカードの枚数を返す。ここでは10枚返すことにする。 koloda:viewForCardAtでカードのViewを返す。ここではコンパイルエラーを取るため、背景がグレーのUIViewを返す。

import UIKit
import Koloda

class ViewController: UIViewController {
    @IBOutlet var kolodaView: KolodaView!
    override func viewDidLoad() {
        super.viewDidLoad()
        kolodaView.dataSource = self
    }
}

extension ViewController: KolodaViewDataSource {
    func kolodaNumberOfCards(_ koloda: KolodaView) -> Int {
        return 10
    }
    func koloda(_ koloda: KolodaView, viewForCardAt index: Int) -> UIView {
        let view = UIView(frame: koloda.bounds)
        view.backgroundColor = UIColor.gray
        return view
    }
}

extension A: BJavaでいうclass A implements Bだと思っていい。 ここではdataSourceとControllerのライフサイクルは同じだが、dataSourceを別クラスに切り出すような場合は、Controllerより先に勝手にdataSourceのインスタンスが破棄されないように気をつける。インスタンスの破棄はdeinitで追える。

deinit {
    print("destroyed")
}

表示されるカードが全部グレーだと寂しいので、ランダムな色の背景を持ったUIViewを返すことにする。UIColorをランダムなFloatのRGBから作れば、ランダムな背景のUIViewが作れる。ランダムなFloatとUIColorを生成するメソッドを作って、koloda:viewForCardAtから呼び出す。

    func koloda(_ koloda: KolodaView, viewForCardAt index: Int) -> UIView {
        let view = UIView(frame: koloda.bounds)
        view.backgroundColor = randomColor()
        return view
    }
    func randomColor() -> UIColor {
        return UIColor(colorLiteralRed: randomFloat(), green: randomFloat(), blue: randomFloat(), alpha: 1)
    }
    func randomFloat() -> Float {
        return Float(Float(arc4random()) / Float(UInt32.max))
    }

できた

f:id:chikulla:20161130193005g:plain

全ソースはGitHub

github.com