LoginSignup
2
1

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-08-23

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

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

再帰版

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

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)
    }
}

非再帰版

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

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の順序どおりに出力されています。

<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を使った方法。
製品版ビルドでは、このコードを含めてはいけません。

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

2
1
0

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
2
1