Can't reach UIWindow

Figure 3.7 of this chapter shows that our view controller should have a superview which is UIWindow, and the text discussion in that section also says so. I tried to confirm this in code, but I don’t get any value for the view’s superview.

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        print(self.view)	// prints an optional UIView
        print(self.view.superview) // prints nil
    }
}

Has anyone else tried this? I’m just confused that the text tells us that every view in the hierarchy should have a superview, with UIWindow being at the root of the hierarchy, but I can’t prove it in code.

Did you check to see in IB that the view is actually embedded in a host view?

If the view is not contained in a host view, then its superview will always be nil.

The term superview was probably used incorrectly in that context.

No, at that point in the lesson the view had not been embedded in a host view. It was at the point just after creating a new project. If you create a brand new project in Xcode using the most basic iOS template, that’s about where the project is at that point. Just replace the boilerplate class code in ViewController.swift with the code I wrote above, and that is pretty much the state of the app.

I think you are right in that the use of the term “superview” was misleading in that context. In that same chapter they had also talked about adding a view to the window, where it becomes a subview of the window. Perhaps they were thinking more about the UIView having a subview relationship to UIWindow, rather than UIWindow being the superview of UIView.

However, being unable to reach UIWindow from a UIView still seems strange to me, even if the view is not embedded within another view in the interface builder. I thought that in most typical UIKit interfaces, all UIViews usually fall within a hierarchy with UIWindow at its root, even if you don’t explicitly nest views within other views in interface builder. That same chapter even says:

“Every application has a single instance of UIWindow that serves as the container for all the views in the application. UIWindow is a subclass of UIView, so the window is itself a view.”

Also, when I run the app in that state, the Debug View Hierarchy shows that the UIView appears to be embedded in a hierarchy with a UIWindow at its root, even though I did not make any changes in the interface builder:

Maybe my attempt to reach UIWindow in code was done incorrectly. Or perhaps there is some disconnect behind the scenes that is blocking me from reaching the UIWindow in this way. Or maybe there is something about the iOS program structure that I am still not understanding fully.

Because the UIKit is doing a lot of magic behind the scenes, accessing the superview or window inside the viewDidLoad function is probably premature.

    override func viewDidLoad() {
        super.viewDidLoad()
        print (self.view)  // view set
        print (self.view.superview) // superview not set yet
        print (self.view.window)    // window not set yet
    }

Try accessing them from elsewhere, where you are sure that they should be set. Here is a hack:

class ViewController: UIViewController {
    override func viewDidLoad() {
        print ("\(type (of:self)): \(#function): begin")
        super.viewDidLoad()
        Self._self = self
        Self.printViewHierarchy ()
        print ("\(type (of:self)): \(#function): end")
    }
    
    private static var _self : ViewController!
    
    static func printViewHierarchy () {
        assert (_self != nil)
        print ("\(type (of:self)): \(#function)")
        print ("\tview:      \(_self.view)")
        print ("\tsuperview: \(_self.view.superview)")
        print ("\twindow:    \(_self.view.window)")
    }
}
//  SceneDelegate.swift
...
    func sceneDidBecomeActive(_ scene: UIScene) {
        // Called when the scene has moved from an inactive state to an active state.
        // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.

        print ("\(type (of:self)): \(#function): begin")
        ViewController.printViewHierarchy()
        print ("\(type (of:self)): \(#function): end")
    }
ViewController: viewDidLoad(): begin
ViewController.Type: printViewHierarchy()
	view:      Optional(<UIView: 0x7f952e6071c0; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x6000007e7e40>>)
	superview: nil
	window:    nil
ViewController: viewDidLoad(): end
SceneDelegate: sceneDidBecomeActive(_:): begin
ViewController.Type: printViewHierarchy()
	view:      Optional(<UIView: 0x7f952e6071c0; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x6000007e7e40>>)
	superview: Optional(<UIDropShadowView: 0x7f952e5056c0; frame = (0 0; 320 568); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x6000007c2500>>)
	window:    Optional(<UIWindow: 0x7f9530a08050; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x6000009a4780>; layer = <UIWindowLayer: 0x6000007ea1c0>>)
SceneDelegate: sceneDidBecomeActive(_:): end

1 Like

Interesting. I never noticed that SceneDelegate.swift file before. It’s been a several years since I messed with UIKit programming and that looks like something new. Checking the Apple docs, I see that the UIScene and UIWindowSceneDelegate stuff were added new in iOS 13. I’ll have to wrap my mind around what these new changes mean.

At first blush, it looks like a lot of the functions that used to be in AppDelegate like the “willEnterForeground” and “didEnterBackground” stuff were stripped out of there and relocated to the SceneDelegate file. Along with some other changes as well.

Thanks for sharing that, @ibex10. What you have shown is quite interesting. You can’t reach the view’s superview or UIWindow from the ViewController’s viewDidLoad method, but you can reach them through the SceneDelegate’s sceneDidBecomeActive method. That makes me curious about the changes made involving SceneDelegate over the last several years.

I also noticed that AppDelegate no longer has a reference to the UIWindow, but SceneDelgate now has it. That’s an interesting change to UIKit that’s happened in recent years.

After some experimenting, I found that a view controller’s viewDidAppear method is a better place than viewDidLoad for making calls to the view’s superview or window. Apparently, by the time iOS calls viewDidAppear your app will have configured the view hierarchy more fully than when viewDidLoad is called.

Try making a new Xcode project from the iOS “App” template (I’m using Xcode 12.5) and paste the following into your ViewController class:

override func viewDidAppear(_ animated: Bool) {
    print("Window is: \(self.view.window)")
    print("Superview is: \(self.view.superview)")
    print("Reach window by chaining calls to superview: \(self.view.superview?.superview?.superview)")
}

All three of these calls worked for me. Note that you may have to go a LOT deeper with the chained superview calls before you reach the window in a more complex project. It’s probably better to just use self.view.window if you want to reach the window.

In conclusion, I was able to confirm that the book’s explanation is accurate. It explains that UIWindow is the ultimate superview of the ViewController’s UIView, which is correct.

My confusion was about how to reach the UIWindow through code from the view controller. Turns out my mistake was trying to reach it too early in the view’s lifecycle. I should have tried to access it from viewDidAppear rather than in viewDidLoad. Thanks ibex10 for your help!

Thank you for the excellent discussion, your findings, and above all for your curiosity.

1 Like

Scenes were introduced in iOS/iPadOS 13. Each scene has a window and they represent an instance of your interface.

In short, the change is because an app can have multiple scenes on the iPad, and each scene needs a window. Having the window in the app delegate meant only having one instance of our interface.

1 Like

Thank you..