【Swift】画面遷移で責務分離する

#はじめに
今回は、ViewControllerで画面遷移をおこなうときに責務を分離して、変更に強くなるようなコードを紹介したいと思います。
以下のようなアプリを例として作ってみたいと思います。

![ezgif.com-gif-maker (2).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/954451/33a38e1a-a207-ca33-13b3-ab6f583b28fd.gif)

#GitHub

https://github.com/0429oonishi/ScreenTransition-practice

#共通部分
以下のような階層にしました
![ScreenShot 2021-05-13 8.03.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/954451/470ec79a-2420-fd79-6bdb-02277701f468.png)

```swift:User
struct User {
    let name: String
    let job: String
}
```

```swift:TaskListTableViewCell
final class TaskListTableViewCell: UITableViewCell {
    
    @IBOutlet private weak var nameLabel: UILabel!
    @IBOutlet private weak var jobLabel: UILabel!
    
    var onTapEvent: (() -> Void)?
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        onTapEvent?()
    }
    
}

// MARK: - setup
extension TaskListTableViewCell {
    
    func setup(user: User, onTapEvent: (() -> Void)?) {
        self.onTapEvent = onTapEvent
        nameLabel.text = user.name
        jobLabel.text = user.job
    }
    
}
```

TaskListTableViewCell.xib
![ScreenShot 2021-05-13 8.04.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/954451/73854dfd-ae34-863e-8a83-44075eb72550.png)

TaskList.storyboard
![ScreenShot 2021-05-13 8.06.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/954451/ebd6ad7c-22df-aad8-e37b-af27f2d7b89e.png)

AddTask.storyboard
![ScreenShot 2021-05-13 8.06.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/954451/bf1f68a4-99a1-0c4a-8e8f-1fc6819f563a.png)

UserInfo.storyboard
![ScreenShot 2021-05-13 8.07.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/954451/5fdd31e4-bf92-2cfd-4c9e-b42db59b3d91.png)

細かな制約は今回の趣旨と関係がないので、GitHubをクローンして確認していただくか、適当に制約はつけておいてください！


#変更前

```swift:TaskListViewController
final class TaskListViewController: UIViewController {
    
    @IBOutlet weak var tableView: UITableView!
    
    private var users = [User]()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView.dataSource = self
        tableView.register(UINib(nibName: "TaskListTableViewCell", bundle: nil),
                           forCellReuseIdentifier: "TaskListTableViewCell")
        tableView.tableFooterView = UIView()
        
    }
    
    @IBAction private func addButtonDidTapped(_ sender: Any) {
        let addTaskVC = UIStoryboard(name: "AddTask", bundle: nil).instantiateViewController(identifier: "AddTaskViewController") as! AddTaskViewController
        addTaskVC.onTapEvent = { user in
            guard let user = user else { return }
            self.users.append(user)
            self.tableView.reloadData()
        }
        addTaskVC.modalPresentationStyle = .fullScreen
        present(addTaskVC, animated: true, completion: nil)
    }
    
}

// MARK: - UITableViewDataSource
extension TaskListViewController: UITableViewDataSource {
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return users.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "TaskListTableViewCell") as! TaskListTableViewCell
        let user = users[indexPath.row]
        cell.setup(user: user) { [weak self] in
            self?.presentUserInfoVC(user: user)
        }
        return cell
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 100
    }
    
    private func presentUserInfoVC(user: User) {
        let userInfoVC = UIStoryboard(name: "UserInfo", bundle: nil).instantiateViewController(identifier: "UserInfoViewController") as! UserInfoViewController
        userInfoVC.user = user
        userInfoVC.modalPresentationStyle = .fullScreen
        present(userInfoVC, animated: true, completion: nil)
    }
    
}
```


```swift:AddTaskViewController
final class AddTaskViewController: UIViewController {
    
    @IBOutlet private weak var nameTextField: UITextField!
    @IBOutlet private weak var jobTextField: UITextField!
    @IBOutlet private weak var backButton: UIButton!
    
    var onTapEvent: ((User?) -> Void)?
    
    @IBAction private func backButtonDidTapped(_ sender: Any) {
        guard let nameText = nameTextField.text,
              let jobText = jobTextField.text else { return }
        let user = (nameText.isEmpty || jobText.isEmpty) ? nil : User(name: nameText, job: jobText)
        onTapEvent?(user)
        dismiss(animated: true, completion: nil)
    }
    
}
```


```swift:UserInfoViewController
final class UserInfoViewController: UIViewController {

    @IBOutlet private weak var nameLabel: UILabel!
    @IBOutlet private weak var jobLabel: UILabel!
    
    var user: User?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        nameLabel.text = user?.name
        jobLabel.text = user?.job
        
    }

    @IBAction private func backButtonDidTapped(_ sender: Any) {
        dismiss(animated: true, completion: nil)
    }
    
}
```

#変更後

