如何在 iOS 中列出 uiviewcontroller 中的所有子视图?

我想在 UIViewController中列出所有的子视图。我尝试了 self.view.subviews,但并不是所有的子视图都被列出来,例如,在 UITableViewCell的子视图没有找到。知道吗?

100034 次浏览

The reason the subviews in a UITableViewCell are not printed is because you must be outputting all the subviews in the top level. The subviews of the cell are not the direct subviews of your view.

In order to get the UITableViewCell's subviews, you need to determine the which subviews belong to a UITableViewCell (using isKindOfClass:) in your print loop and then loop through it's subviews

Edit: This blog post on Easy UIView Debugging may potentially help

self.view.subviews maintain the heirarchy of views.To get subviews of uitableviewcell you have to do something like below.

 for (UIView *subView in self.view.subviews) {
if ([subView isKindOfClass:[UITableView class]]) {
for (UIView *tableSubview in subView.subviews) {
.......
}
}
}

You have to recursively iterate the sub views.

- (void)listSubviewsOfView:(UIView *)view {
    

// Get the subviews of the view
NSArray *subviews = [view subviews];


for (UIView *subview in subviews) {
        

// Do what you want to do with the subview
NSLog(@"%@", subview);


// List the subviews of subview
[self listSubviewsOfView:subview];
}
}

You need to print recursively, this method also tabs based on the depth of the view

-(void) printAllChildrenOfView:(UIView*) node depth:(int) d
{
//Tabs are just for formatting
NSString *tabs = @"";
for (int i = 0; i < d; i++)
{
tabs = [tabs stringByAppendingFormat:@"\t"];
}


NSLog(@"%@%@", tabs, node);


d++; //Increment the depth
for (UIView *child in node.subviews)
{
[self printAllChildrenOfView:child depth:d];
}


}

You could try a fancy array trick, like:

[self.view.subviews makeObjectsPerformSelector: @selector(printAllChildrenOfView)];

Just one line of code. Of course, you might need to adjust your method printAllChildrenOfView to not take any parameters or make a new method.

The xcode/gdb built-in way to dump the view hierarchy is useful -- recursiveDescription, per http://developer.apple.com/library/ios/#technotes/tn2239/_index.html

It outputs a more complete view hierarchy which you might find useful:

> po [_myToolbar recursiveDescription]


<UIToolbarButton: 0xd866040; frame = (152 0; 15 44); opaque = NO; layer = <CALayer: 0xd864230>>
| <UISwappableImageView: 0xd8660f0; frame = (0 0; 0 0); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0xd86a160>>

I'm a bit late to the party, but a bit more general solution:

@implementation UIView (childViews)


- (NSArray*) allSubviews {
__block NSArray* allSubviews = [NSArray arrayWithObject:self];


[self.subviews enumerateObjectsUsingBlock:^( UIView* view, NSUInteger idx, BOOL*stop) {
allSubviews = [allSubviews arrayByAddingObjectsFromArray:[view allSubviews]];
}];
return allSubviews;
}


@end

I use this way:

NSLog(@"%@", [self.view subviews]);

in the UIViewController.

I wrote a category to list all views held by a view controller which inspired by the answers posted before.

@interface UIView (ListSubviewHierarchy)
- (NSString *)listOfSubviews;
@end


@implementation UIView (ListSubviewHierarchy)
- (NSInteger)depth
{
NSInteger depth = 0;
if ([self superview]) {
deepth = [[self superview] depth] + 1;
}
return depth;
}


- (NSString *)listOfSubviews
{
NSString * indent = @"";
NSInteger depth = [self depth];


for (int counter = 0; counter < depth; counter ++) {
indent = [indent stringByAppendingString:@"  "];
}


__block NSString * listOfSubviews = [NSString stringWithFormat:@"\n%@%@", indent, [self description];


if ([self.subviews count] > 0) {
[self.subviews enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
UIView * subview = obj;
listOfSubviews = [listOfSubviews stringByAppendingFormat:@"%@", [subview listOfSubviews]];
}];
}
return listOfSubviews;
}
@end

