UIViewの階層を自前でログ出力する

デバッガでの解析が面倒くさい局面もあるので、自分用に書き残しておきます。
Swiftの勉強用に、非再帰版も作ってみました。

この記事は Swift4.2時点の情報に基づきます。

## 再帰版
普通の実装。
とにかく全てのUIViewを把握したかったので、UIApplicationから取得できる全てのWindow配下のViewを出力しています。

``` swift
import Foundation
import UIKit

func dumpApplicationViewHierarchy() {
    UIApplication.shared.windows.forEach { window in
        print("\(window)")
        
        window.subviews.forEach({ rootView in
            dumpViewHierarchy(from: rootView, depth: 1)
        })
    }
}

func dumpViewHierarchy(from view: UIView, depth: Int = 0) {
    let indent = String(repeating: "  | ", count: depth)
    print("\(indent)\(view)")

    view.subviews.forEach { childView in
        dumpViewHierarchy(from: childView, depth: depth + 1)
    }
}
```

## 非再帰版

再帰関数のスタックオーバーフローが恐ろしい人向けの非再帰版。

``` swift
func dumpApplicationViewHierarchy2() {
    UIApplication.shared.windows.forEach { window in
        print("\(window)")
        
        window.subviews.forEach({ rootView in
            var viewStack = [(view: rootView, depth: 1)]
            
            while let lastNode = viewStack.popLast() {
                let indent = String(repeating: "  | ", count: lastNode.depth)
                print("\(indent)\(lastNode.view)")
                
                viewStack.append(contentsOf: lastNode.view.subviews.map {
                    (view: $0, depth: lastNode.depth + 1)
                }.reversed())
            }
        })
    }
}
```

## 自前関数の出力例

再帰版 及び 非再帰版 の出力例は、以下な感じ。`subviews`の順序どおりに出力されています。

