UITextViewのプレースホルダを追加したい


## 概要

![62f6bac6-b659-11e6-851a-39f199a063f1.png](https://qiita-image-store.s3.amazonaws.com/0/77778/8e0c337e-2a2f-a1f4-e3be-fedce1e6a4e1.png "62f6bac6-b659-11e6-851a-39f199a063f1.png")

UITextFieldはプレースホルダをつけられるけど、UITextViewはつけられないので

## 完成品

```swift
//
//  PlaceHolderedTextView.swift
//  PlaceHolderedTextView
//
//  Created by はるふ on 2016/11/29.
//  Copyright © 2016年 はるふ. All rights reserved.
//

import UIKit

@IBDesignable class PlaceHolderedTextView: UITextView {
    @IBInspectable var placeHolder: String = ""
    @IBInspectable var placeHolderColor: UIColor = .lightGray
    
    private var placeHolderLayer: CATextLayer?
    
    private func createPlaceHolderLayerIfNeed() {
        if placeHolderLayer == nil {
            let layer = CATextLayer()
            layer.fontSize = self.font?.pointSize ?? UIFont.systemFontSize
            layer.frame = CGRect(x: self.textContainerInset.left + self.textContainer.lineFragmentPadding, y: self.textContainerInset.top, width: self.frame.width, height: layer.fontSize+10)
            layer.string = self.placeHolder
            layer.foregroundColor = placeHolderColor.cgColor
            layer.contentsScale = UIScreen.main.scale
            layer.alignmentMode = kCAAlignmentLeft
            self.layer.addSublayer(layer)
            placeHolderLayer = layer
        }
    }
    
    private func removePlaceHolderLayerIfNeed() {
        placeHolderLayer?.removeFromSuperlayer()
        placeHolderLayer = nil
    }
    
    private func updateLayer() {
        // Observerから呼ばれるとmainじゃないかも？
        DispatchQueue.main.async {
            if self.text == nil || self.text.isEmpty {
                self.createPlaceHolderLayerIfNeed()
            } else {
                self.removePlaceHolderLayerIfNeed()
            }
        }
    }
    
    func onChangedText(_ notification: NSNotification?) {
        updateLayer()
    }
    
    // MARK: Observer関連
    
    private func addObserver() {
        updateLayer()
        NotificationCenter.default.addObserver(self, selector: #selector(self.onChangedText(_:)), name: NSNotification.Name.UITextViewTextDidChange, object: self)
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        addObserver()
    }
    
    override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
        addObserver()
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
}
```

## 設定方法

1. 新規ファイルを作成し、上のコードをコピペ
1. StoryboardにUITextViewを配置する
1. UITextViewを選択し、クラスを"PlaceHolderedTextView"に設定

以上でStoryboardから編集できるようになります

## 目指した理想形

探すとすぐに記事が出てきて、だいたい同じようなコードがあるが、
以下の点を鑑み、自分で作ろうと思った。


### コードはViewに完結させたい
ViewControllerからいじるほどでもないし、使い回しも考えると、Viewのコードだけで実現できる方が良い

### Observerを使いたくなかった（実現できなかった）
実現できなかったが・・・

```swift
override var text: String! {
    didSet {
        // ここでupdate
    } 
}
```

できるかと思ったら、できなかった。微妙なタイミングでしか呼ばれない。

delegateを使うと、ViewController側で使えなくなるので、使わなかった

うまく取れる方法があれば、教えていただきたい。

### UILabelまでいらない

わざわざUILabelほどリッチなものはいらないと思った。
極論、一つのViewに完結したコードなので、CoreTextでも良い。

今回はCATextLayerを使いました。

### drawでupdateされるコードが多い

これが結構よくわからなかったが、何故かdrawで更新するコードが多い。

```swift
func draw(_ rect: CGRect) {
}
```

でUILabelとかCALayerとかに変更を加えるのは、目的に合ってない。ここは、CoreTextなどを使った場合に描画する所（だと思う）

### @IBDesignable, @IBInspectable

Storyboardからプレースホルダの色・文字を選択できる方が良い

表示だけなら、ビルドした後は反映されるっぽい
(triggerとか設定しなくて良いっぽい)

### プレースホルダの位置

`self.textContainer.lineFragmentPadding`　（textViewの両端にある5pxぐらいのpadding）
`self.textContainerInset`（textViewの上下にある8pxぐらいのpadding）

を考慮する必要がある。

でも、これでも何故か下に1, 2pxぐらいずれている気がします・・・CATextLayerにpaddingがあるのかな？と思いますが、詳しい方いれば教えていただきたいです。

## おまけ：編集開始したら消す

色の設定によると思いますが、編集開始時にプレースホルダを消したい場合は `updateLayer()` を以下のように書き換えます

```swift
private func updateLayer() {
    // Observerから呼ばれるとmainじゃないかも？
    DispatchQueue.main.async {
        if (self.text == nil || self.text.isEmpty) && !self.isFirstResponder {
            self.createPlaceHolderLayerIfNeed()
        } else {
            self.removePlaceHolderLayerIfNeed()
        }
    }
}
```