To list all views held by a view controller, just NSLog("%@",[self listOfSubviews]), which self means the view controller itself. Though it's not quit efficient.

Plus, you can use NSLog(@"\n%@", [(id)self.view performSelector:@selector(recursiveDescription)]); to do the same thing, and I think it's more efficient than my implementation.

Here is the swift version

 func listSubviewsOfView(view:UIView){


// Get the subviews of the view
var subviews = view.subviews


// Return if there are no subviews
if subviews.count == 0 {
return
}


for subview : AnyObject in subviews{


// Do what you want to do with the subview
println(subview)


// List the subviews of subview
listSubviewsOfView(subview as UIView)
}
}

Simple Swift example:

 var arrOfSub = self.view.subviews
print("Number of Subviews: \(arrOfSub.count)")
for item in arrOfSub {
print(item)
}

Alternatively if you wanted to return an array of all subviews (and nested subviews) from a UIView Extension:

func getAllSubviewsRecursively() -> [AnyObject] {
var allSubviews: [AnyObject] = []


for subview in self.subviews {
if let subview = subview as? UIView {
allSubviews.append(subview)
allSubviews = allSubviews + subview.getAllSubviewsRecursively()
}
}


return allSubviews
}

Swift 2.0 compatible

Here a recursive method to obtains all subviews of a generic view:

extension UIView {
func subviewsList() -> [UIView] {
var subviews = self.subviews
if subviews.count == 0 {
return subviews + []
}
for v in subviews {
subviews += v.listSubviewsOfView()
}
return subviews
}
}

So you can call everywhere in this way:

let view = FooController.view
let subviews = view.subviewsList()

enter image description here

Shortest solution

for subview in self.view.subviews {
print(subview.dynamicType)
}

Result

UIView
UIView
UISlider
UISwitch
UITextField
_UILayoutGuide
_UILayoutGuide

Notes

  • As you can see, this method does not list the subviews recursively. See some of the other answers for that.

Elegant recursive solution in Swift:

extension UIView {


func subviewsRecursive() -> [UIView] {
return subviews + subviews.flatMap { $0.subviewsRecursive() }
}


}

You can call subviewsRecursive() on any UIView:

let allSubviews = self.view.subviewsRecursive()

A C# Xamarin version:

void ListSubviewsOfView(UIView view)
{
var subviews = view.Subviews;
if (subviews.Length == 0) return;


foreach (var subView in subviews)
{
Console.WriteLine("Subview of type {0}", subView.GetType());
ListSubviewsOfView(subView);
}
}

Alternatively, if you want to find all the subviews of a specific type I use:

List<T> FindViews<T>(UIView view)
{
List<T> allSubviews = new List<T>();
var subviews = view.Subviews.Where(x =>  x.GetType() == typeof(T)).ToList();


if (subviews.Count == 0) return allSubviews;


foreach (var subView in subviews)
{
allSubviews.AddRange(FindViews<T>(subView));
}


return allSubviews;


}

I have done it in a category of UIView just call the function passing the index to print them with a nice tree format. This is just another option of the answer posted by James Webster.

#pragma mark - Views Tree


- (void)printSubviewsTreeWithIndex:(NSInteger)index
{
if (!self)
{
return;
}




NSString *tabSpace = @"";


@autoreleasepool
{
for (NSInteger x = 0; x < index; x++)
{
tabSpace = [tabSpace stringByAppendingString:@"\t"];
}
}


NSLog(@"%@%@", tabSpace, self);


if (!self.subviews)
{
return;
}


@autoreleasepool
{
for (UIView *subView in self.subviews)
{
[subView printViewsTreeWithIndex:index++];
}
}
}

I hope it helps :)

