iOS7 Subviews Hierarchy

written in ios, ios7, objective-c

In iOS6 a common approach to tweak the appearance of the UIView subclasses (like UISearchBar, UITextField etc.) was to cycle the subviews searching for a given view to alter.

1
2
3
4
5
6
7
8
9
10
11
id viewThatIWantToTweak = nil;
for (UIView *view in self.searchBar.subviews) {
    if ([view isKindOfClass:NSClassFromString(@"UISearchBarTextField")]) {
        viewThatIWantToTweak = view;
        break;
    }
}

if (viewThatIWantToTweak) {
    // do something with viewThatIWantToTweak
}

The subviews property is defined in the in the UIView interface.

1
@property (nonatomic, readonly, copy) NSArray *subviews;

Unfortunately in iOS7 viewThatIWantToTweak in the previous example will always be nil. The view hierarchy has changed for (apparently) the majority of the UI elements. It is no more sufficient to cycle the subviews to effectively retrieve the desired view: it is necessary to search recursively the subviews array.

Here is how inspecting the subviews of UISearchBar looks like on iOS6.

1
2
3
4
5
6
7
8
(lldb) po self.searchBar
<UISearchBar: 0x8251930; frame = (0 20; 320 460); text = ''; autoresize = W+BM; layer = <CALayer: 0x8251a10>>

(lldb) po self.searchBar.subviews
<__NSArrayM 0x824bec0>(
<UISearchBarBackground: 0x8251e50; frame = (0 0; 320 548); userInteractionEnabled = NO; layer = <CALayer: 0x8251f10>>,
<UISearchBarTextField: 0x8252880; frame = (0 0; 0 0); text = ''; clipsToBounds = YES; opaque = NO; gestureRecognizers = <NSArray: 0x82555d0>; layer = <CALayer: 0x8252a50>>
)

And this is the same on iOS7. The views of class UISearchBarBackground and UISearchBarTextField are now at a level deeper.

1
2
3
4
5
6
7
8
9
10
11
12
13
(lldb) po self.searchBar
<UISearchBar: 0x8c3b280; frame = (0 0; 320 480); text = ''; opaque = NO; autoresize = W+BM; gestureRecognizers = <NSArray: 0x8c3bf50>; layer = <CALayer: 0x8c3b3b0>>

(lldb) po self.searchBar.subviews
<__NSArrayM 0x8c3aaf0>(
<UIView: 0x8c3b5c0; frame = (0 0; 320 480); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x8c3b6a0>>
)

(lldb) po [self.searchBar.subviews[0] valueForKey:@"subviews"]
<__NSArrayM 0x8c44ab0>(
<UISearchBarBackground: 0x8c3c440; frame = (0 0; 320 480); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x8c3c580>>,
<UISearchBarTextField: 0x8c3d420; frame = (0 0; 0 0); text = ''; clipsToBounds = YES; opaque = NO; gestureRecognizers = <NSArray: 0x8c421d0>; layer = <CALayer: 0x8c3d620>>
)

I wrote a simple method to search recursively.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- (id)firstSubviewOfClass:(Class)clazz maxDeepnessLevel:(NSInteger)deepness
{
    if (deepness == 0) {
        return nil;
    }

    NSInteger count = deepness;

    NSArray *subviews = self.subviews;

    while (count > 0) {
        for (UIView *v in subviews) {
            if ([v isKindOfClass:clazz]) {
                return v;
            }
        }

        count--;

        for (UIView *v in subviews) {
            UIView *retVal = [v firstSubviewOfClass:clazz maxDeepnessLevel:count];
            if (retVal) {
                return retVal;
            }
        }
    }

    return nil;
}

The resulting code for the original example on UISearchBar will be:

1
2
3
Class searchBarTextFieldClass = NSClassFromString(@"UISearchBarTextField");
id viewThatIWantToTweak = [self.searchBar firstSubviewOfClass:searchBarTextFieldClass
                                             maxDeepnessLevel:3];

Even clearer let me say. The deepness parameter is to avoid exhaustive search since we are dealing with recursion. As far as I experienced, a value of 2 should be enough to do the trick.

Here is the Gist with the UIView category ready to use.


Comments