Xcode
apple
Swift
Swift3.0
swift4

【Swift】Initialization ClosureでviewDidLoadの肥大化を防ぐ

More than 1 year has passed since last update.

メインの内容は完全にこちらの記事の受け売りですが、勉強になったので、整理を兼ねて残します。
間違えた解釈や補足などあればご教授ください :raised_hands:

※2016/06/29:lazy varの使用について追記しました。
※2017/10/03:Swift4の書き方に修正しました。

Initialization Closure とは?

本題に入る前に、Initialization Closureについて調べてみました。

ストアドプロパティ(Stored property)の初期値を与える際に、初期値となる値を返すクロージャ実行結果を与えることができ、このような書き方をInitialization Closureというようです。

恐らくコードを見た方が早いと思うので、公式リファレンスのサンプルコードを載せます。

struct Chessboard {
    let boardColors: [Bool] = {
        var temporaryBoard = [Bool]()
        var isBlack = false
        for i in 1...8 {
            for j in 1...8 {
                temporaryBoard.append(isBlack)
                isBlack = !isBlack
            }
            isBlack = !isBlack
        }
        return temporaryBoard
    }()
    func squareIsBlackAt(row: Int, column: Int) -> Bool {
        return boardColors[(row * 8) + column]
    }
}

リファレンスはこちら

この例では、チェスのボードの各行・列におけるカラー情報(trueが黒, falseが白)を保持したArrayを Initialization Closure で初期値として与えています。
つまり、let boardColorsの初期値はクロージャ内で算出されたtemporaryBoardの値になります。

boardColorsは前述の Initialization Closure で初期化されているので、下記のように使用できます。

let board = Chessboard()
print(board.squareIsBlackAt(row: 0, column: 1))
// Prints "true"
print(board.squareIsBlackAt(row: 7, column: 7))
// Prints "false"

注意点

  • クロージャの最後に()をつけること
    • ()をつけることで、クロージャが即実行され、ストアドプロパティにクロージャから返った値が代入されます。
  • クロージャの中ではselfや他のプロパティ、メソッドを使用できません
    • クロージャの実行時にはまだそれぞれ初期化されていないためです。

viewDidLoadの肥大化を防ぐ

ここからが本題ですが、はじめに述べたように、完全にこちらの記事の受け売りです。笑

まずは、従来の書き方を見てみましょう。(サンプルコードも参考サイトを元にSwift4の書き方に修正)

従来の書き方

class ViewController: UIViewController {
    let topView = UIView()
    let imageView = UIImageView()
    let goButton = UIButton()

    override func viewDidLoad() {
        topView.frame = CGRect(x: 0, y: 0, width: 100, height: 200)
        topView.backgroundColor = UIColor.red
        view.addSubview(topView)

        imageView.image = UIImage(named: "profile")
        topView.addSubview(imageView)

        goButton.frame = CGRect(x: 0, y: 0, width: 30, height: 30)
        goButton.setTitle("GO!", for: .normal)
        view.addSubview(goButton)
    }
}

topViewimageViewなど、ViewController内の他の場所でも使用したい場合には、「まずviewの初期化だけしておき、viewDidLoadで細かい設定をした上でaddSubviewする」という書き方をします。

私もよくやることがあります。

問題点

この書き方での問題点としては、viewDidLoadで「各viewの設定」「各viewの設置」という2種類のことを行うことから、viewの数が増える分だけコードの見通しの悪さを生じさせる点です。

そこで、 Initialization Closure を活用し、「各viewの設定」を切り分けることでviewDidLoadを簡潔に書くことができるようです。

Initialization Closure の活用

※上記同様に参考サイトを元にSwift4の書き方に修正

class ViewController: UIViewController {
    let topView: UIView = {
        let view = UIView()
        view.frame = CGRect(x: 0, y: 0, width: 100, height: 200)
        view.backgroundColor = UIColor.red
        return view
    }()

    let imageView: UIImageView = {
        let imageView = UIImageView()
        imageView.image = UIImage(named: "profile")
        return imageView
    }()

    let goButton: UIButton = {
        let button = UIButton()
        button.frame = CGRect(x: 0, y: 0, width: 30, height: 30)
        button.setTitle("GO!", for: .normal)
        return button
    }()

    override func viewDidLoad() {
        view.addSubview(topView)
        topView.addSubview(imageView)
        view.addSubview(goButton)
    }
}

こちらでは、「各viewの設定」がそれぞれのクロージャ内で行われ、それぞれのプロパティは設定済みのviewを初期値で持つことができるので、viewDidLoad内ではいきなりaddSubviewすることができます。

これによって、viewDidLoadでは「各viewの設置」を行うという役割に専念できています。
どこで何がされているかがとても分かりやすくなったと思います。

終わりに

これを書いている段階では、簡単なサンプルは動かしてみましたが、実際のアプリケーションの中で使ってはいないので、これから取り入れていこうと思います。

実際に取り入れてみないとわからないこともあるかと思いますが、ひとまず「こういう書き方ができるんだ〜」という発見と、「Initialization Closure」についての整理という意味で書き残しました。

いろんな書き方ができて面白いですね! :blush:

参考

https://thatthinginswift.com/kill-your-viewdidload/

追記(2016/06/29)

lazy varを使用したInitialization Closureでselfを使う

上記の例における、letを使用してInitialization Closureで初期化するプロパティは、そのクラスや構造体の初期化が完了する前に初期値が確定している必要があります。
そのことからも、通常のプロパティの初期化同様、Initialization Closureによる初期化もそのクラスや構造体の初期化完了前に実行されることは明白です。
そのため上記例だと、初期化の完了していないselfInitialization Closure内で使用することはできません。

しかし、コメントにていただいたようにlazy varを使用することでこの問題は解決できます。

lazy varを使用して宣言したプロパティは、 lazy stored propertyといい、クラスや構造体の初期化時には初期化されず、初めてそのプロパティにアクセスした時に初期値がセットされます。

つまり、lazy stored propertyの初期化タイミングはそのクラスや構造体が初期化された後になることがわかります。
そのため、lazy stored propertyの初期値をInitialization Closureでセットする際には、そのクロージャ内でselfを使用することができるようになります。


例のごとく、参考サイトのコードだとこのような感じです。

class ViewController: UIViewController {
    lazy var imagePickerController: ImagePickerController = {
        let picker = ImagePickerController()
        picker.delegate = self // ←selfが初期化されているので使える
        picker.imageLimit = 1
        return picker
    }()
}

下記はエラーになる。

class ViewController: UIViewController {
    let imagePickerController: ImagePickerController = {
        let picker = ImagePickerController()
        picker.delegate = self // ←selfが初期化前なので使えない
        picker.imageLimit = 1
        return picker
    }()
}

追記参照