MacアプリTunacan開発の技術的メモ

画像の高さを揃えて並べてつなげる、というだけの[Tunacan](https://itunes.apple.com/en/app/tunacan-image-concatenator/id980577198?mt=12)というMacアプリを作りました。
開発の過程で学んだ技術的内容のメモです。
iOSアプリをある程度かけるけど、Macアプリは初心者、という人向け。

## UI関連
### Drag & Drop
![tunacan-bridge-image-effect2.gif](https://qiita-image-store.s3.amazonaws.com/0/14097/fe76ad35-744d-31c5-f412-f7c0e0c31131.gif)


ドラッグアンドドロップはiOSアプリではあまり登場しないインターフェイスだが、Macの方には仕組みが用意されている。

`NSView`で`registerForDraggedTypes`を呼ぶと、どのようなタイプのドラッグアンドドロップを受け付けるかを指定できる。`awakeFromNib`内とかで指定。

```
self.registerForDraggedTypes([NSFilenamesPboardType])
```

`NSFilenamesPboardType`を指定するとFinderからのファイルのドラッグアンドドロップが可能に。他にも色とかも指定できる。

画像がビュー上にドラッグされると`draggingEntered`が呼ばれる。

```
override func draggingEntered(sender: NSDraggingInfo) -> NSDragOperation {
    return .Copy
}
```

ドラッグアンドドロップを許可する場合はこのメソッドで適切な値([NSDragOperation](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Protocols/NSDraggingInfo_Protocol/index.html#//apple_ref/c/tdef/NSDragOperation))を返す。対応しないものがドラッグされてきた場合には`None`を返すようにするべき。ドラッグされてきた画像をコピーして使う場合は`Copy`を、リンク的な使い方しかしない場合は`Link`を返す。

ドラッグ中にドラッグされた場所に応じて見た目を変更したい場合は`draggingUpdated`を使え、とのこと。

```
override func draggingUpdated(sender: NSDraggingInfo) -> NSDragOperation {
    println(sender.draggingLocation())
    return .Copy
}
```

ファイルがドロップされると、`prepareForDragOperation`、`performDragOperation`、`concludeDragOperation`が呼ばれる。`prepareForDragOperation`か`performDragOperation`で`false`を返すとドロップ処理はキャンセルされ、`concludeDragOperation`も呼ばれなくなる。対応していないファイルがドロップされた時はここで弾く。

```
override func performDragOperation(sender: NSDraggingInfo) -> Bool {
    let pasteboard = sender.draggingPasteboard()
        
    if contains(pasteboard.types as! [NSString], NSFilenamesPboardType) {
        let files: [String] = pasteboard.propertyListForType(NSFilenamesPboardType) as! [String]
        for file in files {
            if let url = NSURL(fileURLWithPath: file) {
                println(url)
            }
        }
    }
    return true
}
```

[公式ドキュメント](https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/DragandDrop/Tasks/acceptingdrags.html)によると、「`performDragOperation`の中で大まかなドラッグアンドドロップの処理をして、最後に`concludeDragOperation`でUI上に反映させる」というのが正しい実装方法っぽい。

* https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/DragandDrop/Tasks/acceptingdrags.html

### マウスイベント

#### mouseEnteredもmouseExitedも呼ばれない
マウス関連デリゲートメソッドとして、`mouseEntered`や`mouseExited`などがあるが、ただオーバーライドしてもこれらのメソッドは呼ばれない。

* [mouseEntered and mouseExited not called in NSImageView SubClass](http://stackoverflow.com/questions/11188034/mouseentered-and-mouseexited-not-called-in-nsimageview-subclass)
* [NSTrackingArea Class Reference
](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/NSTrackingArea_class/index.html)

マウスイベントを受け取るには、`TrackingArea`を設定してやる必要がある。

> An NSTrackingArea object defines a region of view that generates mouse-tracking and cursor-update events when the mouse is over that region.

`NSView`の`addTrackingArea:`メソッドでエリアを指定すると、そのエリアで発生したマウスイベントが通知されるようになる。`NSView`には`updateTrackingArea`メソッドが実装されており、このメソッドをオーバーライドしてトラッキングエリアを更新することができる。

```
var trackingArea: NSTrackingArea?
override func updateTrackingAreas() {
    if let trackingArea = trackingArea {
        self.removeTrackingArea(trackingArea)
    }
        
        
    let newTrackingArea = NSTrackingArea(rect: self.bounds, options: [.activeAlways, .mouseEnteredAndExited, .mouseMoved], owner: self, userInfo: nil)
    self.addTrackingArea(newTrackingArea)
    self.trackingArea = newTrackingArea
}
```

TrackingAreaのオプションには、通知されるイベントの種類やタイミング(アプリがアクティブな時だけ受け取る、とか)を指定可能。

### Live Window Resizing

> When the user resizes your window, the movement of the window should be smooth. If your code tries to do too much work during this time, the window movement may seem choppy and unresponsive to the user.

https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CocoaViewsGuide/Optimizing/Optimizing.html

Windowのリサイズ中に処理しすぎないこと。レスポンスが悪くなる。リサイズ中かどうかを`inLiveResize`メソッドで取得できるので、これがtrueの時は最小限の描画だけするようにする。 

### デフォルトボタン(青くする)

`key equivalent`でリターンを指定すると、デフォルトボタンになる。

* https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Button/Articles/MakingaButtontheDefaultButton.html

## グラフィック関連
### CALayerの再配置

#### NSView.layout()
constraint-based layout systemで表現できないようなレイアウトを実現するには、NSViewのlayoutメソッドをオーバーライドする。レイアウトが必要な場合は`layout()`を直接呼ぶのではなく、`self.needsLayout = true`を実行すること。

https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSView_Class/index.html#//apple_ref/occ/instm/NSView/layout

### CALayerのアニメーションの無効化

CALayerのtransformやopacityを変更すると、自動で変形のアニメーションが再生される。アニメーションを切ったり、長さを調整したい場合はlayerのプロパティをいじる前に`CATransition.setAnimationDuration`を呼ぶ。

```
CATransaction.setAnimationDuration(0)

imageLayer.frame = CGRectMake(CGRectGetMidX(workspace) - resizedWidth * 0.5, CGRectGetMidY(workspace) - resizedHeight * 0.5, resizedWidth, resizedHeight)
```

### CALayerにCIFilterをかける
![cifilter-on-calayer_1.gif](https://qiita-image-store.s3.amazonaws.com/0/14097/20bb41d0-0126-8bc4-6baf-64b8bf3071a2.gif)

`CALayer`の`filters`に`CIFilter`を追加すると、`CALayer`の表示内容にフィルターをかけることができる。

```
let renderingFilter = CIFilter(name: "CIGaussianBlur")
renderingFilter.name = "blurFilter"
imageLayer = CALayer()
imageLayer.filters = [
    renderingFilter
]
```

OS X 10.9以降でこの機能を使う場合は、`NSView`の方で`layerUsesCoreImageFilters`を有効にする必要がある。

```
self.layerUsesCoreImageFilters = true
```

フィルターのパラメータを後から変更したい場合は、`CALayer`の`setValue(forKeyPath:)`を使わないと反映されないので注意。

```
imageLayer.setValue(5, forKeyPath: "filters.blurFilter.inputRadius")
```



## その他

>  Instead, you should consider writing your own custom classes that can be managed by a higher-level NSView subclass. 
...

> A good example of when to use custom objects is a photo browser that displays thumbnail images of hundreds or even thousands of photos. Wrapping each photo in an NSView instance would both be prohibitively expensive and inefficient. Instead, you would be better off by creating a lightweight class to manage one or more photos and a custom view to manage that lightweight class.

複雑なUIを表現するときはカスタムビューをつくることを検討する。NSViewのインスタンス作りまくるとパフォーマンスが落ちる。

* https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CocoaViewsGuide/Optimizing/Optimizing.html