In my way, UIView's category or extension is much better than others and recursive is the key point to get all subviews

learn more:

https://github.com/ZhipingYang/XYDebugView

enter image description here

Objective-C

@implementation UIView (Recurrence)


- (NSArray<UIView *> *)recurrenceAllSubviews
{
NSMutableArray <UIView *> *all = @[].mutableCopy;
void (^getSubViewsBlock)(UIView *current) = ^(UIView *current){
[all addObject:current];
for (UIView *sub in current.subviews) {
[all addObjectsFromArray:[sub recurrenceAllSubviews]];
}
};
getSubViewsBlock(self);
return [NSArray arrayWithArray:all];
}
@end

example

NSArray *views = [viewController.view recurrenceAllSubviews];

Swift 3.1

extension UIView {
func recurrenceAllSubviews() -> [UIView] {
var all = [UIView]()
func getSubview(view: UIView) {
all.append(view)
guard view.subviews.count>0 else { return }
view.subviews.forEach{ getSubview(view: $0) }
}
getSubview(view: self)
return all
}
}

example

let views = viewController.view.recurrenceAllSubviews()

directly, use sequence function to get all subviews

let viewSequence = sequence(state: [viewController.view]) { (state: inout [UIView] ) -> [UIView]? in
guard state.count > 0 else { return nil }
defer {
state = state.map{ $0.subviews }.flatMap{ $0 }
}
return state
}
let views = viewSequence.flatMap{ $0 }

Details

  • Xcode 9.0.1, Swift 4
  • Xcode 10.2 (10E125), Swift 5

Solution

extension UIView {
private func subviews(parentView: UIView, level: Int = 0, printSubviews: Bool = false) -> [UIView] {
var result = [UIView]()
if level == 0 && printSubviews {
result.append(parentView)
print("\(parentView.viewInfo)")
}


for subview in parentView.subviews {
if printSubviews { print("\(String(repeating: "-", count: level))\(subview.viewInfo)") }
result.append(subview)
if subview.subviews.isEmpty { continue }
result += subviews(parentView: subview, level: level+1, printSubviews: printSubviews)
}
return result
}
private var viewInfo: String { return "\(classForCoder), frame: \(frame))" }
var allSubviews: [UIView] { return subviews(parentView: self) }
func printSubviews() { _ = subviews(parentView: self, printSubviews: true) }
}

Usage

 view.printSubviews()
print("\(view.allSubviews.count)")

Result

enter image description here

This a rewriting of this answer:

You must first get the pointer/reference to the object you intend to print all its subviews. Sometimes you may find it easier to find that object by accessing it through its subview. Like po someSubview.superview. This will give you something like:

Optional<UIView>
▿ some : <FacebookApp.WhatsNewView: 0x7f91747c71f0; frame = (30 50; 354 636); clipsToBounds = YES; layer = <CALayer: 0x6100002370e0>>
  • FaceBookApp is your app name
  • WhatsNewView is the type of your superview
  • 0x7f91747c71f0 is the pointer to the superview.

To print the superView, you must use breakpoints.


Now to do this step you could just click on the 'view debug hierarchy'. No need for breakpoints

enter image description here

Then you could easily do:

po [0x7f91747c71f0 recursiveDescription]

which for me returned something like:

