SwiftのMVCアーキテクチャにおけるコードレイアウトのベストプラクティスを模索&解説してみた

どうも〜〜〜〜
横文字まみれでルー大柴みたいなタイトルですね。

iOS開発でアーキテクチャ何使ってますか？
僕はもっぱらMVCです。

先日、趣味でフレームワークを入れて遊んでいたのですが、Xibを使用すると警告が出る為コードレイアウトで実装したれ！と思ったわけです。

と言うことでサンプル作りました。
ボタン押すと遷移してテーブルセルの機種のかな文字を表示します。

![Animated GIF-downsized_large.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415441/7e0da180-0f9c-63cf-bec9-84b6d8a03feb.gif)


githubに対象プロジェクトあげてます。
[ぎっとはぶ](https://github.com/pinoerumo/SwiftMVCCodeLayout)



#コードレイアウトで実装する上で何に困ったか
SwiftでMVC実装する場合大体UIViewクラスにXibをくっ付けて実装するので描画関係はほとんど気にしなくて済む訳です。

ですが、コードレイアウトの場合どこでレイアウトを指定したら良いのかが読む文献によって違うわけです。

と言うことでコードレイアウトでの実装方法を考えてみました。
一つ一つ解説するので全体のコードは読み飛ばしてもらっていいです。

```swift:SMCLLabelView.swift
class SMCLLabelView: UIView {
    weak var delegate: SMCLLabelViewDelegate?
    /**デバイスのかな情報を表示するラベル*/
    var displayLabel = UILabel()
    
    
    override init(frame: CGRect) {
      super.init(frame: frame)
      setLayout()
    }
    
    
    @available(*, unavailable)
    required init?(coder: NSCoder) {
      fatalError("init(coder:) has not been implemented")
    }
    
    /// レイアウトを設定します
    func setLayout() {
        setBackView()
        setTransitionButton()
    }
    
    
    /// 背景ビューを生成します
    func setBackView() {
        let backView: UIView = {
            let backView = UIView()
            backView.translatesAutoresizingMaskIntoConstraints = false
            backView.backgroundColor = .white
            return backView
        }()
        self.addSubview(backView)
        backView.topAnchor.constraint(equalTo: self.topAnchor, constant:0).isActive = true
        backView.leftAnchor.constraint(equalTo: self.leftAnchor, constant:0).isActive = true
        backView.rightAnchor.constraint(equalTo: self.rightAnchor, constant:0).isActive = true
        backView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant:0).isActive = true
    }
    
    
    /// 次の画面へ遷移するボタンを生成します
    func setTransitionButton() {
        let transitionButton: UIButton = {
            let transitionButton = UIButton()
            transitionButton.translatesAutoresizingMaskIntoConstraints = false
            transitionButton.setTitle("次の画面へ遷移", for: .normal)
            transitionButton.setTitleColor(.blue, for: .normal)
            transitionButton.addTarget(self, action: #selector(tappedTransitionButton), for: .touchUpInside)
            return transitionButton
        }()
        self.addSubview(transitionButton)
        transitionButton.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
        transitionButton.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
        transitionButton.widthAnchor.constraint(equalToConstant: 120).isActive = true
        transitionButton.heightAnchor.constraint(equalToConstant: 30).isActive = true
        
        displayLabel = UILabel()
        displayLabel.translatesAutoresizingMaskIntoConstraints = false
        displayLabel.text = "Labelです"
        self.addSubview(displayLabel)
        displayLabel.topAnchor.constraint(equalTo:transitionButton.bottomAnchor, constant:10.0).isActive = true
        displayLabel.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
    }
    
    
    /// 次の画面へ遷移します
    /// - Parameter button: transitionButton
    @objc func tappedTransitionButton(_ button: UIButton) {
        delegate?.smclLabelView(self, tappedTransitionButton: button)
    }

}

```

実際このコードを走らせると以下のような画面が描画されます。
<img width="375" alt="Simulator Screen Shot - iPhone 8 - 2020-03-10 at 00.06.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415441/11ab0816-9dcd-b75a-c99d-1d62ddb28559.png">

白い画面にButtonとLabelが表示された簡単な構成です。

まず背景の白ビューの解説から

```swift
    /// 背景ビューを生成します
    func setBackView() {
        let backView: UIView = {
            let backView = UIView()
            backView.translatesAutoresizingMaskIntoConstraints = false
            backView.backgroundColor = .white
            return backView
        }()
        self.addSubview(backView)
        backView.topAnchor.constraint(equalTo: self.topAnchor, constant:0).isActive = true
        backView.leftAnchor.constraint(equalTo: self.leftAnchor, constant:0).isActive = true
        backView.rightAnchor.constraint(equalTo: self.rightAnchor, constant:0).isActive = true
        backView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant:0).isActive = true
    }
```


白いビューを生成しsuperViewの４方向にconstant:0を設定しています。
画像はわかりやすくするために紫にしています。
Xibでのオートレイアウト設定だとこの設定と同一になります。
![スクリーンショット 2020-03-10 0.24.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415441/c83d9d1b-5188-17ca-2c28-edddfdbb6034.png)



次にButtonの制約ですがこちらになります。

```swift
/// 次の画面へ遷移するボタンを生成します
transitionButton.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
transitionButton.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true        
transitionButton.widthAnchor.constraint(equalToConstant: 120).isActive = true
transitionButton.heightAnchor.constraint(equalToConstant: 30).isActive = true
```

こちらの二行で対象のビュー(self)の中心に配置しています。

```swift
transitionButton.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
transitionButton.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
```

Xibでのオートレイアウト設定だとこの設定と同一になります。
![スクリーンショット 2020-03-10 0.37.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415441/650507a2-6b06-3122-c010-014f57828cc1.png)


そして固定値で横幅120,高さ30を指定しているのがこちらになります。

```swift
transitionButton.widthAnchor.constraint(equalToConstant: 120).isActive = true
transitionButton.heightAnchor.constraint(equalToConstant: 30).isActive = true
```

先ほどの設定と合わせてXibでのオートレイアウト設定だとこの設定と同一になります。
![スクリーンショット 2020-03-10 0.40.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415441/c1826b18-ed93-b6d1-4927-47dc3ac91357.png)


以下VCでの実装になります。

```swift:SMCLLabelViewController.swift
class SMCLLabelViewController: UIViewController {
    private let labelView = SMCLLabelView()
    private let model = SMCLLabelViewModel()
    
    override func loadView() {
        self.view = labelView
        labelView.delegate = self
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}
```

こちらの

```swift
 self.view = labelView
```
通過時にoverrideしたinitメソッドを通過するのでレイアウトの設定が行われます。





次の画面TableViewとボタンになります。

```swift:
class SMCLTableView: UIView {
    weak var delegate: SMCLTableViewDelegate?
    /**データ表示テーブルビュー*/
    var tableView: UITableView = {
        let table = UITableView()
        table.translatesAutoresizingMaskIntoConstraints = false
        return table
    }()
    
    override init(frame: CGRect) {
      super.init(frame: frame)
      setLayout()
    }
    
    
    @available(*, unavailable)
    required init?(coder: NSCoder) {
      fatalError("init(coder:) has not been implemented")
    }
    
    func setLayout(){
        setBackView()
        setTableView()
        setSendButton()
    }
    
    func setBackView(){
        let backView: UIView = {
            let backView = UIView()
            backView.translatesAutoresizingMaskIntoConstraints = false
            backView.backgroundColor = .white
            return backView
        }()
        self.addSubview(backView)
        backView.topAnchor.constraint(equalTo: self.topAnchor, constant:0).isActive = true
        backView.leftAnchor.constraint(equalTo: self.leftAnchor, constant:0).isActive = true
        backView.rightAnchor.constraint(equalTo: self.rightAnchor, constant:0).isActive = true
        backView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant:0).isActive = true
    }
    
    func setTableView(){
        self.addSubview(tableView)
        tableView.topAnchor.constraint(equalTo: self.topAnchor, constant:0).isActive = true
        tableView.leftAnchor.constraint(equalTo: self.leftAnchor, constant:0).isActive = true
        tableView.rightAnchor.constraint(equalTo: self.rightAnchor, constant:0).isActive = true
        tableView.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 0.9).isActive = true
    }
    
    func setSendButton(){
        //内包するビューを作成
        let buttonView: UIView = {
            let buttonView = UIView()
            buttonView.translatesAutoresizingMaskIntoConstraints = false
            buttonView.backgroundColor = .white
            return buttonView
        }()
        self.addSubview(buttonView)
        buttonView.topAnchor.constraint(equalTo: tableView.bottomAnchor, constant:0).isActive = true
        buttonView.leftAnchor.constraint(equalTo: self.leftAnchor, constant:0).isActive = true
        buttonView.rightAnchor.constraint(equalTo: self.rightAnchor, constant:0).isActive = true
        buttonView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant:0).isActive = true
        
        
        let button: UIButton = {
            let button = UIButton()
            button.translatesAutoresizingMaskIntoConstraints = false
            button.setTitle("追加", for: .normal)
            button.setTitleColor(.blue, for: .normal)
            button.backgroundColor = .white
            button.addTarget(self, action: #selector(tappedAddButton), for: .touchUpInside)
            return button
        }()
        buttonView.addSubview(button)
        button.centerXAnchor.constraint(equalTo: buttonView.centerXAnchor).isActive = true
        button.centerYAnchor.constraint(equalTo: buttonView.centerYAnchor).isActive = true
        button.heightAnchor.constraint(equalTo: buttonView.heightAnchor, multiplier: 0.8).isActive = true
        button.widthAnchor.constraint(equalTo: buttonView.widthAnchor, multiplier: 0.5).isActive = true
    }
    
    
    
    /// 追加ボタン押下時のメソッド
    /// - Parameter button: 追加ボタン
    @objc func tappedAddButton(_ button: UIButton) {
        delegate?.smclTableView(self, tappedAddButton: button)
    }


}

```

実際このコードを走らせると以下のような画面が描画されます。
<img width="375" alt="Simulator Screen Shot - iPhone 8 - 2020-03-10 at 01.55.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415441/547f329e-19b4-3a3d-6b34-a1f5799c087f.png">


TableViewのレイアウトはこちらで設定しています。

```swift
    func setTableView(){
        self.addSubview(tableView)
        tableView.topAnchor.constraint(equalTo: self.topAnchor, constant:0).isActive = true
        tableView.leftAnchor.constraint(equalTo: self.leftAnchor, constant:0).isActive = true
        tableView.rightAnchor.constraint(equalTo: self.rightAnchor, constant:0).isActive = true
        tableView.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 0.9).isActive = true
    }
```
Xibを使用したautolayoutの場合このような設定になります。
![スクリーンショット 2020-03-10 1.51.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415441/3fc875fd-a28b-7189-49e7-4f2fe7d724f4.png)

左、上、右の余白を０にし高さを親ビューの9/10に設定しています。

次にボタンビューです

```swift
    func setSendButton(){
        //内包するビューを作成
        let buttonView: UIView = {
            let buttonView = UIView()
            buttonView.translatesAutoresizingMaskIntoConstraints = false
            buttonView.backgroundColor = .white
            return buttonView
        }()
        self.addSubview(buttonView)
        buttonView.topAnchor.constraint(equalTo: tableView.bottomAnchor, constant:0).isActive = true
        buttonView.leftAnchor.constraint(equalTo: self.leftAnchor, constant:0).isActive = true
        buttonView.rightAnchor.constraint(equalTo: self.rightAnchor, constant:0).isActive = true
        buttonView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant:0).isActive = true
    }
```
Xibを使用したautolayoutの場合このような設定になります。
![スクリーンショット 2020-03-10 2.01.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415441/15a2352c-d03e-1a2d-9eaf-7dbf44a48077.png)

出来たスペースいっぱいにビューを伸ばしているのでテーブルビューのサイズが変わった場合それに比例して高さが伸びるレイアウトになります。

最後にボタンです

```swift
buttonView.addSubview(button)
button.centerXAnchor.constraint(equalTo: buttonView.centerXAnchor).isActive = true
button.centerYAnchor.constraint(equalTo: buttonView.centerYAnchor).isActive = true
button.heightAnchor.constraint(equalTo: buttonView.heightAnchor, multiplier: 0.8).isActive = true
button.widthAnchor.constraint(equalTo: buttonView.widthAnchor, multiplier: 0.5).isActive = true
```
buttonViewの中心に配置、
高さはButtonView.height * 0.8
横幅はButtonView.width * 0.5

と言う風になっております。

おそらくこれだけあればあとはこのパターンを組み合わせて大体のレイアウトが作れると思います。

#最後に
まだまだペーペーでございまして、、、
この書き方がいいとかあったら教えてください、、、

コードでのautolayoutはXibでの設定を完全に理解していないと難しいだろうと感じました。
なので一つ一つの制約に画像を添付していきましたがこれが皆様の助けとなれば幸いです。

#参考にさせていただいた記事
[今更MVCとかでiOSアプリつくってみた（Swift）](https://qiita.com/am10/items/6e76ec231ee1251560cc)
[【swift】MVCモデルの簡単な説明](https://scleapt.com/swift_mvc/)
[iOS 9で追加されたNSLayoutAnchor使うと簡単にわかりやすく間違えずにNSLayoutConstraint（制約）が作れます【Auto Layout】](https://qiita.com/yucovin/items/4bebcc7a8b1088b374c9)
[【Swift4】AutoLayoutをコードで実装する](https://rightcode.co.jp/blog/information-technology/swift4-autolayout-takagai)
[UIViewにおけるレイアウトのライフサイクル](https://qiita.com/shoheiyokoyama/items/2f76938dffa845130acc)
[UIKitのView表示ライフサイクルを理解する](https://qiita.com/shtnkgm/items/f133f73baaa71172efb2)