[iOS]  Swift時代の! コードによるViewレイアウトのベストプラクティス

最近は`AutoLayout`を使う機会も増えてきましたが、メンテナンス性や書きやすさを考えると、(Non-AutoLayoutな）コードでViewのレイアウトを行うほうがメリットがあるケースも、多々あります。

また基本は`AutoLayout`でレイアウトを行いつつも、画面内の特定の動的な`subview`だけは`frame`を触りながらコードベースで行いたいような場面もあるでしょう。

なので普段は`AutoLayout`がメインな方も、使わない場合のレイアウト方法を知っておいて損はないと思います。

この記事では`subview`の`frame`をコード上で触りながらレイアウトを行う場合の、方針や注意点についてまとめています。

## ViewController内でsubviewのレイアウトを行う場合

青い矩形(blueRect)と、赤い矩形(redRect)を`UIView`で作って画面の左右に並べるだけの処理をControllerに書いてみます。

### Controllerの全体像

まず最初に、最終的なControllerのソースを貼ります。
その後で部分ごとに解説していきます。
Controller内でViewのレイアウトを処理を行うコードは以下のようになります。

```swift
import UIKit

// 青い矩形(blueRect)と、赤い矩形(redRect)を`UIView`で作って画面の左右に並べるだけController
class ViewController: UIViewController {
    
    // MARK: - Properties -
    lazy private blueRect:UIView = self.createBlueRect()
    lazy private var redRect:UIView  = self.createRedRect()
  
    // MARK: - Life cycle events -
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.addSubview(blueRect)
        self.view.addSubview(redRect)
        // - UIButtonなどにイベントを仕込みたい場合もここで行う
        // - ViewにModelのデータ等の外部データを渡したい場合もここで行う
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        self.layoutBlueRect()
        self.layoutRedRect()
    }
    
    // MARK: - Create subviews -    
    private func createBlueRect() -> UIView {
        let rect = UIView(frame: CGRectZero)
        rect.backgroundColor = UIColor.blueColor()
        return rect;
    }
    
    private func createRedRect() ->UIView {
        let rect = UIView(frame: CGRectZero)
        rect.backgroundColor = UIColor.redColor()
        return rect;
    }
    
    // MARK: - Layout subviews -
    private func layoutBlueRect() {
        blueRect.frame.size = CGSizeMake(50,50)
        blueRect.center.x = self.view.center.x
        blueRect.center.y = self.view.center.y
    }
    
    private func layoutRedRect() {
        redRect.frame.size = CGSizeMake(50,50)
        redRect.frame.origin.x = self.view.frame.size.width - redRect.frame.size.width
        redRect.center.y = self.view.center.y
    }

}
````

では上のソースを部分ごとに見て行きましょう。


### subviewをlazyなプロパティとして宣言


プロパティの宣言は以下のように行っています。

```swift
lazy private var blueRect:UIView = self.createBlueRect()
```

`lazy`を付けて子Viewのプロパティを宣言しています。`lazy`宣言することによって、プロパティが最初に利用されたタイミングで１度だけ`createBlueRect`が実行され結果が変数の中に実行されます。

２度目以降にプロパティが呼ばれる時は、格納された値が利用されるので、`createBlueRect`メソッドの中に初期化処理を書いておけば、それが１度だけ実行されることが保証されます。

( 注:ただ、このように宣言するとnilに戻すこともできないため、`didReceiveMemoryWarning`での解放などを考慮する場合は`lazy`ではなく`get/set`などを利用して遅延ロードを書くのが良いです )

また今回の例では`View`の初期化メソッドは全て`createXXX`といった名前にしています。
`createBlueRect`の中身の初期化処理は以下のようになっています。

```swift
    private func createBlueRect() -> UIView {
        let rect = UIView(frame: CGRectZero)
        rect.backgroundColor = UIColor.blueColor()
        return rect;
    }
````

`createBlueRect`では上のように初期化処理のみを行って、`layout`に関する処理は行っていません。
各Viewの`frame`に関するレイアウト処理は、`createXXXX`の中で行う<b>べきではありません</b>
なぜなら、各viewの`frame`は画面の向き、`self.view`の状態など外部要素よって変わるものである（場面が多い）ため、
それらが不定な初期化中には、依存する処理を書くことができないからです。

#### viewDidLoadの処理

`viewDidLoad`は`loadView`が実行された後、つまり`self.view`が読み込み終わったタイミングで呼ばれます。

`viewDidLoad`の中では子Viewと他Viewの関連する処理やイベント登録などを行います。
今回の以下の例では`addSubview`のみを行っていますが、`UIButton(UIControl)#addTarget`などを行う場合も、ここで行うと良いでしょう。

```swift
   override func viewDidLoad() {
        super.viewDidLoad()
        self.view.addSubview(blueRect)
        self.view.addSubview(redRect)
        // - UIButtonなどにイベントを仕込みたい場合もここで行う
        // - ViewにModelのデータ等の外部データを渡したい場合もここで行う
    }
````

ここでも`self.frame`を利用するような各`View`のレイアウト処理は行う<b>べきではない</b>ことに注意してください。

なぜなら`viewDidLoad`が呼ばれるのは`self.view`の読み込み後の１度のみだからです。ここに`self.frame`に依存したレイアウト処理を
書いてしまうと、最初の読み込み時にレイアウトが固定されてしまい、画面の向きの変化など`self.view`の再レイアウトがあっても、子Viewのレイアウトが行われなくなってしまいます。

また`AutoLayout`を使っている`View`は、この時点では`frame`のレイアウトがなされていません。なので、それらを利用した処理もここでは書くことはできません。

よって、`self.view`や「AuotLayoutなView」に依存しているレイアウト処理は以下の`viewDidLayoutSubviews`の中で行いましょう。

### viewDidLayoutSubviews

`viewDidLayoutSubviews`はControllerの表示、画面の向きの変更、ナビゲーションバーの表示、様々な理由で`self.view`の`frame`が更新された時に呼ばれます。

また`AutoLayout`を使ってる場合は、この時点で`AutoLayout`によるframeの更新が終わっています。なので`AutoLayout`でレイアウトされた`View`に依存した処理もここに書いても問題ありません。

なので、ここが子View(subviews)のレイアウトを、行う場所として適切です。
今回の例では以下のような実装となっています、

```swift
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        self.layoutBlueRect()
        self.layoutRedRect()
    }

    ....

    private func layoutBlueRect() {
        // 画面左端（縦は中央）に配置
        blueRect.frame.size = CGSizeMake(50,50)
        blueRect.center.x = self.view.center.x
        blueRect.center.y = self.view.center.y
    }
    
    private func layoutRedRect() {
        // 画面右端（縦は中央）に配置
        redRect.frame.size = CGSizeMake(50,50)
        redRect.frame.origin.x = self.view.frame.size.width - redRect.frame.size.width
        redRect.center.y = self.view.center.y
    }
