最近はAutoLayout
を使う機会も増えてきましたが、メンテナンス性や書きやすさを考えると、(Non-AutoLayoutな)コードでViewのレイアウトを行うほうがメリットがあるケースも、多々あります。
また基本はAutoLayout
でレイアウトを行いつつも、画面内の特定の動的なsubview
だけはframe
を触りながらコードベースで行いたいような場面もあるでしょう。
なので普段はAutoLayout
がメインな方も、使わない場合のレイアウト方法を知っておいて損はないと思います。
この記事ではsubview
のframe
をコード上で触りながらレイアウトを行う場合の、方針や注意点についてまとめています。
ViewController内でsubviewのレイアウトを行う場合
青い矩形(blueRect)と、赤い矩形(redRect)をUIView
で作って画面の左右に並べるだけの処理をControllerに書いてみます。
Controllerの全体像
まず最初に、最終的なControllerのソースを貼ります。
その後で部分ごとに解説していきます。
Controller内でViewのレイアウトを処理を行うコードは以下のようになります。
import UIKit
// 青い矩形(blueRect)と、赤い矩形(redRect)を`UIView`で作って画面の左右に並べるだけController
class ViewController: UIViewController {
// MARK: - Properties -
lazy private blueRect:UIView = self.createBlueRect()
lazy private var redRect:UIView = self.createRedRect()
// MARK: - Life cycle events -
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(blueRect)
self.view.addSubview(redRect)
// - UIButtonなどにイベントを仕込みたい場合もここで行う
// - ViewにModelのデータ等の外部データを渡したい場合もここで行う
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.layoutBlueRect()
self.layoutRedRect()
}
// MARK: - Create subviews -
private func createBlueRect() -> UIView {
let rect = UIView(frame: CGRectZero)
rect.backgroundColor = UIColor.blueColor()
return rect;
}
private func createRedRect() ->UIView {
let rect = UIView(frame: CGRectZero)
rect.backgroundColor = UIColor.redColor()
return rect;
}
// MARK: - Layout subviews -
private func layoutBlueRect() {
blueRect.frame.size = CGSizeMake(50,50)
blueRect.center.x = self.view.center.x
blueRect.center.y = self.view.center.y
}
private func layoutRedRect() {
redRect.frame.size = CGSizeMake(50,50)
redRect.frame.origin.x = self.view.frame.size.width - redRect.frame.size.width
redRect.center.y = self.view.center.y
}
}
では上のソースを部分ごとに見て行きましょう。
subviewをlazyなプロパティとして宣言
プロパティの宣言は以下のように行っています。
lazy private var blueRect:UIView = self.createBlueRect()
lazy
を付けて子Viewのプロパティを宣言しています。lazy
宣言することによって、プロパティが最初に利用されたタイミングで1度だけcreateBlueRect
が実行され結果が変数の中に実行されます。
2度目以降にプロパティが呼ばれる時は、格納された値が利用されるので、createBlueRect
メソッドの中に初期化処理を書いておけば、それが1度だけ実行されることが保証されます。
( 注:ただ、このように宣言するとnilに戻すこともできないため、didReceiveMemoryWarning
での解放などを考慮する場合はlazy
ではなくget/set
などを利用して遅延ロードを書くのが良いです )
また今回の例ではView
の初期化メソッドは全てcreateXXX
といった名前にしています。
createBlueRect
の中身の初期化処理は以下のようになっています。
private func createBlueRect() -> UIView {
let rect = UIView(frame: CGRectZero)
rect.backgroundColor = UIColor.blueColor()
return rect;
}
createBlueRect
では上のように初期化処理のみを行って、layout
に関する処理は行っていません。
各Viewのframe
に関するレイアウト処理は、createXXXX
の中で行うべきではありません
なぜなら、各viewのframe
は画面の向き、self.view
の状態など外部要素よって変わるものである(場面が多い)ため、
それらが不定な初期化中には、依存する処理を書くことができないからです。
viewDidLoadの処理
viewDidLoad
はloadView
が実行された後、つまりself.view
が読み込み終わったタイミングで呼ばれます。
viewDidLoad
の中では子Viewと他Viewの関連する処理やイベント登録などを行います。
今回の以下の例ではaddSubview
のみを行っていますが、UIButton(UIControl)#addTarget
などを行う場合も、ここで行うと良いでしょう。
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(blueRect)
self.view.addSubview(redRect)
// - UIButtonなどにイベントを仕込みたい場合もここで行う
// - ViewにModelのデータ等の外部データを渡したい場合もここで行う
}
ここでもself.frame
を利用するような各View
のレイアウト処理は行うべきではないことに注意してください。
なぜならviewDidLoad
が呼ばれるのはself.view
の読み込み後の1度のみだからです。ここにself.frame
に依存したレイアウト処理を
書いてしまうと、最初の読み込み時にレイアウトが固定されてしまい、画面の向きの変化などself.view
の再レイアウトがあっても、子Viewのレイアウトが行われなくなってしまいます。
またAutoLayout
を使っているView
は、この時点ではframe
のレイアウトがなされていません。なので、それらを利用した処理もここでは書くことはできません。
よって、self.view
や「AuotLayoutなView」に依存しているレイアウト処理は以下のviewDidLayoutSubviews
の中で行いましょう。
viewDidLayoutSubviews
viewDidLayoutSubviews
はControllerの表示、画面の向きの変更、ナビゲーションバーの表示、様々な理由でself.view
のframe
が更新された時に呼ばれます。
またAutoLayout
を使ってる場合は、この時点でAutoLayout
によるframeの更新が終わっています。なのでAutoLayout
でレイアウトされたView
に依存した処理もここに書いても問題ありません。
なので、ここが子View(subviews)のレイアウトを、行う場所として適切です。
今回の例では以下のような実装となっています、
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.layoutBlueRect()
self.layoutRedRect()
}
....
private func layoutBlueRect() {
// 画面左端(縦は中央)に配置
blueRect.frame.size = CGSizeMake(50,50)
blueRect.center.x = self.view.center.x
blueRect.center.y = self.view.center.y
}
private func layoutRedRect() {
// 画面右端(縦は中央)に配置
redRect.frame.size = CGSizeMake(50,50)
redRect.frame.origin.x = self.view.frame.size.width - redRect.frame.size.width
redRect.center.y = self.view.center.y
}
subview
のframeを触る処理は煩雑になることも多いため、layoutXXXX
という名前の別メソッドに切り出しています。またSwiftでは構造体の値を直接上書きできるため、 redRect.center.y = 10
という書き方ができるので良いですね。(Objective-C
では、このコードはエラーになっていました)
さて、これで、self.view
が変化するごとに合わせて、subviews
も変化するようになりました。
試しに上記のコードを動かしてみてください。画面を回転させてself.frame
のサイズが変化したとしても、子Viewは左右の両端に配置されたままになっているはずです。
上の例はシンプルなので見やすいですが、複雑なレイアウト処理をControllerに書いていくと、Controller自体が膨らんでいってしまいます。
もっと複雑な初期化処理やレイアウト処理がある場合、例えば子Viewがさらに子Viewを持つケース(孫View)などがある場合はView
をCustomView
に切り出しすことを検討したほうが良いでしょう。
そこで、次にレイアウト処理も含めてView
の処理をCustomView
に切り出した場合のレイアウト方法について説明します。
CustomViewに切り出す場合のケース
CustomViewでのレイアウトについて考えてみます。
今回は例としてタイトル文字(UILabel)と、送信ボタン(UIButton)の2つのsubviewで構成されるCustomViewを考えてみましょう。 またタイトル文字(UILabel)の中の文字列は外から渡せるようにします。
CustomViewの全体のコード
Controller同様、最初に全体コードを貼っておきます。
その下で各部分の詳細について説明していきます。
import UIKit
class CustomView: UIView {
// MARK: - Properties -
lazy private var titleLabel:UILabel = self.createTitleLabel()
lazy private var submitButton:UIButton = self.createSubmitButton()
var title:String? {
get {
return titleLabel.text
}
set(title) {
titleLabel.text = title
self.setNeedsLayout()
}
}
// MARK: - Life cycle events -
required override init(frame: CGRect) {
super.init(frame: frame)
self.commonInit()
}
required init(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
self.commonInit()
}
private func commonInit() {
self.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
self.addSubview(titleLabel)
self.addSubview(submitButton)
}
override func layoutSubviews() {
super.layoutSubviews()
self.layoutTitleLabel()
self.layoutButton()
}
// MARK: - Create subviews -
private func createTitleLabel() -> UILabel {
let label = UILabel(frame: CGRectZero)
label.font = UIFont.systemFontOfSize(12)
label.textColor = UIColor.blackColor()
return label
}
private func createSubmitButton() -> UIButton {
let button = UIButton.buttonWithType(UIButtonType.System) as UIButton
button.setTitle("送信ボタン", forState: UIControlState.Normal)
return button
}
// MARK: - Layout subviews -
private func layoutTitleLabel() {
titleLabel.sizeToFit()
titleLabel.center.x = self.frame.size.width/2
titleLabel.frame.origin.y = 10
}
private func layoutButton() {
submitButton.sizeToFit()
submitButton.center.x = self.frame.size.width/2
submitButton.frame.origin.y = CGRectGetMaxY(titleLabel.frame) + 10
}
}
プロパティに関して
以下のsubviewをlazy
で宣言する部分はControllerと同じです。
lazy private var titleLabel:UILabel = self.createTitleLabel()
lazy private var submitButton:UIButton = self.createSubmitButton()
今回、外からタイトル文言を指定できるようにしたいので、title
をget/set
を利用して以下のように宣言しています。
var title:String? {
get {
return titleLabel.text
}
set(title) {
titleLabel.text = title
self.setNeedsLayout()
}
}
get/set
でtitleLabel.text
の値にアクセスしています。
これにより、customViewの外からtitleLabel
の構造を知ることなくtitle文言だけを更新できます。
初期化処理に関して
init
がcoder/frame
どちらの引数からでも共通の処理が呼ばれるようにcommonInit
というメソッドを作って、その中で初期化処理を定義しています。
private func commonInit() {
self.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
self.addSubview(titleLabel)
self.addSubview(submitButton)
}
処理の中身は、自分自身の初期化と、subviewの追加のみです。
Controllerの時と同様、この中でself.frame
に依存したsubview
のレイアウト処理は書くべきではありません。
なぜならView
のframe
は、画面の回転などによって変化することがあるからです。init
内にsubview
のレイアウト処理を書いてしまうとself.frame
が変化しても、再レイアウトが行われなくなってしまいます。
CustomView
におけるsubviewのレイアウトはlayoutSubviews
の中で行いましょう。
layoutSubviews
layoutSubviews
は、View
のframe
が変化した時に自動的に呼び出されます。(自分自身で呼び出すことは非推奨です)
なのでここに、subviewのレイアウトを処理を書いておけば、view
の状態に合わせて、必要な時にレイアウト処理が行われます。
今回の実装は以下のようにしていて、Controllerの時、同様に要素ごとにlayoutXXXX()
というメソッドに切り出しています。
override func layoutSubviews() {
super.layoutSubviews()
self.layoutTitleLabel()
self.layoutButton()
}
以上の実装で、view.frame
のサイズが変わっても、都度適切にレイアウトが行われるCustomView
が出来ました。
Controllerで作ったCustomViewを利用する際のコード
上で作ったCustomViewをControllerで利用する例は以下のようになります。
import UIKit
class ViewController: UIViewController {
// MARK: - Properties -
lazy private var customView:CustomView = self.createCustomView()
// MARK: - Life cycle events -
override func viewDidLoad() {
super.viewDidLoad()
customView.title = "タイトルです"
self.view.addSubview(customView)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.layoutCustomView()
}
// MARK: - Create subviews -
private func createCustomView() -> CustomView {
let rect = CustomView(frame: CGRectZero)
return rect;
}
// MARK: - Layout subviews -
private func layoutCustomView() {
customView.frame.size = CGSizeMake(180,80)
customView.center = self.view.center
}
}
ControllerではcustomView
のframe
だけを触って、customView
自身が持つ要素のレイアウトはcustomView#layoutSubviews
に任せていることがわかります。
またviewDidLoad
の中でcustomView.title
をセットしています。
まとめ
-
UIViewController#view
が持つsubview
のレイアウトはviewDidLayoutSubviews
の中で行う -
CustomView
が持つsubview
のレイアウトはlayoutSubviews
の中で行う
以上のように書けばAutoLayout
によるレイアウトが混在していても正常にレイアウトが行われます。
実際の場面では今回のようなシンプルな例では全てAutoLayout
で書くほうが簡単にできると思いますが、動的な要素が多い場面等などで、この記事を参考にしてしてみてください!