255
253

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

[iOS] Swift時代の! コードによるViewレイアウトのベストプラクティス

Last updated at Posted at 2014-12-26

最近はAutoLayoutを使う機会も増えてきましたが、メンテナンス性や書きやすさを考えると、(Non-AutoLayoutな)コードでViewのレイアウトを行うほうがメリットがあるケースも、多々あります。

また基本はAutoLayoutでレイアウトを行いつつも、画面内の特定の動的なsubviewだけはframeを触りながらコードベースで行いたいような場面もあるでしょう。

なので普段はAutoLayoutがメインな方も、使わない場合のレイアウト方法を知っておいて損はないと思います。

この記事ではsubviewframeをコード上で触りながらレイアウトを行う場合の、方針や注意点についてまとめています。

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の処理

viewDidLoadloadViewが実行された後、つまり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.viewframeが更新された時に呼ばれます。

また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)などがある場合はViewCustomViewに切り出しすことを検討したほうが良いでしょう。

そこで、次にレイアウト処理も含めて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()

今回、外からタイトル文言を指定できるようにしたいので、titleget/setを利用して以下のように宣言しています。

    var title:String? {
        get {
            return titleLabel.text
        }
        set(title) {
            titleLabel.text = title
            self.setNeedsLayout()
        }
    }

get/settitleLabel.textの値にアクセスしています。
これにより、customViewの外からtitleLabelの構造を知ることなくtitle文言だけを更新できます。

初期化処理に関して

initcoder/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のレイアウト処理は書くべきではありません。

なぜならViewframeは、画面の回転などによって変化することがあるからです。init内にsubviewのレイアウト処理を書いてしまうとself.frameが変化しても、再レイアウトが行われなくなってしまいます。

CustomViewにおけるsubviewのレイアウトはlayoutSubviewsの中で行いましょう。

layoutSubviews

layoutSubviewsは、Viewframeが変化した時に自動的に呼び出されます。(自分自身で呼び出すことは非推奨です)
なのでここに、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ではcustomViewframeだけを触って、customView自身が持つ要素のレイアウトはcustomView#layoutSubviewsに任せていることがわかります。
またviewDidLoadの中でcustomView.titleをセットしています。

まとめ

  • UIViewController#viewが持つsubviewのレイアウトはviewDidLayoutSubviewsの中で行う
  • CustomViewが持つsubviewのレイアウトはlayoutSubviewsの中で行う

以上のように書けばAutoLayoutによるレイアウトが混在していても正常にレイアウトが行われます。

実際の場面では今回のようなシンプルな例では全てAutoLayoutで書くほうが簡単にできると思いますが、動的な要素が多い場面等などで、この記事を参考にしてしてみてください!

255
253
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
255
253

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?