Edited at

UINavigationControllerと領域拡張に潜む罠

More than 5 years have passed since last update.

以下、StoryBoardでレイアウトを組んでいる人には無縁な内容かと思います。

iOS7でのUIの刷新により、「ナビゲーションやタブバーにレイアウトがめり込む」という問題と格闘してきた人は多いと思います。

一方で、「コードベースで書いたUIScrollViewUITableViewは何の対処もしなくても問題なく動いた」と不思議に思ったり、「xibでデシリアライズしたUIScrollViewに変な余白が発生した」と困惑したりしたことがないでしょうか?


諸悪の根源・暗黙のcontentInsets

その原因は暗黙のうちに設定されるcontentInsetsの存在にあります。



  • UIViewControllerviewUINavigationController経由で表示される。

  • 領域拡張(edgesForExtendedLayout)が有効


  • view一番目の子要素UIScrollViewおよびそのサブクラス

以上の条件を満たした場合、自動的にedgesForExtendedLayoutによる領域拡張設定を相殺する、contentInsetsによる余白が設定されます。


基本的な仕様

この機能はUINavigationControllerを経由したときのみ動作します。UITabBarControllerのみではcontentInsetsは設定されません。しかしUINavigationBarと同時に用いた場合、UITabBar分の49ptの余白も設定されます。

コードで記述しているか、InterfaceBuilderを利用しているかは関係ありません。loadViewメソッド内でスクロールビューを生成した場合でも、xibファイルからデシリアライズされた場合でも判定が行われます。両者を混在させた場合、xib側が優先される挙動を見せますが、ちゃんと検証してないです。

既に書いたとおり、対象となるのはUIViewControllerviewプロパティの 最初の子要素 です。最初の子要素にcontentInsetsプロパティがない場合、兄弟要素にスクロールビューが存在してもcontentInsetsは設定されません。当然、複数のスクロールビューを持つ画面では、条件を満たしたものだけにcontentInsetsが付与されます。

contentInsetsの付与は、viewWillAppear:animated:からviewDidAppear:animated:までの間に、ビューコントローラーのライフサイクルで一度だけ行われます。

contentInsetsの付与は上書きではなく、「加算」方式です。例えばviewDidLoad:で当該のスクロールビューにcontentInsets.topで20ptを与えていた場合、そこにステータスバー+ナビゲーションバー64pt分が加算され、viewDidAppear:animated:の時点では84pt設定されていることになります。

もしNavigationBarhiddenにしている場合、その分は減算されステータスバー分の20pt分だけ加算されます。試していないですが、ステータスバーを非表示にしている場合も影響を与えると思います。


仕様のマズさ

恐るべきことに、 当該のスクロールビューがどこに配置されるかとは無縁に余白が設定 されます。画面の上端に接していれば64pt、下端に接していればタブバー分49ptなどといった気の効いた実装にはなっていません。

付与された余白を消すにはviewDidAppear:animated:のタイミングしかありません。デバイスの向きによって必要な余白幅が変わるため、この点は仕様がない部分もありますが…。

画面の向きが固定のアプリであれば、viewDidLoad:の時点で負のcontentInsetsを設定して相殺するという手段を用いることができるかもしれません。


説明がヒドい

この暗黙のcontentInsetsの付与を止める手段は二つ在ります。

ひとつがedgesForExtendedLayoutUIRectEdgeNoneを指定し、領域拡張自体を止めるという手です。

もうひとつがUIViewControllerのプロパティであるautomaticallyAdjustsScrollViewInsetsにNOを指定することです。このプロパティの存在については、iOS7移行ガイドに説明があるのですが、これが実にヒドい。


● automaticallyAdjustsScrollViewInsets

スクロールビューの装飾(立体的にくぼんだような表示)は自動調整されますが、これが望ましくない場合は、automaticallyAdjustsScrollViewInsetsをNOと設定してください(automaticallyAdjustsScrollViewInsetsのデフォルト値はYES)。


訳も悪いのですが、仮に原文を読んだとしてもUINavigationController経由かつ、一定の条件を満たしたUIViewControllerのスクロールビューに対して、自動的にcontentInsetsが付与される…という仕様を汲み取ることは到底不可能です。

Class Referenceでは次のような説明になっています。


Specifies whether or not the view controller should automatically adjust its scroll view insets.

Default value is YES, which allows the view controller to adjust its scroll view insets in response to the screen areas consumed by the status bar, navigation bar, and toolbar or tab bar. Set to NO if you want to manage scroll view inset adjustments yourself, such as when there is more than one scroll view in the view hierarchy.


こちらもUINavigationControllerが絡まないと機能しない点について触れていないので、リファレンスを読むだけでプロパティの真意を掴めた人がどれだけいるのか謎です。


ハッピーな解決法

edgesForExtendedLayoutを切りましょう。

Opaqueなナビゲーションバーに全く問題を感じない。blurエフェクトは誰に望まれているのか…。