In this programming tutorial (or guide, if you want so), I am going to explain a simple way of printing out the entire subviews hierarchy of any UIView (subclass). We are going to accomplish this by creating a Category on UIView, and adding a single recursive method which will do its job of going down through the entire tree-structure of the view.
Now for those of you who are wondering what a ‘category’ is: It’s a neat Obj-C way of adding methods to classes without any need to subclass them. For more details see the Apple’s documentation.
There are several cases when you are in need of finding out some underlaying stuff, for example what the MKMapView consist of; in order to modify certain labels, etc. I think there is really no need to talk about it more.
Here is what the header of our category (file UIView+printSubviews.h) looks like:
#import <Foundation/Foundation.h>
@interface UIView (PrintSubviews)
- (void)printSubviewsWithIndentation:(int)indentation;
@end
The way of declaring a Category in Objective-C is just great and simple, isn’t it? What’s even better is that our method will work with any subclass of UIView (UIScrollView, MKMapView, UITableView, UIButton – to name a few). Now here comes the actual implementation file (UIView+printSubviews.m):
#import "UIView+printSubviews.h"
@implementation UIView (PrintSubviews)
- (void)printSubviewsWithIndentation:(int)indentation {
// Get all the subviews of the current view
NSArray *subviews = [self subviews];
// Loop through the whole subviews array. We are using the plain-old C-like for loop,
// just for its simplicity and also to be provided with the iteration number
for (int i = 0; i < [subviews count]; i++) {
// Get the subview at current index
UIView *currentSubview = [subviews objectAtIndex:i];
// We will create our description using this mutable string
NSMutableString *currentViewDescription = [[NSMutableString alloc] init];
// Indent the actual description to provide visual clue of how deeply is the current view nested
for (int j = 0; j <= indentation; j++) {
[currentViewDescription appendString:@" "];
}
// Construct the actual description string. Note that we are using just index of the current view
// and name of its class, but it's up to you to print anything you are interested in
// (for example the frame property using the NSStringFromCGRect(currentSubview.frame) )
[currentViewDescription appendFormat:@"[%d]: class: '%@'", i, NSStringFromClass([currentSubview class])];
// Log the description string to the console
NSLog(@"%@", currentViewDescription);
// Be good memory citizen
[currentViewDescription release];
// the 'recursiveness' nature of this method. Call it on the current subview, with greater indentation
[currentSubview printSubviewsWithIndentation:indentation+1];
}
}
@end
I guess there is nothing more to explain, really. It’s that simple. Now I will show you an example of usage, along with the output that this method produces. I will call it on an instance of MKMapView with some annotations, one of them selected (therefore displaying the callout view).
Just a note for the less experienced of you: You need to import the header file of this category in each class where you are going to use it, like this:
#import "UIView+printSubviews.h"
then call the -printSubviewsWithIndentation: method on the view you wish to inspect:
NSLog(@"*** Printing out all the subviews of MKMapView ***");
[mapView printSubviewsWithIndentation:0];
and that’s what the output looks like (in my case):
Now you can see all the behind-the-scenes views that the mapView consists of. And the numbers in square brackets in front of every line are exactly what they suggest to be - indexes of the subviews in its superview’s subview array.
You can for example retrieve the MKAnnotationContainerView using the following statement:
UIView *annotationContainerView = [[[[[[mapView subviews] objectAtIndex:0] subviews] objectAtIndex:0] subviews] objectAtIndex:1];
This is of course not very safe. It’s generally not a good idea to depend on the particular order of subviews. So you should always test if the [subviews count] > 0, and perhaps use the NSClassFromString() in a similar manner like this:
for (UIView *subview in subviews) {
if ([subview isKindOfClass:NSClassFromString(@"NameOfTheClass")]) {
// We found the subview that we want
}
}
The last thing I wanted to do is to warn you: Apple discourages developers from doing this, because subviews of an UIKit class are considered to be private, and therefore they can change in future updates of the framework. So if you decide to modify or alter any of the subview, you should do it only when there is no other choice. And you should retrieve the view as defensively as possible, and be prepared for the scenarios when the code doesn’t get the view it wanted.
I hope this post was helpful to you guys. You are welcome to point out any weaknesses or suggest any improvements using the comment section below.