Xcode8 UIViewController.view.subviewsのframeについて(追記しました)

ビューのサイズが1000x1000になる

Xcode8からStoryboardやxibでオブジェクトがframe値を持たなくなった影響で、UIViewController.view.subviewsのビューが正常なサイズを取得できなくて困るという話をよく耳にしている。

こんな感じのやつ

<UIView: 0x7ff823d04e80; frame = (0 0; 1000 1000); autoresize = RM+BM; layer = <CALayer: 0x61000003dca0>>

自分もこれで困ることはあったけど、まぁ仕方ないからラクしないで修正しようか。くらいの気持ちでした。

検証した

まずは通常のライフサイクルを確認する。

UIViewControllerUIViewを拡張してログ出力するようにした。

// UIViewControllerのログ
print("\(Date().description) : \(type(of: self)).\(#function):\(#line)")

// UIViewのログ
print("\(Date().description) : \(type(of: self)).\(#function):\(#line) : size: \(self.bounds)")

構成

// ViewControllerの構成(以下のログ表示名)
+ UINavigationController (NC)
  |
  + UIViewController (VC)

// Viewの構成(以下のログ表示名)
+ VC.view(V1)
  |
  + view(V2)
    |
    + view(V3)

何もリサイズとかしなかった場合

UIViewController.viewDidLayoutSubviews()が呼ばれた後でビューのサイズが取得できるようになりました。

// アプリを起動したところからログとってます
VC.init(coder:):19
NC.init(coder:):87
NC.awakeFromNib():92
VC.awakeFromNib():24
NC.viewDidLoad():97
NC.viewWillAppear:102
NC.viewWillLayoutSubviews():118
V3.init(coder:):29 : size: (0.0, 0.0, 1000.0, 1000.0)
V2.init(coder:):29 : size: (0.0, 0.0, 1000.0, 1000.0)
V1.init(coder:):29 : size: (0.0, 0.0, 375.0, 667.0)
V1.awakeFromNib():36 : size: (0.0, 0.0, 375.0, 667.0)
V2.awakeFromNib():36 : size: (0.0, 0.0, 1000.0, 1000.0)
V3.awakeFromNib():36 : size: (0.0, 0.0, 1000.0, 1000.0)
VC.viewDidLoad():30
VC.viewDidLoad():32
V1.setNeedsLayout():58 : size: (0.0, 0.0, 375.0, 667.0)
VC.viewWillAppear:37
V3.needsUpdateConstraints():78 -> true : size: (0.0, 0.0, 1000.0, 1000.0)
V2.needsUpdateConstraints():78 -> true : size: (0.0, 0.0, 1000.0, 1000.0)
V1.needsUpdateConstraints():78 -> true : size: (0.0, 0.0, 375.0, 667.0)
V1.layoutMarginsDidChange():73 : size: (0.0, 0.0, 375.0, 667.0)
V3.updateConstraints():68 : size: (0.0, 0.0, 1000.0, 1000.0)
V2.updateConstraints():68 : size: (0.0, 0.0, 1000.0, 1000.0)
VC.updateViewConstraints():46
V1.updateConstraints():68 : size: (0.0, 0.0, 375.0, 667.0)
V1.setNeedsLayout():58 : size: (0.0, 0.0, 375.0, 667.0)
VC.viewWillLayoutSubviews():53
V1.layoutSubviews():48 : size: (0.0, 0.0, 375.0, 667.0)
VC.viewDidLayoutSubviews():62
V2.layoutSubviews():48 : size: (0.0, 0.0, 375.0, 603.0)
V3.layoutSubviews():48 : size: (0.0, 0.0, 187.5, 301.5)
V3.layoutSubviews():48 : size: (0.0, 0.0, 187.5, 301.5)
V1.layoutIfNeeded():53 : size: (0.0, 0.0, 375.0, 667.0)
NC.viewDidLayoutSubviews():127
NC.viewWillLayoutSubviews():118
V1.layoutIfNeeded():53 : size: (0.0, 0.0, 375.0, 667.0)
NC.viewDidLayoutSubviews():127
V1.draw:41 : size: (0.0, 0.0, 375.0, 667.0)
V2.draw:41 : size: (0.0, 0.0, 375.0, 603.0)
V3.draw:41 : size: (0.0, 0.0, 187.5, 301.5)
V1.layoutIfNeeded():53 : size: (0.0, 0.0, 375.0, 667.0)
V1.layoutIfNeeded():53 : size: (0.0, 0.0, 375.0, 667.0)
VC.viewDidAppear:42
NC.viewDidAppear:107

レイアウトの再計算を仕込んだ場合

次にUIViewController.viewDidLoad()UIView.layoutIfNeeded()を読んだ場合、UIViewController.viewDidLoad()UIView.layoutIfNeeded()が呼ばれた後からビューのサイズが取得できるようになりました。
UIViewController.awakeFromNib()でもだいたい同じでした。

VC.init(coder:):19
NC.init(coder:):87
NC.awakeFromNib():92
VC.awakeFromNib():24
NC.viewDidLoad():97
NC.viewWillAppear:102
NC.viewWillLayoutSubviews():118
V3.init(coder:):29 : size: (0.0, 0.0, 1000.0, 1000.0)
V2.init(coder:):29 : size: (0.0, 0.0, 1000.0, 1000.0)
V1.init(coder:):29 : size: (0.0, 0.0, 375.0, 667.0)
V1.awakeFromNib():36 : size: (0.0, 0.0, 375.0, 667.0)
V2.awakeFromNib():36 : size: (0.0, 0.0, 1000.0, 1000.0)
V3.awakeFromNib():36 : size: (0.0, 0.0, 1000.0, 1000.0)
VC.viewDidLoad():30
V3.updateConstraints():68 : size: (0.0, 0.0, 1000.0, 1000.0)
V2.updateConstraints():68 : size: (0.0, 0.0, 1000.0, 1000.0)
VC.updateViewConstraints():46
V3.needsUpdateConstraints():78 -> false : size: (0.0, 0.0, 1000.0, 1000.0)
V2.needsUpdateConstraints():78 -> false : size: (0.0, 0.0, 1000.0, 1000.0)
V1.needsUpdateConstraints():78 -> true : size: (0.0, 0.0, 375.0, 667.0)
V1.updateConstraints():68 : size: (0.0, 0.0, 375.0, 667.0)
V1.setNeedsLayout():58 : size: (0.0, 0.0, 375.0, 667.0)
V1.setNeedsLayout():58 : size: (0.0, 0.0, 375.0, 667.0)
VC.viewWillLayoutSubviews():53
V1.layoutSubviews():48 : size: (0.0, 0.0, 375.0, 667.0)
VC.viewDidLayoutSubviews():62
V2.layoutSubviews():48 : size: (0.0, 0.0, 375.0, 667.0)
V3.layoutSubviews():48 : size: (0.0, 0.0, 187.5, 333.5)
V3.layoutSubviews():48 : size: (0.0, 0.0, 187.5, 333.5)
V1.layoutIfNeeded():53 : size: (0.0, 0.0, 375.0, 667.0)
VC.viewDidLoad():32
VC.viewWillAppear:37
V3.needsUpdateConstraints():78 -> false : size: (0.0, 0.0, 187.5, 333.5)
V2.needsUpdateConstraints():78 -> false : size: (0.0, 0.0, 375.0, 667.0)
V1.needsUpdateConstraints():78 -> false : size: (0.0, 0.0, 375.0, 667.0)
V1.layoutMarginsDidChange():73 : size: (0.0, 0.0, 375.0, 667.0)
V2.setNeedsLayout():58 : size: (0.0, 0.0, 375.0, 667.0)
V1.setNeedsLayout():58 : size: (0.0, 0.0, 375.0, 667.0)
VC.viewWillLayoutSubviews():53
V1.layoutSubviews():48 : size: (0.0, 0.0, 375.0, 667.0)
VC.viewDidLayoutSubviews():62
V2.layoutSubviews():48 : size: (0.0, 0.0, 375.0, 603.0)
V3.layoutSubviews():48 : size: (0.0, 0.0, 187.5, 301.5)
V1.layoutIfNeeded():53 : size: (0.0, 0.0, 375.0, 667.0)
NC.viewDidLayoutSubviews():127
NC.viewWillLayoutSubviews():118
V1.layoutIfNeeded():53 : size: (0.0, 0.0, 375.0, 667.0)
NC.viewDidLayoutSubviews():127
V1.draw:41 : size: (0.0, 0.0, 375.0, 667.0)
V2.draw:41 : size: (0.0, 0.0, 375.0, 603.0)
V3.draw:41 : size: (0.0, 0.0, 187.5, 301.5)
V1.layoutIfNeeded():53 : size: (0.0, 0.0, 375.0, 667.0)
V1.layoutIfNeeded():53 : size: (0.0, 0.0, 375.0, 667.0)
VC.viewDidAppear:42
NC.viewDidAppear:107

