[UIKit考察]AutoLayoutと他のレイアウトエンジンをどのように接続するか (最大の横幅はどうやって知る?)
この記事は、開発するiOSアプリの中で、レイアウトパフォーマンスを上げるため、部分的にAutoLayout以外のレイアウトエンジンを用いた場合に、どのようにAutoLayoutの世界と接続することが出来るか。という考察です。
考え方の基本として、AutoLayoutからしたらその他のレイアウトエンジンはFrameベースで行うマニュアルレイアウトと同じものになるので、
AutoLayoutの制約の中でマニュアルレイアウトで作られたカスタムUIViewのコンポーネントを正しく動かすには?
を考えることになります。
先に結論
考察の結論として、スッキリしたものではないが、
- マニュアルレイアウトで作られたカスタムコンポーネントはUILabelのサブクラスを使う
- UILabelの持つtextRectメソッドをOverrideし、自身が膨張可能な幅を知り高さを計算しAutoLayoutに返却
この手法でマニュアルレイアウト(カスタムレイアウトエンジン)とAutoLayoutを概ね問題なく接続できた。
(概ねというのは、実装を試した時に特定のパターンでうまく動作しなかったことがあったため。)
以下、解説となります。
最大の幅を知り、高さを計算するにはどうする?
すべてのレイアウトエンジンはコンポーネントが持つコンテンツと制約をもとにサイズを計算する機能を持ちます。
コンテンツと制約とは例として、
Labelが持つテキストに応じて、「Widthが200ptのときにHeightは何pt?」
というイメージです。
幅が確定した時の高さがとても大切になります。
余談ですが、幅が分かっているときの高さの計算方法はAutoLayoutでは次のメソッドを使って計算できます。
UIView.systemLayoutSizeFitting(_:)UIView.systemLayoutSizeFitting(_:withHorizontalFittingPriority:verticalFittingPriority:)
を用いてサイズ計算が可能です。
AutoLayoutに準拠したカスタムコンポーネントをUIViewサブクラスで作る
UIViewのサブクラスを作り、カスタムコンポーネントを作る時に、UIKitが持つプリミティブなコンポーネントを詰め合わせて作る場合には特に問題は発生しません。
(UIKitが持つプリミティブなコンポーネントとは UILabel, UIButtonなど)
しっかりとサブクラス内でも制約を組むことでAutoLayoutがカスタムコンポーネントの外とレイアウトを行ってくれます。
intrinsicContentSizeをoverrideする、なども不要です。
課題が発生するときは、
- UIKitが持つプリミティブなコンポーネントを内部で使わないとき (例えば、自前で描画するとき)
- 幅に合わせて高さを変更する実装を行いたいとき
例えば、自前で描画を行うテキストラベルなどが挙げられます。
そのテキストラベルは、横幅に合わせて改行し、高さが変動する振る舞いが必要になるとして、どのように実装すべきでしょうか。
テキストラベルは自身が膨張可能な最大の横幅を知らない限りは高さを計算することは不可能です。
横幅は隣り合うコンポーネントによって徐々に決まります。
intrinsicContentSizeの実装がアイデアに浮かびますが、これでは不可能で、intrinsicContentSizeはコンポーネント自身が持つ状態をもとに自分自身の表示にサイズを返すプロパティです。
これでは外部に合わせた大きさを返すことはできません。
sizeThatFitsが呼ばれるかと思ったが、AutoLayoutを使用している場合は全く呼び出されません。
そこで、色々調べている中、実際に独自の描画を行うテキストラベルのOSSからヒントを得ることが出来ました。
UILabelとUITextFieldは外部のレイアウトに合わせて自身のサイズを返すメソッドを持っています。
UILabel.textRect(forBounds bounds: CGRect, limitedToNumberOfLines numberOfLines: Int) -> CGRect
このメソッドのbounds引数からレイアウトによって決定されたテキストラベルが膨張可能な最大の幅を取得することができます。
UIViewの階層にはこれに相当するメソッドは見当たらなかった。
つまり、この仕組みを借りることで、AutoLayout以外のレイアウトエンジンも接続可能になります。
textRectメソッドのbounds.widthを使用して、高さを計算し、CGRectとして返却すればよいのです。
TextureとAutoLayoutの接続に成功
このアプローチにより、Textureで実装されたUIコンポーネントをAutoLayoutの世界でも正しく自身のコンテンツに合わせて伸縮させることに成功しました。
独自描画のテキストラベルがなぜUILabelサブクラスなのかが理解できた
私は、以前からNantesやTTTAttributedLabelなどの独自で描画するテキストラベルがなぜUILabelのサブクラスなのかが理解できませんでした。
自前で描画するので、UIViewのサブクラスで十分であり、UILabelの持つプロパティをoverrideしても特にメリットは無いだろう、と
しかし、UIViewのサブクラスではAutoLayout準拠が行えないため、だと今回の調査により理解できました。
とはいえ、作るものがカスタムラベルでない場合もUILabelサブクラスはちょっと気持ち悪いので、実はベストプラクティスがあるんじゃないかとも考えています。
なにかあれば教えて頂けると嬉しいです。