こんにちは!Apple信者のiOSエンジニアです。
iOSアプリ開発では、ときにレイアウトから内部処理までをStoryboardを使わずにコードのみで行うほうが効率的な場合があります。
たとえば複数人開発の際にStoryboardのコンフリクトを避けたり、細かいレイアウトを使いまわしたりする場合ですね。
コードのみでレイアウトを行う利点はこのようにたくさんあるのですが、その反面、コードが冗長になってしまうというデメリットもあります。
厳格なMVCモデルで明確にControllerとViewを分けていたとしても、Viewクラスの呼び出し関数内での描画処理コードがどうしても長くなってしまう。
そんなお悩みを抱えている方もいるのではないでしょうか。
そこで今回は、冗長になってしまいがちな描画処理を少しでもシンプルにするために、lazy句を用いたプロパティ定義についてご紹介していきます。
具体的なコードを用いて説明していきますので、すぐにでも使えます。ぜひ読んでみてくださいね。
lazyプロパティとは
まずはlazyプロパティについて軽くご紹介します。
lazyプロパティ(Lazy Stored Property)とは、実際に値が使用されるまで初期化を遅らせてくれるプロパティのことです。
lazyを直訳すると「怠惰」「怠けた」という意味を持ちます。
なので「処理が走っても呼ばれるギリギリまではダラダラ怠けているプロパティ」だと僕は覚えていました(笑)
本来のプロパティは、そのプロパティが呼ばれるまでにプログラマ側が初期化処理を行う必要がありますよね。
しかしlazy句を使うと「呼ばれた際にそのプロパティに対して行う処理」を変数内に定義しておくことで、処理が走ったタイミングで初めて初期化をしてくれるんです。
lazyプロパティの使い方
では実際に、lazyプロパティを使用していきましょう。
今回はViewController内で、画面描画の際にボタンを画面中央に表示されるというコードを記述しました。
上のようなレイアウトを、コードのみで行っていきます。
なので、ボタンに対して次のような指定をする必要があります。
- 「テストボタン」というタイトルテキストをセット
- タイトルテキストの文字色を「viewの背景色」に指定
- タイトルラベルのフォントサイズを「18」に設定
- ラディアス(角丸)を「10」に設定
- ボーダーラインの太さを「1.0」に指定
- ボーダーラインの色を「青」にする
- 背景色を「緑」にする
- タップされたときのアクションを指定する
- レイアウトを指定する
では実際に、これらの指定をコードで行っていきましょう。
lazyを使わなかった場合
はじめに、lazyを使わずに書いた場合のコードを以下に示しますね。
import UIKit
class ViewController: UIViewController {
var button: UIButton = UIButton()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
button.setTitle("テストボタン", for: .normal)
button.setTitleColor(self.view.backgroundColor, for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 18)
button.layer.cornerRadius = 10
button.layer.borderWidth = 1.0
button.layer.borderColor = UIColor.blue.cgColor
button.backgroundColor = .green
button.addTarget(self, action: #selector(buttonTapAction), for: .touchUpInside)
view.addSubview(button)
// ボタンのレイアウトをコードから指定
button.translatesAutoresizingMaskIntoConstraints = false
button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
button.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
button.heightAnchor.constraint(equalToConstant: 150).isActive = true
button.widthAnchor.constraint(equalToConstant: 150).isActive = true
}
@objc func buttonTapAction() {
print("tapped")
}
}
lazyを使わない場合には、このような記述になります。。
今回はViewController内で行っているので、viewDidLoad関数内でボタンへの指定を記述している形ですね。
この場合だと、どうしてもviewDidLoad内に記述する内容が多くなってしまう。
今回はボタンがたった一つだけですが、当然、アプリ開発では複数のUI部品を使用しますよね。
UI部品が増えれば増えるほど、viewDidLoad内、もしくはその下に定義する関数内のコードが増えてしまうわけです。
lazyを使った場合
次に全く同じ指定を、lazyを使って書いていきましょう。
import UIKit
class ViewController: UIViewController {
lazy var button: UIButton = {
let button = UIButton()
button.setTitle("テストボタン", for: .normal)
// 初期化は遅延されるため、ViewControllerのインスタンスを参照できる
button.setTitleColor(self.view.backgroundColor, for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 18)
button.layer.cornerRadius = 10
button.layer.borderWidth = 1.0
button.layer.borderColor = UIColor.blue.cgColor
button.backgroundColor = .green
button.addTarget(self, action: #selector(buttonTapAction), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
view.addSubview(button)
// ボタンのレイアウトをコードから指定
button.translatesAutoresizingMaskIntoConstraints = false
button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
button.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
button.heightAnchor.constraint(equalToConstant: 150).isActive = true
button.widthAnchor.constraint(equalToConstant: 150).isActive = true
}
@objc func buttonTapAction() {
print("tapped")
}
}
lazyを用いて書いたコードでは、viewDidLoad内をシンプルにできましたね。
これはご覧の通り、「lazy var buttonと定義したプロパティ内に初期化されたときの処理」を書いているからです。
view.addSubview(button)でボタンをViewに追加したときにはじめて、lazyプロパティ内に書かれている処理が実行されるということ。
そのためレイアウト処理など本当に必要な処理のみをviewDidLoad内に残し、スッキリ書けることがわかりますね。
lazyプロパティを使うメリット
lazyプロパティを使うメリットはたくさんあるので、いくつか挙げていきますね。
関数外でselfの呼び出しができる
lazyは遅延処理を指定できるため、自身(ViewController)がインスタンス化されたあとの記述もプロパティの中で完結させられます。
変数内でselfを呼び出すのは、使い慣れないうちは不思議に感じるかもしれませんね。
しかしこのselfを呼び出せることで、先程のコードで示したように、ボタンの文字色にviewの背景色を用いることもできます。
関数外でもselfを呼び出して、関数と分けて先んじて記述しておけることは大きなメリットですよね。
初期化の記述を考慮しなくても良い
lazy句のあるプロパティの初期化のタイミングは、呼び出したその時です。
なのでプログラマ側で、タイミングを図って初期化をする必要はありません。
たとえばですが、グローバル変数で上のようにオプショナル型のプロパティを定義した場合には、初期化のタイミングや方法に気を使う場合があります。
そういったことを考慮せずに、ロジック部分の処理に集中できるというのもlazyを用いるメリットですね。
重いプロパティの生成を遅延させられる
たとえば、ネストさせたUI部品をアニメーションさせながら初期化するときなど。
そんな初期化の処理自体が重たくなってしまう場合にも、lazyを使うといいでしょう。
詳しくは後述しますが、例外を除いて呼ばれたタイミングで一度初期化されたら、あとは呼ばれることはありません。
開発中のアプリのメモリ管理などで頭を悩ませている人は、一度lazyプロパティを使ってみてくださいね。
lazyプロパティを使う上での注意点
ただし、lazy句を使う上での注意点もあります。
僕の経験も交えながら、具体的にお伝えしていきますね。
lazyは複数回走ることもある
lazy句を使っても、例外的に処理が複数回走ることがあります。
それはマルチスレッドで同じプロパティにアクセスしたとき。
なので例えばですが、「初期化されたら指定の変数に+1をする」というような処理をlazyプロパティ内で記述すると、複数回呼ばれたときに期待しない動作になることがあります。
そのためスレッドパターンを見直すか、値に+1などの処理は関数内で行うようにしましょう。
似た書き方に注意
これは僕がいまよりもさらに駆け出しの頃に実際に経験した例ですが、似た書き方でlazyが抜け落ちている場合がありました。
var button: UIButton = {
let button = UIButton()
button.setTitle("テストボタン", for: .normal)
// この↓のself句で構文エラーになる
button.setTitleColor(self.view.backgroundColor, for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 18)
button.layer.cornerRadius = 10
button.layer.borderWidth = 1.0
button.layer.borderColor = UIColor.blue.cgColor
button.backgroundColor = .green
button.addTarget(self, action: #selector(buttonTapAction), for: .touchUpInside)
return button
}()
上のような形では、lazy(=遅延処理)が行われません。
そのためself句を用いた指定はもちろんですが、呼び出し時の変数の状態をみたり、もちろん値を参照するなどもNGです。
この書き方自体は便利なのですが、遅延処理をするか否かを考えて書き分けを行っていきましょう。
さいごに
この記事では、プロパティの遅延処理には欠かせないlazy句を使った記述方法についてご紹介をしてきました。
UI部品のレイアウトも含めてコードのみで記述をする場面などでは、lazyは特に役に立つ方法です。
ViewController内でのコードでの記述が長くて困っている方は、是非試してみてくださいね!