ペンギン村 Tech Blog

技術をこよなく愛するエンジニア集団が在住するペンギン村から、世界へ役立つ(かもしれない)技術情報を発信する技術系ブログです。某アラレちゃんが済む村とは一切関係ありません。んちゃ!

【iOS】【Swift】太り過ぎなUIViewController.viewDidLoadをスリムにして可読性を爆上げする

前書き

本命/義理チョコを今か今かと待ちわびています、ナガクラ(@nagakuta)です!一ヶ月ぶりの投稿になります!
今回は上級者が開発時に使うといわれる、ちょこっと役立つTipsを紹介します!(バレンタインだけに)

TL;DR

viewDidLoadをスリムにするには

  1. XIBでViewを実装する場合はdidSetを使う
  2. コードでViewを実装する場合はClosureを使う

太り過ぎなviewDidLoad

皆さんは、Viewを実装する際にどの方法でUIパーツの設定を行いますか?(´ε`;)ウーン…

自分は、今回の方法を知るまでは以下の方法を採っていました。

  • XIBでCustom Viewのレイアウトを作成する場合
    • Interface Builderにて設定 or viewDidLoad内でコードによる設定
  • コードでCustom Viewを実装する場合
    • loadViewでCustom Viewを実装、viewDidLoadで細かい設定を実装

しかし、上記の方法では、UIパーツの数が多ければ多いほどviewDidLoadが肥大化してしまい、最悪の場合は何十行もあるviewDidLoadになってしまうこともありましたorz

final class CustomViewController: UIViewController {

    @IBOutlet private weak var firstLabel: UILabel!
    @IBOutlet private weak var secondLabel: UILabel!
    @IBOutlet private weak var thirdLabel: UILabel!
    @IBOutlet private weak var fourthLabel: UILabel!
    @IBOutlet private weak var firstButton: UIButton!
    @IBOutlet private weak var secondButton: UIButton!
    @IBOutlet private weak var thirdButton: UIButton!

    // MARK: - Life Cycle Methods

    override func viewDidLoad() {
        super.viewDidLoad()

        // FirstLabelの設定
        self.firstLabel.text = /* 省略 */
        self.firstLabel.font = /* 省略 */
        self.firstLabel.textColor = /* 省略 */
        self.firstLabel.textAlignment = /* 省略 */
        self.firstLabel.lineBreakMode = /* 省略 */

        // SecondLabelの設定
        self.secondLabel.text = /* 省略 */
        self.secondLabel.font = /* 省略 */
        self.secondLabel.textColor = /* 省略 */
        self.secondLabel.textAlignment = /* 省略 */
        self.secondLabel.lineBreakMode = /* 省略 */

        (中略)

        // FirstButtonの設定
        self.firstButton.setTitle("ボタン1", for: .normal)
        self.firstButton.setTitleColor(UIColor.red, for: .normal)
        self.firstButton.tintColor = /* 省略 */

        // SecondButtonの設定
        self.secondButton.setTitle("ボタン2", for: .normal)
        self.secondButton.setTitleColor(UIColor.blue, for: .normal)
        self.secondButton.tintColor = /* 省略 */

        (省略)
    }

}

(可読性が低い…低すぎるぞ…!😨😨😨)

それでは可読性が低いので、各UIパーツごとに設定用のメソッド(e.g. setupLabel)を実装し、それをviewDidLoadで呼び出す、という方法で回避を試みましたが…

final class CustomViewController: UIViewController {

    (省略)

    // MARK: - Life Cycle Methods

    override func viewDidLoad() {
        super.viewDidLoad()

        self.setupFirstLabel()
        self.setupSecondLabel()

        (中略)

        self.setupFirstButton()
        self.setupSecondButton()

        (省略)
    }

}

// MARK: - Private Methods
extension CustomViewController {

    /// FirstLabelの設定
    private func setupFirstLabel() {
        self.firstLabel.text = /* 省略 */
        self.firstLabel.font = /* 省略 */
        self.firstLabel.textColor = /* 省略 */
        self.firstLabel.textAlignment = /* 省略 */
        self.firstLabel.lineBreakMode = /* 省略 */
    }

    /// SecondLabelの設定
    private func setupSecondLabel() {
        self.secondLabel.text = /* 省略 */
        self.secondLabel.font = /* 省略 */
        self.secondLabel.textColor = /* 省略 */
        self.secondLabel.textAlignment = /* 省略 */
        self.secondLabel.lineBreakMode = /* 省略 */
    }

    (中略)

    /// FirstButtonの設定
    private func setupFirstButton() {
        self.firstButton.setTitle("ボタン1", for: .normal)
        self.firstButton.setTitleColor(UIColor.red, for: .normal)
        self.firstButton.tintColor = /* 省略 */
    }

    /// SecondButtonの設定
    private func setupSecondButton() {
        self.secondButton.setTitle("ボタン2", for: .normal)
        self.secondButton.setTitleColor(UIColor.blue, for: .normal)
        self.secondButton.tintColor = /* 省略 */
    }

    (省略)

}

(何だこれは…やたらメソッドが増えるじゃないか…😥😥😥)

結局のところ、この方法もベストとは言えない方法だと思っていました。

viewDidLoadをシェイプアップする冴えた方法

なんとかviewDidLoadを膨らませず、かつ設定用メソッドの実装以外の方法でUIパーツの設定を行えないか…
その方法を探るべく、インターネットを彷徨っていたところ、一件のページを見つけました。

thatthinginswift.com

その記事中には、

  • viewDidLoadでUIパーツの設定やプロパティの初期化をするのは古臭いし、いい方法じゃないよね
  • Swiftでは、didSetClosureでプロパティの初期化をする方法があるって知ってた?
  • じゃあ、これらを使ってUIパーツの設定を行えばいいんじゃないかな

といったことが書かれていました。

「…え、Closureでプロパティの初期化ができるの!?」と驚いた私がApple公式のSwiftガイドを見ると、たしかにClosureでのプロパティ初期化の方法が載っているではありませんか。

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

なるほど、確かにこの方法をUIパーツに適用すれば、わざわざviewDidLoadでゴリゴリと設定を実装しなくても良くなりそうです。

実際にやってみた

XIBでCustom Viewのレイアウトを実装している場合

XIBでCustom Viewのレイアウトを実装する場合、IBOutletでコードと結びつけてから設定を行います。

その場合はClosureによる初期化ができないので、didSetを利用して設定します。

final class CustomViewController: UIViewController {

    @IBOutlet private weak var firstLabel: UILabel! {
        didSet {
            self.firstLabel.text = /* 省略 */
            self.firstLabel.font = /* 省略 */
            self.firstLabel.textColor = /* 省略 */
            self.firstLabel.textAlignment = /* 省略 */
            self.firstLabel.lineBreakMode = /* 省略 */
        }
    }

    @IBOutlet private weak var secondLabel: UILabel! {
        didSet {
            self.secondLabel.text = /* 省略 */
            self.secondLabel.font = /* 省略 */
            self.secondLabel.textColor = /* 省略 */
            self.secondLabel.textAlignment = /* 省略 */
            self.secondLabel.lineBreakMode = /* 省略 */
        }
    }

    @IBOutlet private weak var thirdLabel: UILabel! {
        didSet {
            (省略)
        }
    }

    @IBOutlet private weak var fourthLabel: UILabel! {
        didSet {
            (省略)
        }
    }

    @IBOutlet private weak var firstButton: UIButton! {
        didSet {
            self.firstButton.setTitle("ボタン1", for: .normal)
            self.firstButton.setTitleColor(UIColor.red, for: .normal)
            self.firstButton.tintColor = /* 省略 */
        }
    }

    @IBOutlet private weak var secondButton: UIButton! {
        didSet {
            self.secondButton.setTitle("ボタン2", for: .normal)
            self.secondButton.setTitleColor(UIColor.blue, for: .normal)
            self.secondButton.tintColor = /* 省略 */
        }
    }

    @IBOutlet private weak var thirdButton: UIButton! {
        didSet {
            (省略)
        }
    }

    // MARK: - Life Cycle Methods

    override func viewDidLoad() {
        super.viewDidLoad()

        (ここで何もしなくていい😄)
    }

}

おお…!これならviewDidLoadが初期化で膨れ上がることもないし、設定用のメソッドを書かなくてもいいし、どのUIパーツにどの設定がされているかが一目瞭然ですね😊😊😊

ちなみに、このdidSetが呼ばれるタイミングはviewDidLoadの前になります。IBOutletプロパティに値がセットされるタイミング(loadViewと同じタイミング…?勉強不足ですみません🙇)で各々のUIパーツの設定がなされます。

コードでCustom Viewを実装する場合

そして、Custom Viewをコードで実装する場合はClosureで初期化を行います。

final class CustomViewController: UIViewController {

    private let firstLabel: UILabel! = {
        let label: UILabel = UILabel()
        label.text = /* 省略 */
        label.font = /* 省略 */
        label.textColor = /* 省略 */
        label.textAlignment = /* 省略 */
        label.lineBreakMode = /* 省略 */
        return label
    }()

    private let secondLabel: UILabel! = {
        let label: UILabel = UILabel()
        label.text = /* 省略 */
        label.font = /* 省略 */
        label.textColor = /* 省略 */
        label.textAlignment = /* 省略 */
        label.lineBreakMode = /* 省略 */
        return label
    }()

    private let thirdLabel: UILabel! = {
        let label: UILabel = UILabel()
        (中略)
        return label
    }()

    private let fourthLabel: UILabel! = {
        let label: UILabel = UILabel()
        (中略)
        return label
    }()

    private let firstButton: UIButton! = {
        let button: UIButton = UIButton()
        button.setTitle("ボタン1", for: .normal)
        button.setTitleColor(UIColor.red, for: .normal)
        button.tintColor = /* 省略 */
        return button
    }()

    private let secondButton: UIButton! = {
        let button: UIButton = UIButton()
        button.setTitle("ボタン2", for: .normal)
        button.setTitleColor(UIColor.blue, for: .normal)
        button.tintColor = /* 省略 */
        return button
    }()

    private let thirdButton: UIButton! = {
        let button: UIButton = UIButton()
        (中略)
        return button
    }()

    private lazy var customView: UIView = { [weak self] in
        let view: UIView = UIView(frame: UIScreen.main.bounds)

        // ここでViewを追加する
        view.addSubview(self?.firstLabel)
        view.addSubview(self?.secondLabel)

        (中略)

        view.addSubview(self?.firstButton)
        view.addSubview(self?.secondButton)

        (中略)

        return view
    }()

    // MARK: - Life Cycle Methods

    override func loadView() {
        self.view = self.customView
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        (ここで何もしなくていい😄)
    }

}

こちらもdidSetの場合と同じく、どのUIパーツでどの設定がされているかが分かりやすいと思います👍

letlazy varで宣言しているプロパティがありますが、その違いは「初期化時にプロパティの値を利用したいか」です。

letの場合はプロパティを利用しなくてもいい場合です。lazy varの場合は、weak selfでプロパティを弱参照にて利用することができます👍

あとがき

いかがでしょうか。はじめに載せたソースと最後に載せたソース、どちらが可読性の高いものだったでしょうか。

自分は、この方法をデファクトスタンダードにしたいと思っています(๑•̀ㅂ•́)و✧
なので、この記事を見て「良さそう!」と思った方、今すぐリファクタリングだ!!

追記

Qiitaのこちらの記事でも今回の記事と同様の内容が紹介されていました!こちらもぜひ参考にしてみてください!

qiita.com