Exploring View Hierarchy
Controlling view hierarchy is important for architecture, preventing visual and user interaction bugs, and also performance optimizations. Many issues I see related to touch events not handled or layout off from expected. Understanding how view and view controller hierarchies interact and ability to easily navigate through this interconnection will delight your development process.
This article is about how UIView
, UIViewController
, and UIWindow
connect. I explain how to traverse hierarchy of views, view controllers, and the responder chain. We will explore how app’s structure looks with push, modal, and embed presentations. And build the console version of Debug View Hierarchy tool in Xcode.
UI Tools
The Debug View Hierarchy in Xcode captures a snapshot of user interface and allows to navigate and inspect view objects.
Sometimes the console is more convenient. We can use private methods to introspect the UI.
-[UIView recursiveDescription]-[UIViewController _printHierarchy]
You can pause app execution and use expressions in the console to call recursiveDescription
and _printHierarchy
.
(lldb) expr -l objc -O -- [[[UIApplication sharedApplication] keyWindow] recursiveDescription](lldb) expr -l objc -O -- [[[[UIApplication sharedApplication] keyWindow] rootViewController] _printHierarchy]
-l objc -O --
is used to switch context from Swift to Objective-C.
View Hierarchy
Apps often contain many views and more than one view controller, building up two hierarchies:
UIView
hierarchy is a tree with it’s root in a window (UIWindow
is a subclass ofUIView
). We can traverse this tree usingsubviews
andsuperview
properties.UIViewController
hierarchy starts from therootViewController
of a window. View controller can present or be a container for other view controllers, creating two different hierarchies.
Presented View Controllers
When we display view controller using Present Modally
or Preset As Popover
segue or present(_:animated:completion:)
method, we create presenting → presented connection.
Presented view controllers build up a stack of a kind: the last view controller we present is visible and typically dismissed first.
Presented view controllers connect in a doubly linked list with presentedViewController
and presentingViewController
properties.
Container View Controllers
Container view controller manages other view controllers it owns. In a storyboard we use Show
and Show Detail
segue or we can use addChild(_:)
method, creating parent → child connection.
This forms a tree, that can be traversed with children
and parent
properties.
Many common view controllers are containers: UINavigationController
, UITabBarController
, UISplitViewController
, UIPageViewController
.
Combination is possible, i.e. when a container presents a different view controller. But it is not possible for a view controller to be presented and in a container at the same time.
When a container presents we get combination of two structures, presented view controller can form another tree.
Differences between two hierarchies become distinct when it comes to event handling. In a tree siblings are equivalent where in a stack the top has priority.
Responder Chain
UIKit uses the responder chain to receive and handle events. This will help us traverse view and view controller hierarchies.
The responder chain connects responder objects from a view to application delegate:
UIView → UIViewController → UIWindow → UIApplication → UIApplicationDelegate
There can be many views and view controllers in the responder chain. We can traverse the responder chain using the next
property:
- For the view the
next
is it’s superview, or the view controller if the view is the root view; - For the view controller the
next
is it’s presenting view controller or the window.
The responder chain is a list and we can write a simple function to traverse it:
We can use it in LLDB in Swift, but in a rather cumbersome way. We need to import UIKit and the app module first:
(lldb) expr import UIKit
(lldb) expr import <App Module Name>
Then we can take the address of the responder object from one of the previous commands, cast it to UIResponder
, and use the extension:
(lldb) expr print(unsafeBitCast(<UIResponder Address>, UIResponder.self).responderChain)
Note: you may also need to set target language:
(lldb) settings set target.language swift
Or switch context for every expression: expr -l Swift -- import UIKit
You can create a breakpoint somewhere early to automate this:
Common Hierarchies
Let’s take a look at a storyboard with some common kinds of segues and hierarchies they create.
We have push, modal, and embed segue. The initial view controller is UINavigationController
.
We can pause the app execution and introspect view hierarchy, using the console and commands above, to see the structure. I used different class names to better illustrate it.
In the root scene we can track path in the view controller’s tree:
navigation → [ root → [ yellow ] ]
View hierarchy is alike with additional views introduced by navigation controller.
We can see how an event will travel in the responder chain from the yellow view to the app delegate.
Push presentation is implemented by the navigation controller, and we can see this reflected in view controller hierarchy:
navigation → [ root → [ yellow ] , cyan ]
View hierarchy has changed because the navigation controller displays only the top view controller in the navigation stack.
Presented view controller marked with + in the log to distinguish it from the child view controllers:
navigation → [ root → [ yellow ] ] + magenta
View hierarchy contains only views of the visible view controller and some private views.
Building Debug View Hierarchy
View, view controller hierarchies, and responder chain are all the pieces needed to build the Debug View Hierarchy tool. Here how it works:
traverseHierarchy
method is build using variation of the visitor design pattern with closure;- Traversal starts in the window and using Depth First Search goes through view and view controllers in the hierarchy. This way one branch of the hierarchy traversed fully before the next one;
- Only visible views are processed;
- The responder chain rules are followed to connect the view and the view controller.
The traverseHierarchy
can be used to print out the view hierarchy in the console:
(lldb) expr UIApplication.shared.keyWindow?.traverseHierarchy { responder, _ in print(responder) }
By implementing a convenience method:
Voilà:
(lldb) expr UIWindow.printKeyWindowHierarchy()
To better understand view hierarchy I recommend reading documentation and View Controller Programming Guide for iOS, The View Controller Hierarchy.
And add the Debug View Hierarchy in Xcode tool to your workflow.
Hope you find this article useful. Cheers!