```swift:TaskListViewController
final class TaskListViewController: UIViewController {
    
    @IBOutlet weak var tableView: UITableView!
    
    private var users = [User]()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView.dataSource = self
        tableView.register(TaskListTableViewCell.nib,
                           forCellReuseIdentifier: TaskListTableViewCell.identifier)
        tableView.tableFooterView = UIView()
        
    }
    
    @IBAction private func addButtonDidTapped(_ sender: Any) {
        let addTaskVC = AddTaskViewController.instantiate { user in
            guard let user = user else { return }
            self.users.append(user)
            self.tableView.reloadData()
        }
        addTaskVC.modalPresentationStyle = .fullScreen
        present(addTaskVC, animated: true, completion: nil)
    }
    
}

// MARK: - UITableViewDataSource
extension TaskListViewController: UITableViewDataSource {
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return users.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: TaskListTableViewCell.identifier) as! TaskListTableViewCell
        let user = users[indexPath.row]
        cell.setup(user: user) { [weak self] in
            self?.presentUserInfoVC(user: user)
        }
        return cell
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 100
    }
    
    private func presentUserInfoVC(user: User) {
        let userInfoVC = UserInfoViewController.instantiate(user: user)
        userInfoVC.modalPresentationStyle = .fullScreen
        present(userInfoVC, animated: true, completion: nil)
    }
    
}
```


```swift:AddTaskViewController
final class AddTaskViewController: UIViewController {
    
    @IBOutlet private weak var nameTextField: UITextField!
    @IBOutlet private weak var jobTextField: UITextField!
    @IBOutlet private weak var backButton: UIButton!
    
    private var onTapEvent: ((User?) -> Void)?
    
    @IBAction private func backButtonDidTapped(_ sender: Any) {
        guard let nameText = nameTextField.text,
              let jobText = jobTextField.text else { return }
        let user = (nameText.isEmpty || jobText.isEmpty) ? nil : User(name: nameText, job: jobText)
        onTapEvent?(user)
        dismiss(animated: true, completion: nil)
    }
    
    static func instantiate(onTapEvent: @escaping (User?) -> Void) -> AddTaskViewController {
        let addTaskVC = UIStoryboard.addTask.instantiateViewController(
            identifier: AddTaskViewController.identifier
        ) as! AddTaskViewController
        addTaskVC.onTapEvent = onTapEvent
        return addTaskVC
    }
    
}

private extension UIStoryboard {
    
    static var addTask: UIStoryboard {
        UIStoryboard(name: "AddTask", bundle: nil)
    }
    
}
```


```swift:UserInfoViewController
final class UserInfoViewController: UIViewController {

    @IBOutlet private weak var nameLabel: UILabel!
    @IBOutlet private weak var jobLabel: UILabel!
    
    private var user: User?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        nameLabel.text = user?.name
        jobLabel.text = user?.job
        
    }

    @IBAction private func backButtonDidTapped(_ sender: Any) {
        dismiss(animated: true, completion: nil)
    }
    
    static func instantiate(user: User) -> UserInfoViewController {
        let userInfoVC = UIStoryboard.userInfo.instantiateViewController(identifier: UserInfoViewController.identifier) as! UserInfoViewController
        userInfoVC.user =  user
        return userInfoVC
    }
    
}

private extension UIStoryboard {
    
    static var userInfo: UIStoryboard {
        UIStoryboard(name: "UserInfo", bundle: nil)
    }
    
}
```

#解説
1番のポイントは、遷移元で遷移先の変更に応じて変更をする必要がなくなったところです。
例えば、変更後のコードは、TaskListViewController（遷移元）からAddTaskViewController(遷移先)に遷移しますが、AddTaskViewControllerに何かTaskListViewControllerから渡したいものが増えたとしても、AddTaskViewControllerを変更するだけですみます。TaskListViewControllerは余分な情報を持ちすぎなくてすみます。TaskListViεwControllerは、AddTaskViewControllerに遷移さえできればいいので、AddTaskViewControllerに何を渡すのかに関しては関心がありません。どうでもいいことです。なので、その情報はAddTaskViewControllerに直接持たせました。さらに、こうすることで、AddTaskViewControllerのonTapEvent変数はprivateをつけることができます。

以下のコードに関しては、AddTaskViewControllerでしか使わないことが保証されていることと、AddTaskViewControllerでしか必要がない情報なので、private extensionとすることでこのファイル内でしか扱えないようにしました。そもそも、extension UIStoryboardを作るかどうかは、好みなので、そこはお任せします。

```swift
private extension UIStoryboard {
    
    static var addTask: UIStoryboard {
        UIStoryboard(name: "AddTask", bundle: nil)
    }
    
}
```

遷移先がUserInfoViewControllerの場合も同様です。

さらに、ハードコーディングをしないことも重要ですが、今回の趣旨と関係ないので、説明は省きます。以下の記事とGitHubなどを参考に、UITableViewCellのidなどをハードコーディングしないようにしてみてください！

https://qiita.com/REON/items/6388b6a522b9d7fc949c


#おわりに
この記事がいいなと思っていただけたら、LGTMとストックをお願いします！