【iOS】Pathの描画をパーセンテージで指定する

## まず始めに

Pathをアニメーションで描画するのではなく、パーセンテージで描画する方法について書こうと思います。
パーセンテージ指定でPathの描画をさせると、以下のGIFアニメーションのようにスライダーと連動して描画できるようになったりもします。
![path.gif](https://qiita-image-store.s3.amazonaws.com/0/60325/f6eeacd1-3d0f-5b32-edd3-49a0f1d6eeac.gif "path.gif")

## どのように実装しているのか

実装する上でのポイントは以下のようになります。
- `CAShapeLayer`のインスタンスを`UIView`の`drawRect`で描画する
- `CAShapeLayer`の`strokeEnd`を使ってパーセンテージ指定できるようにする

``` swift
class PathStokeAdjustableView: UIView { 
    private let shapeLayer = CAShapeLayer()
    
    private var _stroke: CGFloat = 0 {
        didSet {
            shapeLayer.strokeEnd = _stroke
        }
    }
    var stroke: CGFloat {
        set {
            _stroke = max(0, min(1, newValue))
        }
        get {
            return _stroke
        }
    }
    
    //MARK: - XibやStroyboardから呼ばれた時
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
    override func awakeFromNib() {
        super.awakeFromNib()
        layer.addSublayer(shapeLayer)
        shapeLayer.strokeEnd = 0
        layoutIfNeeded()
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        setNeedsDisplay()
    }
    
    //MARK: - コードで生成された時
    init() {
        super.init(frame: .zero)
        layer.addSublayer(shapeLayer)
        shapeLayer.strokeEnd = 0
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        layer.addSublayer(shapeLayer)
        shapeLayer.strokeEnd = 0
    }
    
    //MARK: - Pathの描画
    override func drawRect(rect: CGRect) {
        UIColor.whiteColor().setFill()
        shapeLayer.lineWidth = 10
        shapeLayer.strokeColor = UIColor.blackColor().CGColor
        shapeLayer.fillColor = UIColor.whiteColor().CGColor
        shapeLayer.path = path().CGPath
    }
}

extension PathStokeAdjustableView {
    func path() -> UIBezierPath {
        let path = UIBezierPath()
        path.moveToPoint(CGPoint(x: CGRectGetMinX(bounds), y: CGRectGetMinY(bounds)))
        path.addLineToPoint(CGPoint(x: CGRectGetMaxX(bounds), y: CGRectGetMinY(bounds)))
        path.addLineToPoint(CGPoint(x: CGRectGetMaxX(bounds), y: CGRectGetMaxY(bounds)))
        path.addLineToPoint(CGPoint(x: CGRectGetMinX(bounds), y: CGRectGetMaxY(bounds)))
        path.addLineToPoint(CGPoint(x: CGRectGetMinX(bounds), y: CGRectGetMinY(bounds)-5))
        return path
    }
}
```

`Storyboard`には`UIView`と`UISlider`を適当に追加してください。

![stroyboard.png](https://qiita-image-store.s3.amazonaws.com/0/60325/53ffd073-467e-2a72-309b-9207635c8774.png "stroyboard.png")

この後、`PathStokeAdjustableView `を`Storyboard`の中で指定するか、`ViewController`の中で`addSubView`するかによって書き方が少し変わってきます。

#### `Storyboard`で指定する場合

以下のスクリーンショットのように、追加した`UIView`のCustom Classを`PathStokeAdjustableView `に指定してください。

![screen.png](https://qiita-image-store.s3.amazonaws.com/0/60325/7a192026-b21b-41f9-a761-5877d8cb755b.png "screen.png")

`ViewController`自体の実装は`Slider`の値を受け取ったとき、`PathStokeAdjustableView `のインスタンスに`stroke`の値として受け取ったものを渡すだけです。

```swift
class ViewController: UIViewController {
    @IBOutlet weak var pathView: PathStokeAdjustableView!
    
    @IBAction func sliderValueChanged(sender: AnyObject) {
        guard let slider = sender as? UISlider else {
            return
        }
        pathView.stroke = CGFloat(slider.value)
    }
}
```

#### `ViewController`で生成する場合

- `Storyboard`で追加した`UIView`のCustom Classは変更せず、`UIView`のままにしてください。
- 上記の`UIView`を`pathContainerView `として使用します。
- `PathStokeAdjustableView`のインスタンスを生成して、`AutoLayout`などを使用し`pathContainerView `に`addSubview`してください。
- `stroke`へ値を渡す実装に違いはありません。

```swift
class ViewController: UIViewController {
    @IBOutlet weak var pathContainerView: UIView!
    private let pathView =  PathStokeAdjustableView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        pathView.translatesAutoresizingMaskIntoConstraints = false
        pathContainerView.addSubview(pathView)
        pathContainerView.addConstraints([
            NSLayoutConstraint(item: pathView, attribute: .Top, relatedBy: .Equal, toItem: pathContainerView, attribute: .Top, multiplier: 1, constant: 0),
            NSLayoutConstraint(item: pathView, attribute: .Right, relatedBy: .Equal, toItem: pathContainerView, attribute: .Right, multiplier: 1, constant: 0),
            NSLayoutConstraint(item: pathView, attribute: .Left, relatedBy: .Equal, toItem: pathContainerView, attribute: .Left, multiplier: 1, constant: 0),
            NSLayoutConstraint(item: pathView, attribute: .Bottom, relatedBy: .Equal, toItem: pathContainerView, attribute: .Bottom, multiplier: 1, constant: 0)
        ])
    }
    
    @IBAction func sliderValueChanged(sender: AnyObject) {
        guard let slider = sender as? UISlider else {
            return
        }
        pathView.stroke = CGFloat(slider.value)
    }
}
```

## 注意点
Pathの`stroke`には対応していますが、`fill`には対応していません。

## 最後に
Pathをパーセンテージ指定して描画できるようにすることによって、`UISlider`の値に合わせるだけでなく、`UITableView`などのスクロールに合わせて描画したりもできるようになります。