```log
<UIWindow: 0x7fd43e424370; frame = (0 0; 375 812); gestureRecognizers = <NSArray: 0x60000117c2d0>; layer = <UIWindowLayer: 0x600001f6ca00>>
  | <UILayoutContainerView: 0x7fd43e408190; frame = (0 0; 375 812); autoresize = W+H; gestureRecognizers = <NSArray: 0x60000117d320>; layer = <CALayer: 0x600001f6dd20>>
  |   | <UINavigationTransitionView: 0x7fd43e423c00; frame = (0 0; 375 812); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x600001f6e760>>
  |   |   | <UIViewControllerWrapperView: 0x7fd43e6178f0; frame = (0 0; 375 812); autoresize = W+H; layer = <CALayer: 0x600001f60800>>
  |   |   |   | <UIView: 0x7fd43e4091a0; frame = (0 0; 375 812); autoresize = W+H; layer = <CALayer: 0x600001f64380>>
  |   |   |   |   | <UIStackView: 0x7fd43e409380; frame = (0 88; 375 690); opaque = NO; autoresize = RM+BM; layer = <CATransformLayer: 0x600001f643a0>>
  |   |   |   |   |   | <UILabel: 0x7fd43e42b490; frame = (0 0; 375 172.667); text = 'Label Item 1'; opaque = NO; autoresize = RM+BM; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x600003c045f0>>
  |   |   |   |   |   | <UILabel: 0x7fd43e42bea0; frame = (0 172.667; 375 172.333); text = 'Label Item 2'; opaque = NO; autoresize = RM+BM; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x600003c040f0>>
  |   |   |   |   |   | <UILabel: 0x7fd43e42c190; frame = (0 345; 375 172.667); text = 'Label Item 3'; opaque = NO; autoresize = RM+BM; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x600003c04820>>
  |   |   |   |   |   | <UIButton: 0x7fd43e40c990; frame = (0 517.667; 375 172.333); opaque = NO; autoresize = RM+BM; layer = <CALayer: 0x600001f64280>>
  |   |   |   |   |   |   | <UIButtonLabel: 0x7fd43e42c480; frame = (167 77.3333; 41 18); text = 'Dump'; alpha = 0.2; opaque = NO; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x600003c04b90>>
  |   | <UINavigationBar: 0x7fd43e41b3d0; frame = (0 44; 375 44); opaque = NO; autoresize = W; layer = <CALayer: 0x600001f6c720>>
  |   |   | <_UIBarBackground: 0x7fd43e41bee0; frame = (0 -44; 375 88); userInteractionEnabled = NO; layer = <CALayer: 0x600001f6c780>>
  |   |   |   | <UIImageView: 0x7fd43e41cbd0; frame = (0 88; 375 0.333333); userInteractionEnabled = NO; layer = <CALayer: 0x600001f6c7c0>>
  |   |   |   | <UIVisualEffectView: 0x7fd43e41ce00; frame = (0 0; 375 88); layer = <CALayer: 0x600001f6c7e0>>
  |   |   |   |   | <_UIVisualEffectBackdropView: 0x7fd43e426220; frame = (0 0; 375 88); autoresize = W+H; userInteractionEnabled = NO; layer = <UICABackdropLayer: 0x600001f6e4e0>>
  |   |   |   |   | <_UIVisualEffectSubview: 0x7fd43e4237e0; frame = (0 0; 375 88); autoresize = W+H; userInteractionEnabled = NO; layer = <CALayer: 0x600001f6e780>>
  |   |   | <_UINavigationBarLargeTitleView: 0x7fd43e41deb0; frame = (0 0; 0 50); clipsToBounds = YES; hidden = YES; layer = <CALayer: 0x600001f6c9a0>>
  |   |   |   | <UILabel: 0x7fd43e41b8a0; frame = (0 0; 0 0); userInteractionEnabled = NO; layer = <_UILabelLayer: 0x600003c006e0>>
  |   |   | <_UINavigationBarContentView: 0x7fd43e41d660; frame = (0 0; 375 44); layer = <CALayer: 0x600001f6c820>>
  |   |   |   | <UILabel: 0x7fd43e605510; frame = (164 11.6667; 47 20.3333); text = 'Demo'; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x600003c09130>>
  |   |   | <_UINavigationBarModernPromptView: 0x7fd43e4235e0; frame = (0 0; 0 50); alpha = 0; hidden = YES; layer = <CALayer: 0x600001f6dc20>>
```


## プライベートAPI版

この手の話題で良く紹介されるプライベートAPIを使った方法。
製品版ビルドでは、このコードを含めてはいけません。

```swift
func dumpApplicationViewHierarchy3() {
    UIApplication.shared.windows.forEach { window in
        print("\(window)")
        window.subviews.forEach({ rootView in
            let description = rootView.perform("recursiveDescription")
            print("\(String(describing: description))")
        })
    }
}
```

## プライベートAPI版の出力例
`perform`の結果は、長い文字列として得られる。これを`NSLog()`で出力すると途中で打ち切られるので注意が必要。

