addSublayerで追加されたCALayerはビューのサイズ変更に追従しない問題の対応

それはグラデーションを扱ってるときに起きました:fearful:
どうもIBで作ったサイズのまま変わらないでグラデーションがかかっているのです・・・:bug:

# どんな状況？
まずは下の記事を参考に`GradationView`なるものを作りました。
[Swiftで背景にグラデーションを設定する](https://qiita.com/Gutenberg0x/items/ac737fb3305da3389bf9)

```swift:GradationView
import UIKit

class GradationView: UIView {
    
    @IBInspectable var topColor: UIColor = .clear
    @IBInspectable var bottomColor: UIColor = .white
    
    func setGradation() {
        let gradientLayer = CAGradientLayer()
        gradientLayer.frame.size = self.frame.size
        gradientLayer.colors = [self.topColor.cgColor, self.bottomColor.cgColor]
        self.layer.addSublayer(gradientLayer)
    }
}
```

そしてIB上で`GradationView`を配置し、IBOutletでVCに繋いで、`setGradation()`が呼ばれるようにします。
制約は画面幅いっぱいになるようにつけています。高さは適当に固定しています。
![スクリーンショット 2018-07-04 12.24.03.png](https://qiita-image-store.s3.amazonaws.com/0/84917/792a6afe-0a2f-e02a-2736-c7e8a1cecf10.png)

```swift:繋いだVC
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var gradationView: GradationView! {
        willSet {
            newValue.topColor = .red
            newValue.bottomColor = .blue
            newValue.setGradation()
        }
    }
    ...
}
```

これをまずはIB上のVCと同じ画面サイズの端末で実行してみます。
<img width="244" alt="スクリーンショット 2018-07-04 12.33.17.png" src="https://qiita-image-store.s3.amazonaws.com/0/84917/93f6782e-17f9-9932-6a0b-63dd57ec2318.png">
想定通り表示されました。

次に、IB上のVCより大きい画面サイズの端末で実行してみます。
<img width="234" alt="スクリーンショット 2018-07-04 12.35.09.png" src="https://qiita-image-store.s3.amazonaws.com/0/84917/e6380921-fcb6-5dfc-5450-317d26c815b6.png">
グラデーションが途中で切れてしまいました！View Hierarchyを見てみるとビュー自体は幅が伸びていることがわかります。
![スクリーンショット 2018-07-04 12.36.22.png](https://qiita-image-store.s3.amazonaws.com/0/84917/214ebc2e-270b-0010-64bc-9708127c9ed3.png)

# なぜ起こるの？
タイトルの通りですが、addSublayerで追加されたCALayerはビューのサイズ変更に追従してくれないみたいです。
ビューはAutoLayoutによって端末サイズに合わせてサイズが変更されますが、CALayerはそのまま残ります。

# 対応策
ググってみると3通り対応策がありました。
[CALayer And Auto Layout With Swift](https://marcosantadev.com/calayer-auto-layout-swift/)
詳細は上の記事を見てもらいたいのですが、ざっくりまとめると以下の通りです。
1. AutoLayoutの制約が効いたあとでCALayerの`frame`を更新する
2. ビューの`bounds`をKVOで監視し、変更があったらCALayerの`frame`を更新する
3. ビューのlayerをCAGradientLayerにしてしまう

今回私は#3の対応策を取ることにしました。
関心のあるCALayerが1つの場合はこれで対応できそうです。もし、複数ある場合は#1または#2で対応することになると思います。

```swift:改良したGradationView
import UIKit

class GradationView: UIView {
    
    @IBInspectable var topColor: UIColor = .clear
    @IBInspectable var bottomColor: UIColor = .white
    
    override class var layerClass: AnyClass {
        return CAGradientLayer.self
    }
    
    func setGradation() {
        guard let gradientLayer = self.layer as? CAGradientLayer else {
            return
        }
        gradientLayer.colors = [self.topColor.cgColor, self.bottomColor.cgColor]
    }
}
```

これで、無事グラデーションがビューのサイズ変更に追従するようになってくれました。

#まとめ
addSublayerで追加されたCALayerはビューのサイズ変更に追従してくれない。
関心のあるCALayerが1つだけの場合は、ビューのlayer自体を関心のあるCALayerに置き換えてしまうと良い。
関心のあるCALayerが複数の場合は、ビューのサイズ変更が起きた後のタイミングでCALayerのサイズ変更を行う。

# 終わりに
最初はなんで自動で変わってくれないんだよ、と思いましたが、よくよく考えてみればaddSubViewしたビューが親ビューにサイズ追従しないのと同じような話なんですかね:thinking:
しかしframeをいじるしかサイズ変更できないというのはイカしてないと思うので、レイヤーにもAutoLayoutがつけられるようになったらいいんじゃないかなーと思います:thumbsup:
なんだかAutoLayoutを使い始めた頃を思い出させる現象でした:smile_cat: