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 にアクセスするとこんなんが出る。
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
やったね。
国旗の使い方2
これの記事です。12/21日分
前回作った255枚の国旗のPDFをCocoaアプリケーションから使えるようにします。
PDFのロード
適当にiOSアプリケーションプロジェクトを作ります。Single View Applicationで。
使う
全部の.pdfファイルをAssets.xcassets
にドロップします。私のMacBook Airはここで固まります。
pdfを配置できたので、ImageView
を適当にViewに配置しましょう
コントローラとImageViewを関連づけます。
とりあえずセントビンセント及びグレナディーン諸島でも出しておきます。
ViewControllerのviewDidLoad()
でImageViewにセントビンセント及びグレナディーン諸島を設定します。
Assetsにあるイメージはコード保管でサムネイル出てきます。素敵ですね。
ビルドしてSVGからPDFに変えたセントビンセント及びグレナディーン諸島がImageViewでちゃんと表示できるか確認します。 軽いとはいえAssetが255もあるのでビルドにそれなりに時間がかかります。
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にするのはだめっぽいので、
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生成してもいいのですが、投稿タイムリミットが迫ってるので普通にコピペします。
CollectionViewで見せる
投稿のタイムr ので適当にUICollectionViewをレイアウトします。
ViewControllerを書きます。CollectionViewのnumberOfItemsInSection
でCountryKey.length()
を返します。
cellForItemAt
でCountryKey.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を返すと良しです。
ああ間に合った。
国旗の使い方1
これです
モチベーション
国旗って眺めてるだけで面白くないですか?アルゼンチンの
真ん中のやつとかなかなか良い表情をしてます。
じゃあこれはだーれだ?
はいウルグアイでしたー
じゃあ朴訥な彼は?
え?エクアドルっすよ!
こういう国旗の画像を集めてCocoaとかから使いたい。
収集
国連加盟国だけでも193あるので、一枚にかけるサイズは抑えつつ、国旗に細かく描かれていることを表現してもらいたい。ので、ベクターイメージを集めることにする。WikipediaのFlag_of_国名.svg
のファイルを引っ張ってくるんでもいいが、ググったら一発目で
見つけちゃった
これを使うことにします。
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です。
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
はいできた
ここまでできたらCocoaのAssetを追加してドラッグ&ドロップするだけですが、255ファイルあるんでいちいち追加するのは辛いです。次回は全部の国旗をCocoaからごそっと丸ごと使えるようにします。
Kolodaの使い方
アドベントカレンダー用の投稿
モチベーション
CocoaでTinderみたいなViewを提供するライブラリがある。
これを使いたい。使う。
Carthageの準備
Cocoaライブラリのパッケージマネージャとして、だいたいみんなCocoaPodsかCarthageを使う。 ここではシンプルで軽量なCarthageを使う。
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プロジェクトを作る。
作ったプロジェクトのルートディレクトリ(*.xcodeprojがあるとこ)にCartfile
という名前のファイルを作る。
今回は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.framework
とpop.framework
がCarthageディレクトリ以下にできてる。
Xcodeを開いて、プロジェクトルートのGeneralタブの一番下Linked Frameworks and Librariesに2つの.frameworkをドロップする。
Build Phasesタブを開いて、左の+
ボタン(Add a new build phase)を押し、New Run Script Phase
を選択する。
Run Script
のコマンドに
/usr/local/bin/carthage copy-frameworks
を書く。さらにInput Filesに
$(SRCROOT)/Carthage/Build/iOS/pop.framework $(SRCROOT)/Carthage/Build/iOS/Koloda.framework
を書く。popがKolodaの前提ライブラリなので、先頭に持ってこないとビルド失敗する。こんな感じになるはず。
なんでこんなスクリプトを設定しなきゃいけないのかというと、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
にする。
配置したKolodaViewをViewの真ん中らへんにレイアウトする。
ControllerとViewを繋げる
Main.storyboard
を開きながら、右上の円が二つ重なってるアイコンを押して、ViewController.swiftを表示する。ViewController.swiftにimport Koloda
を書いてKolodaをインポートする。
Main.storyboardのツリーからctrlを押しながらKoloda View
をクリックして、ViewController.swiftのviewDidLoad()
の上らへんにドラッグ&ドロップする。
ドロップしたらNameをkolodaView
にして、Connectを押す。
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: B
はJavaでいう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)) }
できた
全ソースはGitHubで