```
<UIWindow: 0x7fd43e424370; frame = (0 0; 375 812); gestureRecognizers = <NSArray: 0x60000117c2d0>; layer = <UIWindowLayer: 0x600001f6ca00>>
Optional(Swift.Unmanaged<Swift.AnyObject>(_value: <UILayoutContainerView: 0x7fd43e408190; frame = (0 0; 375 812); autoresize = W+H; gestureRecognizers = <NSArray: 0x60000117d320>; layer = <CALayer: 0x600001f6dd20>>
   | <UINavigationTransitionView: 0x7fd43e423c00; frame = (0 0; 375 812); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x600001f6e760>>
   |    | <UIViewControllerWrapperView: 0x7fd43e6178f0; frame = (0 0; 375 812); autoresize = W+H; layer = <CALayer: 0x600001f60800>>
   |    |    | <UIView: 0x7fd43e4091a0; frame = (0 0; 375 812); autoresize = W+H; layer = <CALayer: 0x600001f64380>>
   |    |    |    | <UIStackView: 0x7fd43e409380; frame = (0 88; 375 690); opaque = NO; autoresize = RM+BM; layer = <CATransformLayer: 0x600001f643a0>>
   |    |    |    |    | <UILabel: 0x7fd43e42b490; frame = (0 0; 375 172.667); text = 'Label Item 1'; opaque = NO; autoresize = RM+BM; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x600003c045f0>>
   |    |    |    |    | <UILabel: 0x7fd43e42bea0; frame = (0 172.667; 375 172.333); text = 'Label Item 2'; opaque = NO; autoresize = RM+BM; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x600003c040f0>>
   |    |    |    |    | <UILabel: 0x7fd43e42c190; frame = (0 345; 375 172.667); text = 'Label Item 3'; opaque = NO; autoresize = RM+BM; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x600003c04820>>
   |    |    |    |    | <UIButton: 0x7fd43e40c990; frame = (0 517.667; 375 172.333); opaque = NO; autoresize = RM+BM; layer = <CALayer: 0x600001f64280>>
   |    |    |    |    |    | <UIButtonLabel: 0x7fd43e42c480; frame = (167 77.3333; 41 18); text = 'Dump'; alpha = 0.2; opaque = NO; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x600003c04b90>>
   | <UINavigationBar: 0x7fd43e41b3d0; frame = (0 44; 375 44); opaque = NO; autoresize = W; layer = <CALayer: 0x600001f6c720>>
   |    | <_UIBarBackground: 0x7fd43e41bee0; frame = (0 -44; 375 88); userInteractionEnabled = NO; layer = <CALayer: 0x600001f6c780>>
   |    |    | <UIImageView: 0x7fd43e41cbd0; frame = (0 88; 375 0.333333); userInteractionEnabled = NO; layer = <CALayer: 0x600001f6c7c0>>
   |    |    | <UIVisualEffectView: 0x7fd43e41ce00; frame = (0 0; 375 88); layer = <CALayer: 0x600001f6c7e0>>
   |    |    |    | <_UIVisualEffectBackdropView: 0x7fd43e426220; frame = (0 0; 375 88); autoresize = W+H; userInteractionEnabled = NO; layer = <UICABackdropLayer: 0x600001f6e4e0>>
   |    |    |    | <_UIVisualEffectSubview: 0x7fd43e4237e0; frame = (0 0; 375 88); autoresize = W+H; userInteractionEnabled = NO; layer = <CALayer: 0x600001f6e780>>
   |    | <_UINavigationBarLargeTitleView: 0x7fd43e41deb0; frame = (0 0; 0 50); clipsToBounds = YES; hidden = YES; layer = <CALayer: 0x600001f6c9a0>>
   |    |    | <UILabel: 0x7fd43e41b8a0; frame = (0 0; 0 0); userInteractionEnabled = NO; layer = <_UILabelLayer: 0x600003c006e0>>
   |    | <_UINavigationBarContentView: 0x7fd43e41d660; frame = (0 0; 375 44); layer = <CALayer: 0x600001f6c820>>
   |    |    | <UILabel: 0x7fd43e605510; frame = (164 11.6667; 47 20.3333); text = 'Demo'; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x600003c09130>>
   |    | <_UINavigationBarModernPromptView: 0x7fd43e4235e0; frame = (0 0; 0 50); alpha = 0; hidden = YES; layer = <CALayer: 0x600001f6dc20>>))
```


## 参考リンク
ビューの階層構造をログ出力する
https://qiita.com/calmscape/items/35aa39cb4101bec53e94
デバッグのためにView階層を把握する
https://qiita.com/akatsuki174/items/45d4bd7cb150defbf116
iOS UIViewの階層構造をprintする extension by Swift 3.0
https://qiita.com/gi0_0iv/items/159fcdb19074ee66ca38
How to debug your view hierarchy using recursiveDescription
https://www.hackingwithswift.com/articles/101/how-to-debug-your-view-hierarchy-using-recursivedescription
Technical Note TN2239 iOS Debugging Magic
https://developer.apple.com/library/archive/technotes/tn2239/_index.html