````

`subview`のframeを触る処理は煩雑になることも多いため、`layoutXXXX`という名前の別メソッドに切り出しています。またSwiftでは構造体の値を直接上書きできるため、 `redRect.center.y = 10`という書き方ができるので良いですね。(`Objective-C`では、このコードはエラーになっていました)

さて、これで、`self.view`が変化するごとに合わせて、`subviews`も変化するようになりました。
試しに上記のコードを動かしてみてください。画面を回転させて`self.frame`のサイズが変化したとしても、子Viewは左右の両端に配置されたままになっているはずです。

上の例はシンプルなので見やすいですが、複雑なレイアウト処理をControllerに書いていくと、Controller自体が膨らんでいってしまいます。

もっと複雑な初期化処理やレイアウト処理がある場合、例えば子Viewがさらに子Viewを持つケース(孫View)などがある場合は`View`を`CustomView`に切り出しすことを検討したほうが良いでしょう。

そこで、次にレイアウト処理も含めて`View`の処理を`CustomView`に切り出した場合のレイアウト方法について説明します。

## CustomViewに切り出す場合のケース

CustomViewでのレイアウトについて考えてみます。
今回は例としてタイトル文字(UILabel)と、送信ボタン(UIButton)の２つのsubviewで構成されるCustomViewを考えてみましょう。 またタイトル文字(UILabel)の中の文字列は外から渡せるようにします。

### CustomViewの全体のコード

Controller同様、最初に全体コードを貼っておきます。
その下で各部分の詳細について説明していきます。

~~~swift
import UIKit

class CustomView: UIView {
    
    // MARK: - Properties -
    lazy private var titleLabel:UILabel = self.createTitleLabel()
    lazy private var submitButton:UIButton = self.createSubmitButton()
    
    var title:String? {
        get {
            return titleLabel.text
        }
        set(title) {
            titleLabel.text = title
            self.setNeedsLayout()
        }
    }

    // MARK: - Life cycle events -
    required override init(frame: CGRect) {
        super.init(frame: frame)
        self.commonInit()
    }

    required init(coder aDecoder: NSCoder) {
        super.init(coder:aDecoder)
        self.commonInit()
    }
    
    private func commonInit() {
        self.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
        self.addSubview(titleLabel)
        self.addSubview(submitButton)
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        self.layoutTitleLabel()
        self.layoutButton()
    }
    
