メインの内容は完全にこちらの記事の受け売りですが、勉強になったので、整理を兼ねて残します。
間違えた解釈や補足などあればご教授ください
※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)
}
}
topView
やimageView
など、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」についての整理という意味で書き残しました。
いろんな書き方ができて面白いですね!
参考
追記(2016/06/29)
lazy var
を使用したInitialization Closure
でselfを使う
上記の例における、let
を使用してInitialization Closure
で初期化するプロパティは、そのクラスや構造体の初期化が完了する前に初期値が確定している必要があります。
そのことからも、通常のプロパティの初期化同様、Initialization Closure
による初期化もそのクラスや構造体の初期化完了前に実行されることは明白です。
そのため上記例だと、初期化の完了していないself
をInitialization 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
}()
}