<FacebookApp.WhatsNewView: 0x7f91747c71f0; frame = (30 50; 354 636); clipsToBounds = YES; layer = <CALayer: 0x6100002370e0>>
| <UIStackView: 0x7f91747c75f0; frame = (45 60; 264 93); layer = <CATransformLayer: 0x610000230ec0>>
|    | <UIImageView: 0x7f916ef38c30; frame = (10.6667 0; 243 58); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x61000003b840>>
|    | <UIStackView: 0x7f91747c8230; frame = (44.6667 58; 174.667 35); layer = <CATransformLayer: 0x6100006278c0>>
|    |    | <FacebookApp.CopyableUILabel: 0x7f91747a80b0; baseClass = UILabel; frame = (44 0; 86.6667 16); text = 'What's New'; gestureRecognizers = <NSArray: 0x610000c4a770>; layer = <_UILabelLayer: 0x610000085550>>
|    |    | <FacebookApp.CopyableUILabel: 0x7f916ef396a0; baseClass = UILabel; frame = (0 21; 174.667 14); text = 'Version 14.0.5c Oct 05, 2...'; gestureRecognizers = <NSArray: 0x610000c498a0>; layer = <_UILabelLayer: 0x610000087300>>
| <UITextView: 0x7f917015ce00; frame = (45 183; 264 403); text = '   •   new Adding new feature...'; clipsToBounds = YES; gestureRecognizers = <NSArray: 0x6100000538f0>; layer = <CALayer: 0x61000042f000>; contentOffset: {0, 0}; contentSize: {264, 890}>
|    | <<_UITextContainerView: 0x7f9170a13350; frame = (0 0; 264 890); layer = <_UITextTiledLayer: 0x6080002c0930>> minSize = {0, 0}, maxSize = {1.7976931348623157e+308, 1.7976931348623157e+308}, textContainer = <NSTextContainer: 0x610000117b20 size = (264.000000,340282346638528859811704183484516925440.000000); widthTracksTextView = YES; heightTracksTextView = NO>; exclusionPaths = 0x61000001bc30; lineBreakMode = 0>
|    |    | <_UITileLayer: 0x60800023f8a0> (layer)
|    |    | <_UITileLayer: 0x60800023f3c0> (layer)
|    |    | <_UITileLayer: 0x60800023f360> (layer)
|    |    | <_UITileLayer: 0x60800023eca0> (layer)
|    | <UIImageView: 0x7f9170a7d370; frame = (-39 397.667; 36 2.33333); alpha = 0; opaque = NO; autoresize = TM; userInteractionEnabled = NO; layer = <CALayer: 0x60800023f4c0>>
|    | <UIImageView: 0x7f9170a7d560; frame = (258.667 -39; 2.33333 36); alpha = 0; opaque = NO; autoresize = LM; userInteractionEnabled = NO; layer = <CALayer: 0x60800023f5e0>>
| <UIView: 0x7f916ef149c0; frame = (0 587; 354 0); layer = <CALayer: 0x6100006392a0>>
| <UIButton: 0x7f91747a8730; frame = (0 0; 0 0); clipsToBounds = YES; opaque = NO; layer = <CALayer: 0x610000639320>>
|    | <UIButtonLabel: 0x7f916ef00a80; frame = (0 -5.66667; 0 16); text = 'See More Details'; opaque = NO; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x610000084d80>>

as you must have guessed my superview has 4 subviews:

  • a stackView (the stackView itself has an image and another stackView(this stackView has 2 custom labels))
  • a textView
  • a view
  • a button

This is fairly new to me, but has helped me debug a my views' frames (and text and type). One of my subviews wasn't showing up on the screen, so used recursiveDescription and I realized the width of my one of my subView's was 0... so I went corrected its constraints and the subview was appearing.

If all you want is an array of UIViews, this is a one liner solution (Swift 4+):

extension UIView {
var allSubviews: [UIView] {
return self.subviews.reduce([UIView]()) { $0 + [$1] + $1.allSubviews }
}
}
- (NSString *)recusiveDescription:(UIView *)view
{
NSString *s = @"";
NSArray *subviews = [view subviews];
if ([subviews count] == 0) return @"no subviews";


for (UIView *subView in subviews) {
s = [NSString stringWithFormat:@"<%@; frame = (%f %f : %f %f) \n ",NSStringFromClass([subView class]), subView.frame.origin.x, subView.frame.origin.y ,subView.frame.size.width, subView.frame.size.height];
[self recusiveDescription:subView];
}
return s;
}