    // MARK: - Create subviews -
    private func createTitleLabel() -> UILabel {
        let label = UILabel(frame: CGRectZero)
        label.font = UIFont.systemFontOfSize(12)
        label.textColor = UIColor.blackColor()
        return label
    }
    
    private func createSubmitButton() -> UIButton {
        let button = UIButton.buttonWithType(UIButtonType.System) as UIButton
        button.setTitle("送信ボタン", forState: UIControlState.Normal)
        return button
    }
    
    // MARK: - Layout subviews -
    private func layoutTitleLabel() {
        titleLabel.sizeToFit()
        titleLabel.center.x = self.frame.size.width/2
        titleLabel.frame.origin.y = 10
    }
    
    private func layoutButton() {
        submitButton.sizeToFit()
        submitButton.center.x = self.frame.size.width/2
        submitButton.frame.origin.y = CGRectGetMaxY(titleLabel.frame) + 10
    }
    

}
~~~~

### プロパティに関して

以下のsubviewを`lazy`で宣言する部分はControllerと同じです。

```swift
    lazy private var titleLabel:UILabel = self.createTitleLabel()
    lazy private var submitButton:UIButton = self.createSubmitButton()
````

今回、外からタイトル文言を指定できるようにしたいので、`title`を`get/set`を利用して以下のように宣言しています。

```swift
    var title:String? {
        get {
            return titleLabel.text
        }
        set(title) {
            titleLabel.text = title
            self.setNeedsLayout()
        }
    }

````

`get/set`で`titleLabel.text`の値にアクセスしています。
これにより、customViewの外から`titleLabel`の構造を知ることなくtitle文言だけを更新できます。

### 初期化処理に関して

`init`が`coder/frame`どちらの引数からでも共通の処理が呼ばれるように`commonInit`というメソッドを作って、その中で初期化処理を定義しています。

```swfit
    private func commonInit() {
        self.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
        self.addSubview(titleLabel)
        self.addSubview(submitButton)
    }
````

処理の中身は、自分自身の初期化と、subviewの追加のみです。
Controllerの時と同様、この中で`self.frame`に依存した`subview`のレイアウト処理は書くべきではありません。

なぜなら`View`の`frame`は、画面の回転などによって変化することがあるからです。`init`内に`subview`のレイアウト処理を書いてしまうと`self.frame`が変化しても、再レイアウトが行われなくなってしまいます。

`CustomView`におけるsubviewのレイアウトは`layoutSubviews`の中で行いましょう。

### layoutSubviews

`layoutSubviews`は、`View`の`frame`が変化した時に自動的に呼び出されます。(自分自身で呼び出すことは非推奨です）
なのでここに、subviewのレイアウトを処理を書いておけば、`view`の状態に合わせて、必要な時にレイアウト処理が行われます。

今回の実装は以下のようにしていて、Controllerの時、同様に要素ごとに`layoutXXXX()`というメソッドに切り出しています。

```swift
    override func layoutSubviews() {
        super.layoutSubviews()
        self.layoutTitleLabel()
        self.layoutButton()
    }
````

以上の実装で、`view.frame`のサイズが変わっても、都度適切にレイアウトが行われる`CustomView`が出来ました。

### Controllerで作ったCustomViewを利用する際のコード

上で作ったCustomViewをControllerで利用する例は以下のようになります。

```swift
import UIKit

class ViewController: UIViewController {

    // MARK: - Properties -
    lazy private var customView:CustomView = self.createCustomView()
  
    // MARK: - Life cycle events -
    override func viewDidLoad() {
        super.viewDidLoad()       
        customView.title = "タイトルです"
        self.view.addSubview(customView)
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        self.layoutCustomView()
    }
    
    // MARK: - Create subviews -
    private func createCustomView() -> CustomView {
        let rect = CustomView(frame: CGRectZero)
        return rect;
    }
    
    // MARK: - Layout subviews -
    private func layoutCustomView() {
        customView.frame.size = CGSizeMake(180,80)
        customView.center = self.view.center
    }

}
````

Controllerでは`customView`の`frame`だけを触って、`customView`自身が持つ要素のレイアウトは`customView#layoutSubviews`に任せていることがわかります。
また`viewDidLoad`の中で`customView.title`をセットしています。

### まとめ

- `UIViewController#view`が持つ`subview`のレイアウトは`viewDidLayoutSubviews`の中で行う
- `CustomView`が持つ`subview`のレイアウトは`layoutSubviews`の中で行う

以上のように書けば`AutoLayout`によるレイアウトが混在していても正常にレイアウトが行われます。

実際の場面では今回のようなシンプルな例では全て`AutoLayout`で書くほうが簡単にできると思いますが、動的な要素が多い場面等などで、この記事を参考にしてしてみてください！