その他

ビューのサイズがおかしくなるのといっても、UIViewに問題があるわけではなく、xibやstoryboardから生成されたインスタンスのsubviewsがおかしくなるというところだったので、なるべくUIViewを拡張するような対応はしたくないな。と個人的には考えています。
もちろんUIViewのインスタンスの場合はそうも言ってられないのかなとは思うのですが、UIViewControllerの方が出番多いし、そっちの問題を片付けられたらそれでいいよね。と思っています。

stack overflowではUIView.layoutSubviews()のoverrideが推奨されているような感じですが、あまり気がすすまないです。

あと、今回のソースコードをgithubにあげました。
https://github.com/ikenie3/xcode8-frame

以下追記@2016/10/25

UIViewControllerUINavigationControllerのログでUIViewController.topLayoutGuide.lengthも追加で出力するようにしました。

// Before
print("\(Date().description) : \(type(of: self)).\(#function):\(#line)")

// After
print("\(Date().description) : \(type(of: self)).\(#function):\(#line), topLayoutGuide: \(self.topLayoutGuide.length)")

結果ですがUIViewController.updateViewConstraintsの時点でステータスバー+ナビゲーションバーの高さ取得できていました。

レイアウトの再描画を行わなかったときのログだけ出しました。

2016-10-25 08:09:56 +0000 : VC.init(coder:):19, topLayoutGuide: 0.0
2016-10-25 08:09:56 +0000 : NC.init(coder:):86, topLayoutGuide: 0.0
2016-10-25 08:09:56 +0000 : NC.awakeFromNib():91, topLayoutGuide: 0.0
2016-10-25 08:09:56 +0000 : VC.awakeFromNib():24, topLayoutGuide: 0.0
2016-10-25 08:09:56 +0000 : NC.updateViewConstraints():110, topLayoutGuide: 0.0
2016-10-25 08:09:56 +0000 : NC.viewDidLoad():96, topLayoutGuide: 0.0
2016-10-25 08:09:56 +0000 : NC.viewWillAppear:101, topLayoutGuide: 0.0
2016-10-25 08:09:56 +0000 : NC.viewWillLayoutSubviews():117, topLayoutGuide: 20.0
2016-10-25 08:09:56 +0000 : V3.init(coder:):29 : size: (0.0, 0.0, 1000.0, 1000.0)
2016-10-25 08:09:56 +0000 : V2.init(coder:):29 : size: (0.0, 0.0, 1000.0, 1000.0)
2016-10-25 08:09:56 +0000 : V1.init(coder:):29 : size: (0.0, 0.0, 375.0, 667.0)
2016-10-25 08:09:56 +0000 : V1.awakeFromNib():36 : size: (0.0, 0.0, 375.0, 667.0)
2016-10-25 08:09:56 +0000 : V2.awakeFromNib():36 : size: (0.0, 0.0, 1000.0, 1000.0)
2016-10-25 08:09:56 +0000 : V3.awakeFromNib():36 : size: (0.0, 0.0, 1000.0, 1000.0)
2016-10-25 08:09:56 +0000 : VC.viewDidLoad():29, topLayoutGuide: 0.0
2016-10-25 08:09:56 +0000 : VC.viewDidLoad():31, topLayoutGuide: 0.0
2016-10-25 08:09:56 +0000 : V1.setNeedsLayout():58 : size: (0.0, 0.0, 375.0, 667.0)
2016-10-25 08:09:56 +0000 : VC.viewWillAppear:36, topLayoutGuide: 0.0
2016-10-25 08:09:56 +0000 : V3.needsUpdateConstraints():78 -> true : size: (0.0, 0.0, 1000.0, 1000.0)
2016-10-25 08:09:56 +0000 : V2.needsUpdateConstraints():78 -> true : size: (0.0, 0.0, 1000.0, 1000.0)
2016-10-25 08:09:56 +0000 : V1.needsUpdateConstraints():78 -> true : size: (0.0, 0.0, 375.0, 667.0)
2016-10-25 08:09:56 +0000 : V1.layoutMarginsDidChange():73 : size: (0.0, 0.0, 375.0, 667.0)
2016-10-25 08:09:56 +0000 : V3.updateConstraints():68 : size: (0.0, 0.0, 1000.0, 1000.0)
2016-10-25 08:09:56 +0000 : V2.updateConstraints():68 : size: (0.0, 0.0, 1000.0, 1000.0)
2016-10-25 08:09:56 +0000 : VC.updateViewConstraints():45, topLayoutGuide: 64.0
2016-10-25 08:09:56 +0000 : V1.updateConstraints():68 : size: (0.0, 0.0, 375.0, 667.0)
2016-10-25 08:09:56 +0000 : V1.setNeedsLayout():58 : size: (0.0, 0.0, 375.0, 667.0)
2016-10-25 08:09:56 +0000 : VC.viewWillLayoutSubviews():52, topLayoutGuide: 64.0
2016-10-25 08:09:56 +0000 : V1.layoutSubviews():48 : size: (0.0, 0.0, 375.0, 667.0)
2016-10-25 08:09:56 +0000 : VC.viewDidLayoutSubviews():61, topLayoutGuide: 64.0
2016-10-25 08:09:56 +0000 : V2.layoutSubviews():48 : size: (0.0, 0.0, 375.0, 603.0)
2016-10-25 08:09:56 +0000 : V3.layoutSubviews():48 : size: (0.0, 0.0, 187.5, 301.5)
2016-10-25 08:09:56 +0000 : V3.layoutSubviews():48 : size: (0.0, 0.0, 187.5, 301.5)
2016-10-25 08:09:56 +0000 : V1.layoutIfNeeded():53 : size: (0.0, 0.0, 375.0, 667.0)
2016-10-25 08:09:56 +0000 : NC.viewDidLayoutSubviews():126, topLayoutGuide: 20.0
2016-10-25 08:09:56 +0000 : NC.viewWillLayoutSubviews():117, topLayoutGuide: 20.0
2016-10-25 08:09:56 +0000 : V1.layoutIfNeeded():53 : size: (0.0, 0.0, 375.0, 667.0)
2016-10-25 08:09:56 +0000 : NC.viewDidLayoutSubviews():126, topLayoutGuide: 20.0
2016-10-25 08:09:56 +0000 : V1.draw:41 : size: (0.0, 0.0, 375.0, 667.0)
2016-10-25 08:09:56 +0000 : V2.draw:41 : size: (0.0, 0.0, 375.0, 603.0)
2016-10-25 08:09:56 +0000 : V3.draw:41 : size: (0.0, 0.0, 187.5, 301.5)
2016-10-25 08:09:56 +0000 : V1.layoutIfNeeded():53 : size: (0.0, 0.0, 375.0, 667.0)
2016-10-25 08:09:56 +0000 : V1.layoutIfNeeded():53 : size: (0.0, 0.0, 375.0, 667.0)
2016-10-25 08:09:56 +0000 : VC.viewDidAppear:41, topLayoutGuide: 64.0
2016-10-25 08:09:56 +0000 : NC.viewDidAppear:106, topLayoutGuide: 20.0