SketchのiOS UI Design Templateを🈲で埋め尽くすプラグイン

# はじめに
そんなに真面目じゃないネタ記事です。
今回はタイトルのようなSketch Pluginを作ることでレイヤー操作や調査方法について学んでいきます。

## 作るプラグイン
実行するとレイヤーの背景をマル禁画像に書き換えます。

![kin.png](https://qiita-image-store.s3.amazonaws.com/0/61792/be58498d-c3d5-3c6a-b300-17a4f39d35ac.png)

以下は実際に実行したもの（今回の完成品）になります。
普通のレイヤーやシンボル、またレイヤー名も🈲に変わります。

![5a5d9011f1ec96064c90878bdf2248b7.gif](https://qiita-image-store.s3.amazonaws.com/0/61792/19816bbf-1249-5258-8012-deb9b786f3ec.gif)

🈲だとバカっぽいですが画像を「Confidential（機密）」にすればそれっぽい感じになりそうです。

![Confidential.png](https://qiita-image-store.s3.amazonaws.com/0/61792/d387f0ce-1070-a410-6963-9d2b855869cd.png)

![image2.gif](https://qiita-image-store.s3.amazonaws.com/0/61792/cf680fb4-bd27-9ef1-dde2-430410df760a.gif)

## 注意事項
* これは開発練習用のプラグインです。
* 大切なSketchファイルでは実行しないでください。
* 使用者の責任でご使用ください。
* 実行してもUndoすれば戻せます。

なお以下では、「レイヤーを🈲に変える」という表記が面倒なので**「Kinする」**という独自表記にさせていただきます。

# ソース
以下に置いてあります。画像ファイルもあります。
https://github.com/hanamiju/SketchConfidentialPlugin

なおこのエントリーでは細かくsketch pluginの作り方は示しません。[公式ドキュメント](http://developer.sketchapp.com/)等をご覧ください。
今回いじるファイルは以下のファイルのみです。

```Confidential.sketchplugin/Contents/Sketch/script.cocoascript ```

# 開発
## 画像を読み込む
まずはKin画像を読み込みます。
Cocoaの心得が少しあれば普通に理解できると思います。

### ソース
```js:script.cocoascript
    var plugin = context.plugin
    var imageFilePath = plugin.urlForResourceNamed("kin.png")         // 画像のURL取得
    var imageData = [NSData dataWithContentsOfURL:imageFilePath] // データに起こす
    var image = NSImage.alloc().initWithData(imageData)          // imageオブジェクトにする

```

### 解説
今回の.sketchpluginファイルは以下の構成になっています。

```
Confidential.sketchplugin
 └ Contents
    ├ Sketch
    │  ├ script.sketchscript
    │  └ manifest.json
    │
    └ Resource
       └ kin.png
```

```urlForResourceNamed()``` でResourseフォルダのファイル名を指定すると画像のURL取得が取得できるのでこれをもとに画像をオブジェクト化します。

## 選択したShapeLayerをKinする
選択したレイヤーを取得してKinをしていきます。
Sketchの操作でいう、レイヤーを選択してFillの方法をImageにしてKin.pngを入れるのと同じことをします。

<img width="979" alt="スクリーンショット_2016-12-17_21_22_24.png" src="https://qiita-image-store.s3.amazonaws.com/0/61792/f9cff74c-8f33-39ce-d529-5562f80e3978.png">


### ソース
```js:script.cocoascript
var onRun = function(context) {
    var plugin = context.plugin
    var imageFilePath = plugin.urlForResourceNamed("kin.png")
    var imageData = NSData.dataWithContentsOfURL(imageFilePath)
    var image = NSImage.alloc().initWithData(imageData)

    changeLayers(context, image)
};

function changeLayers(context, image) {
    var selections = context.selection  // 選択したレイヤー
    // 選択したレイヤー分だけKinする
    change(selections, image)
}

function change(layers, image) {
	for(var i = 0; i < layers.length; i++){
		var layer = layers[i];
		if(layer.class() == MSShapeGroup){
		    var fill = layer.style().fills().firstObject();
		    if (fill != nil) {
				fill.setFillType(4);  // 塗りつぶし方法（画像）
				fill.setImage(MSImageData.alloc().initWithImage_convertColorSpace(image, false));
				fill.setPatternFillType(3);  // 塗りつぶしパターン（レイヤーにFit）
			}
		}
	}
}

```


### 解説
#### changeLayers関数
```context.selection``` で現在選択されているレイヤーが取得できます。

#### change関数
レイヤーは```MSShapeGroup```というクラスなのでレイヤーにこいつが入っていたらKinしていきます。
ちなみに、ソースのコメントに書いてある塗りつぶし情報/塗りつぶしパターンはSketch.appでいう以下の選択項目です。

![スクリーンショット_2016-12-17_16_45_39.png](https://qiita-image-store.s3.amazonaws.com/0/61792/71c1d770-db65-c78e-d541-9ee93cd44e65.png)

![スクリーンショット_2016-12-17_16_45_58.png](https://qiita-image-store.s3.amazonaws.com/0/61792/0f630be6-4ec7-2b6b-69e3-f86a09a50872.png)


### 結果
![b5f65368b4432b6a685b66dd7eaf8522.gif](https://qiita-image-store.s3.amazonaws.com/0/61792/76997a70-5849-5740-8976-d473cd610aa4.gif)

レイヤー単体ではKinできましたが、グループレイヤーには適用できていないようです。

## 選択したグループをKinする
グループにもKinを対応させていきます。

### ソース
```js:script.cocoascript
// onRun(), changeLayers()の記載は省略

function change(layers, image) {
	for(var i = 0; i < layers.length; i++){
		var layer = layers[i];
		if(layer.class() == MSShapeGroup){
		    var fill = layer.style().fills().firstObject();
		    if (fill != nil) {
				fill.setFillType(4);
				fill.setImage(MSImageData.alloc().initWithImage_convertColorSpace(image, false));
				fill.setPatternFillType(3);
			}
		// 追加
		} else if (isGroup(layer)) {
			change(layer.layers(), image)
		} 
	}
}

// 追加
var isGroup = function(layer){
    return layer.isMemberOfClass(MSLayerGroup.class())
};
```

### 解説

```isGroup``` でレイヤーがグループ(MSLayerGroup)かどうかを判別する処理を実装します。
MSLayerGroupオブジェクトはプロパティに```layers```という子レイヤーを持っているのでこれをまた```change```関数に投げます。

### 結果
![group.gif](https://qiita-image-store.s3.amazonaws.com/0/61792/8c9c9924-ea33-4419-176c-53964f33f06c.gif)

グループにもKinできましたが、まだうまく動いていないようです。
動いていないのはシンボルです。シンボルはgroupとは別のやり方でKinしてあげないといけなそうです。

## 選択したシンボルをKinする
次はレイヤーがシンボルだった時の処理を書きたいです。
シンボルはグループとは構成が違うようで子レイヤーのプロパティが見当たりませんでした。

しょうがないので**シンボルをグループに強制的に戻します。**
そして、グループレイヤーに対してKinします。

### ソース
```js:script.cocoascript
// onRun(), changeLayers(), isGroupの記載は省略

function change(layers, image) {
	for(var i = 0; i < layers.length; i++){
		var layer = layers[i];
		if(layer.class() == MSShapeGroup){
		    var fill = layer.style().fills().firstObject();
		    if (fill != nil) {
				fill.setFillType(4);
				fill.setImage(MSImageData.alloc().initWithImage_convertColorSpace(image, false));
				fill.setPatternFillType(3);
			}
		} else if (isGroup(layer)) {
			change(layer.layers(), image)
		// 追加
		} else if (isSymbol(layer)) {
			changeSymbolInstance(layer, image)
		}
	}
}

// 追加
function changeSymbolInstance(symbolInstance, image) {
	// シンボルをグループレイヤーに直す
	var layer = symbolInstance.detachByReplacingWithGroup()
	change(layer.layers(), image)
}

// 追加
var isSymbol = function(layer){
    return layer.isMemberOfClass(MSSymbolInstance.class())
};
```

### 解説
シンボルはMSSymbolInstanceクラスなので```isSymbol``` でレイヤーがMSSymbolInstanceクラスか判断します。
そして```MSSymbolInstance.detachByReplacingWithGroup()```でシンボルをグループに戻します。
グループに直したものを```change```関数でKinします。

### 結果

![symbol.gif](https://qiita-image-store.s3.amazonaws.com/0/61792/f7ce43d6-6825-81d3-5e50-fddd59947d95.gif)

これでシンボルも**無理矢理**Kinできました。
いい感じになってきたのでテキストもKinにしていきましょう。

## 選択したTextLayerをKinする
テキストは画像に変えられないので、文字を「🈲」に変えてしまいましょう。

### ソース

```js:script.cocoascript

function change(layers, image) {
	for(var i = 0; i < layers.length; i++){
		var layer = layers[i];
		if(layer.class() == MSShapeGroup){
		    var fill = layer.style().fills().firstObject();
		    if (fill != nil) {
				fill.setFillType(4);
				fill.setImage(MSImageData.alloc().initWithImage_convertColorSpace(image, false));
				fill.setPatternFillType(3);
			}
		// 追加
		}　else if (layer.class() == MSTextLayer) {
			layer.stringValue = "🈲"
		}  else if (isGroup(layer)) {
			change(layer.layers(), image)
		} else if (isSymbol(layer)) {
			changeSymbolInstance(layer, image)
		}
	}
}
```

### 解説
テキストレイヤーのクラスはMSTextLayerです。こいつがレイヤーできたら```stringValue```プロパティにKinします。

### 結果

![text.gif](https://qiita-image-store.s3.amazonaws.com/0/61792/adc998f4-db2d-a47b-ccd5-60f1326c5033.gif)

テキストもKinできました。
ここまでくるとサイドバーのレイヤー名もKinしたいところです。

## 選択したレイヤー名をKinする
レイヤー名もテキストと同じく文字を「🈲」に変えます。

### ソース

```js:script.cocoascript

function change(layers, image) {
	for(var i = 0; i < layers.length; i++){
		var layer = layers[i];
		// 追加
		layer.setName("🈲")
		if(layer.class() == MSShapeGroup){
		    var fill = layer.style().fills().firstObject();
		    if (fill != nil) {
				fill.setFillType(4);
				fill.setImage(MSImageData.alloc().initWithImage_convertColorSpace(image, false));
				fill.setPatternFillType(3);
			}
		}　else if (layer.class() == MSTextLayer) {
			layer.stringValue = "🈲"
		}  else if (isGroup(layer)) {
			change(layer.layers(), image)
		} else if (isSymbol(layer)) {
			changeSymbolInstance(layer, image)
		}
	}
}
```

### 解説
レイヤーの```setName()```メソッドにKinをするだけです。

### 結果
![layername.gif](https://qiita-image-store.s3.amazonaws.com/0/61792/99fd49ff-6dd3-a455-dd10-40304d266422.gif)


ここまでくるといよいよ**狂気じみてきました。**

今まではわざわざ選択したレイヤーに対してのみKinしてきましたが、いちいち選択するのもかったりいので次はpage内の**全レイヤーを強制的にKin**していきます。

## 選択していないLayerもKinする

現在のpageのすべてのlayerが取れるのでこれをKinすればいちいち選択することもなさそうです。

### ソース

```js:script.cocoascript
function changeLayers(context, image) {
	var doc = context.document
	// 変更
	var layers = doc.currentPage().layers()
	change(layers, image)
}
```

### 解説
なし

### 結果

![currerntpage.gif](https://qiita-image-store.s3.amazonaws.com/0/61792/396df6b2-8c87-617a-c9ff-914ebfdd2df8.gif)

壮観ですね。

# 宿題
他にも色々Kinしてみたい感じではありますが、これより先は僭越ながらここまでお読みいただいた方への宿題とさせていただきたいです。

* SketchファイルすべてのpageをKinする: 10点
* ShapeLayerのBorderを全て赤に変える: 5点
* 画像Layerがあったら、画像Layer削除して同じ位置に同じサイズのLayerを作り、それをKinする: 30点
* Undoできなくさせる（UndoManagerを見つけ出してUndoをリセットすればできるはず...）: **50点**

↑のいずれかでもできましたらプルリクいただけると嬉しいです。

https://github.com/hanamiju/SketchConfidentialPlugin


# プロパティ/メソッドの調査方法

Sketchの独自クラスを駆使していかないと、Kinがうまくできないのですが、[公式リファレンス](http://developer.sketchapp.com/reference/api/class/api/Document.js~Document.html)を見てもあまり詳しい情報が載っていません。

そこでSketch.appのヘッダーを見て良さそうなプロパティ/メソッドを探します。
[class-dump](http://qiita.com/littlebusters/items/b919693f4f3d4c183ce0#class-dump)する方法もありますが、私は面倒なので[Sketch-Headers](https://github.com/abynim/Sketch-Headers)にお世話になっています。

Sketch-Headersはclass-dumpしたヘッダーファイル群のリポジトリです。
割と頻繁に更新されています。

## 例
シンボルをKinした時に必要だった```MSSymbolInstance.detachByReplacingWithGroup()```をどう調べたかの例です。

### レイヤーのクラス名を調べる
最初の時点ではMSSymbolInstanceクラスがあることも知りません。
まずKinできなかったレイヤーのクラス名を調べます。
普通に以下のようにログを仕掛けてプラグインを実行します。

```js
log(layer)
```

コンソールを確認すると以下のように表示されます。

```objc
2016/12/17 18:11:02.025 Confidential (Sketch Plugin)[2158]: <MSSymbolInstance: 0x7f9a81041c00> hoge (BAFD957A-AAE2-4D58-B330-792B6CEECEB7)
```
ここでMSSymbolInstanceクラスの存在を知ります。

### Sketch-Headersを見る
クラス名がわかったので[Sketch-Header](https://github.com/abynim/Sketch-Headers/blob/master/Headers/MSSymbolInstance.h)を見ます。
ここで子レイヤーの情報あるかなとか、それっぽい関数ないかなと調べて色々試します。

その結果、今回は```detachByReplacingWithGroup()```使って**シンボルをぶっ壊そう**という結論になりました。

# むすび
Kinできるレイヤーが増えるということは、あなたがPluginで操作できる領域が増えるということです。ぜひ、あらゆるSketchファイルをKinしてPlugin力を上げていってください。


# 参考
* [Sketch Developer](http://developer.sketchapp.com/)
* [Sketch-Headers](https://github.com/abynim/Sketch-Headers)
* [Sketch.app Plugin の開発メモ - Qiita](http://qiita.com/littlebusters/items/b919693f4f3d4c183ce0)