【翻訳】How To Fix Your Fat ViewController

iOS の開発ではよくあることですが、 UIViewController は Views 、 outlet 、 action 、 layout 、ビジネスロジックなど、あらゆる要素で埋め尽くされてしまうため、非常にゴチャゴチャして管理しづらくなります。

実際、 UIViewController は非常にシンプルであるべきです。 UIViewController は View を管理し、アクションの発生を監視するだけで良いのです。このチュートリアルでは、私がいつもプロジェクトに適用しているベストプラクティスの1つを共有したいと思います。

## 解決方法

画面を複数の UIView コンポーネント に分割し、それぞれの UIView コンポーネント が全てのレイアウトとスタイルを管理します。また、アクションや変更を ViewController に通知する責任も負います。その上、 UIView コンポーネント は最終的にプロジェクト全体で再利用可能になります。すごい！

![](https://storage.googleapis.com/zenn-user-upload/0d0777b430fc-20230721.png)

このシンプルなプロフィールページ（上の画像）を開発するとしましょう。そこで、私たちの頭の中では、ViewController にこれらのものを全て配置することになるでしょう。単純に storyboard を開き、アウトレット（ラベル、画像）といくつかの制約を追加し始めることができます。その通りです。残念ながら、これは私たちがいつもやっている一般的なやり方です。

でも、これを読めば、もうそんな間違いを繰り返すことはないでしょう。

問題は、UI にさらに変更を加えたり、機能を追加したりするときに起こります。だから、 ViewController が太りやすくなります。

## 分割する

さて、 UIViewController は常にスリムなままにしておいて、別々の UIView に分割し始めたい。以下はその手順です。

1. まず、 ViewController に UIScrollView と UIStackView を追加して、コンポーネントを動的に追加できるようにします。

2. 3つの UIView （ HeaderView 、 StatsView 、 PostView ）に分けます。

3. 各コンポーネントの xib ファイル と UIView ファイル を作成します。

4. 各 UIView で全てのスタイルを管理します。

5. UI をデータで更新する関数を UIView に作成します。

6. 各 UIView にプロトコル（デリゲート）を追加して、 UI の更新やアクションのイベントを  ViewController が取得できるようにします。

![](https://storage.googleapis.com/zenn-user-upload/765da2b92d92-20230721.png)

## コーディングの時間

新しいiOSプロジェクトを作成しましょう。各ステップを進める前に、ここ(https://github.com/xmhafiz/SlimViewControllerDemo)にアップロードされている完成したプロジェクトを参考にしてください。

### 1. ViewController に StackView を追加して、コンポーネントを動的に追加できるようにする。

ViewController.swift を開き、 UIStackView() と UIScrollView() の変数をプログラムで追加します。まず、 UIScrollView を追加し、 superView の端に固定します。次に、 scrollView の中に UIStackView を追加し、端も固定します。また、 stackView の幅の制約を scrollView に設定します。

```swift
import UIKit

class ViewController: UIViewController {
    lazy var scrollView = UIScrollView()
    lazy var mainStackView: UIStackView = {
        let stackView = UIStackView()
        stackView.axis = .vertical
        return stackView
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupView()
    }
    
    func setupView() {
        scrollView.pinToEdges(inView: view)
        mainStackView.pinToEdges(inView: scrollView)
        mainStackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
    }
}

extension UIView {
    func pinToEdges(constant: CGFloat = 0, inView superview: UIView) {
        superview.addSubview(self)
        self.translatesAutoresizingMaskIntoConstraints = false
        self.topAnchor.constraint(equalTo: superview.topAnchor, constant: constant).isActive = true
        self.bottomAnchor.constraint(equalTo: superview.bottomAnchor, constant: constant).isActive = true
        self.leadingAnchor.constraint(equalTo: superview.leadingAnchor, constant: constant).isActive = true
        self.trailingAnchor.constraint(equalTo: superview.trailingAnchor, constant: constant).isActive = true
    }
}
```

プロジェクトを実行しても、今のところ何も変わりません。

### 2 .3つの UIView （ HeaderView 、 StatsView 、 PostView ）に分解します。

このステップでは、3つの UIView Swift ファイルと3つの xib ファイル を作成し、それらをリンクします。

a.　HeaderView.swift と HeaderView.xib

必ず、"ファイルの所有者" と "クラス" 名をリンクしてください。また、　HeaderView.swift　で作成した全ての　 IBOutlets　をリンクしてください。

![](https://storage.googleapis.com/zenn-user-upload/d82de4a165a2-20230721.png)

```swift
import UIKit

class HeaderView: UIView {
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var subtitleLabel: UILabel!
    @IBOutlet weak var profileImageView: UIImageView!
    @IBOutlet weak var editButton: UIButton!
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        commonInit()
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    
    func commonInit() {
        let xibFileName = "HeaderView" // xib extension not included
        let view = Bundle.main.loadNibNamed(xibFileName, owner: self, options: nil)![0] as! UIView
        self.addSubview(view)
        view.frame = self.bounds
        setupView()
    }
    
    func setupView() {
        editButton.clipsToBounds = true
        editButton.layer.cornerRadius = editButton.frame.height/2
        profileImageView.clipsToBounds = true
        profileImageView.layer.cornerRadius = profileImageView.frame.height/2
    }
    
    func update(profile: DemoProfile) {
        profileImageView.image = UIImage(named: profile.avatarImageName)
        titleLabel.text = profile.name
        subtitleLabel.text = profile.role
    }
}
```

b.　StatsView.swift　と　StatsView.xib　（前回と同じで、「ファイルの所有者」とアウトレットをリンクします。）

![](https://storage.googleapis.com/zenn-user-upload/50780ec8f03f-20230721.png)

```swift
//  StatsView.swift
import UIKit

class StatsView: UIView {
    @IBOutlet weak var viewContainerView: UIView!
    @IBOutlet weak var readContainerView: UIView!
    @IBOutlet weak var totalViewValueLabel: UILabel!
    @IBOutlet weak var totalReadValueLabel: UILabel!
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        commonInit()
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    
    func commonInit() {
        let xibFileName = "StatsView" // xib extension not included
        let view = Bundle.main.loadNibNamed(xibFileName, owner: self, options: nil)![0] as! UIView
        self.addSubview(view)
        view.frame = self.bounds
        setupView()
    }
    
    func setupView() {
        viewContainerView.clipsToBounds = true
        viewContainerView.layer.cornerRadius = 10
        readContainerView.clipsToBounds = true
        readContainerView.layer.cornerRadius = 10
    }
    
    func updateContent(profile: DemoProfileStore) {
        totalViewValueLabel.text = profile.totalViews
        totalReadValueLabel.text = profile.totalReads
    }
}
```

c.　最後に作成する　UIView　ファイル　は、　PostView.swift　と　PostView.xib　です。

![](https://storage.googleapis.com/zenn-user-upload/13562271fb4d-20230721.png)

```swift
//
//  PostView.swift
import UIKit

class PostView: UIView {
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var subtitleLabel: UILabel!
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        commonInit()
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    
    func commonInit() {
        let xibFileName = "PostView" // xib extension not included
        let view = Bundle.main.loadNibNamed(xibFileName, owner: self, options: nil)![0] as! UIView
        self.addSubview(view)
        view.frame = self.bounds
    }
    
    func updatePost(title: String, description: String) {
        titleLabel.text = title
        subtitleLabel.text = description
    }
}
```

### 3.　全てのスタイルは各 UIView で管理されます。

### 4.　UI　をデータで更新する関数を　UIView　に作成します。

使用する適切なオブジェクトを渡すようにしてください。このプロジェクトでは、必要な全てのプロパティを持つ　DemoProfile　を使用します。

### 5.各　UIView　にプロトコル（デリゲート）を追加して、　UI　の更新やアクションのイベントを　Controller　が取得できるようにします。

このステップでは、UIView コンポーネントが何らかのアクションや変更に対して ViewController に通知できるようにするために、　Swift プロトコルを使用します。

UIView　コンポーネントは独立したクラスなので、　ViewController　は内部で起こったことを何も知りません。

今回のケースでは、　"Edit Profile"　ボタンがタップされたときに　Controller　に通知するためのプロトコル（デリゲート）を　HeaderView　に追加するだけです。

a.　まず、　HeaderView.swift　に　HeaderViewDelegate　というプロトコルを追加します。

```swift
protocol HeaderViewDelegate {
    func didTapEditButton()
}

class HeaderView: UIView {
    //...
    // add delegate variable
    var delegate: HeaderViewDelegate?
    //...
  
    // update this function with button target
    func setupView() {
        // ...
        // add actions
        editButton.addTarget(self, action: #selector(self.handleEditAction), for: .touchUpInside)
    }
  
    @objc func handleEditAction() {
        delegate?.didTapEditButton()
    }
}
```

b.　次に、上に示したように、　didTapEditButton()　というメソッドを追加します。そして、ボタンのターゲット・アクションがデリゲート・メソッド（19行目の上のコード）を呼び出すようにします。

c.　ViewController　クラスで、　HeaderViewDelegate　を実装し、　HeaderView　オブジェクトの　.delegate　を　self　に設定して、実装メソッドで呼び出すようにします（下図）。

![](https://storage.googleapis.com/zenn-user-upload/238b37f324a7-20230721.png)

ViewController.swift　の一番下に　HeaderViewDelegate　の実装を追加して、エラーを回避します。

```swift
extension ViewController: HeaderViewDelegate {
    func didTapEditButton() {
        print("Edit button is tapped!")
    }
}
```

最後に、この3つのコンポーネントを　stackView　に含めるように　ViewController　を更新しましょう。更新後の　ViewController　は以下のようになります。

```swift
//
//  ViewController.swift
import UIKit

class ViewController: UIViewController {

    lazy var headerView = HeaderView()
    lazy var summaryView = StatsView()
    lazy var postView = PostView()
    
    lazy var scrollView = UIScrollView()
    lazy var mainStackView: UIStackView = {
        let stackView = UIStackView()
        stackView.axis = .vertical
        stackView.addArrangedSubview(headerView)
        stackView.addArrangedSubview(postView)
        stackView.addArrangedSubview(summaryView)
        return stackView
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupView()
        // mock fetching data and populate into views
        setupData()
    }
    
    func setupView() {
        scrollView.pinToEdges(inView: view)
        mainStackView.pinToEdges(inView: scrollView)
        mainStackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
        
        // setup delegate
        headerView.delegate = self
    }
    
    func setupData() {
        let profile = DemoProfile()
        // update header
        headerView.update(profile: profile)
        // update text
        postView.updatePost(title: profile.postTitle, description: profile.postDescription)
        // update summary
        summaryView.updateContent(profile: profile)
    }
}

extension ViewController: HeaderViewDelegate {
    func didTapEditButton() {
        print("Edit button is tapped!")
    }
}
```

## ミッション完了！

おめでとう！これで全てのステップが完了し、プロジェクトを実行する準備ができました。  ViewController.swift は52行しかありません。やったね🎉。

完全なソースコードはここ(https://github.com/xmhafiz/SlimViewControllerDemo)からダウンロードできます。既存のプロジェクトに実装することで、（将来のモジュールや機能によって）よりすっきりし、より拡張しやすくなります。

読んでくれてありがとう。フィードバックは大歓迎です。

"学ぶこと、それが私たちのスキルを成長させる方法です"

## 参考記事

https://docs.swift.org/swift-book/LanguageGuide/Protocols.html

https://github.com/xmhafiz/SlimViewControllerDemo

# 【翻訳元の記事】

How To Fix Your Fat ViewController
https://medium.com/geekculture/slim-uiviewcontroller-break-into-multiple-uiview-components-78d7a67d7468