Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Programmatically create an NSViewController without an XIB in Swift 3

I'm trying to make a macOS app without using Interface Builder. My project builds and runs, but my primary view controller doesn't seem to be loading its view. That is, the viewDidLoad() method is not invoked. I'm using Xcode-beta 8.0 beta 6 (8S201h).

The Swift 3 documentation for NSViewController says this about the view property:

If this property’s value is not already set when you access it, the view controller invokes the loadView() method. That method, in turn, sets the view from the nib file identified by the view controller’s nibName and nibBundle properties.

If you want to set a view controller’s view directly, set this property’s value immediately after creating the view controller.

And for viewDidLoad:

For a view controller created programmatically, this method is called immediately after the loadView() method completes.

Finally, for isViewLoaded:

A view controller employs lazy loading of its view: Immediately after a view controller is loaded into memory, the value of its isViewLoaded property is false. The value changes to true after the loadView() method returns and just before the system calls the viewDidLoad() method.

So the documentation leads me to believe it is possible.


My project looks like this:

 .
 ├── main.swift
 └── Classes
     ├── AppDelegate.swift
     └── Controllers   
         └── PrimaryController.swift

main.swift

import Cocoa

let delegate = AppDelegate()
NSApplication.shared().delegate = delegate
NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)

AppDelegate.swift

import Cocoa

class AppDelegate: NSObject, NSApplicationDelegate {
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        let pc = PrimaryController(nibName: nil, bundle: nil)!
        print(pc.isViewLoaded)
        pc.view = NSView()
        print(pc.isViewLoaded)
    }
}

PrimaryController.swift

import Cocoa

class PrimaryController: NSViewController {
    override func loadView() {
        print("PrimaryController.loadView")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        print("PrimaryController.viewDidLoad")
    }
}

Building and running a project with the above yields this console output:

false
true

That is, the view loads for the controller, but the loadView and viewDidLoad methods are never invoked.

What am I missing?

like image 523
Nick Avatar asked Sep 04 '16 16:09

Nick


2 Answers

Minimal setup to make ViewController from code (Swift 4.1)

class WelcomeViewController: NSViewController {

   private lazy var contentView = MyCustomClassOrJustNSView()

   override func loadView() {
      view = contentView
   }

   init() {
      super.init(nibName: nil, bundle: nil)
   }

   required init?(coder: NSCoder) {
      fatalError()
   }
}
like image 116
Vlad Avatar answered Nov 17 '22 00:11

Vlad


A careful reading of the documentation is enlightening. Specifically:

If this property’s value is not already set when you access it, the view controller invokes the loadView() method.

Meaning, loadView() is only invoked when an attempt is made to read the view property before the property has a value. Assigning a value to the property directly will bypass the method call.

So if AppDelegate.applicationDidFinishLaunching(_:) is written like this:

    let pc = PrimaryController(nibName: nil, bundle: nil)!
    print(pc.isViewLoaded)
    let x = pc.view // accessing the view property before it has a value
    print(pc.isViewLoaded)

...then loadView will be invoked by the system to attempt to load the view:

false
PrimaryController.loadView
false

Note that viewDidLoad() is still not invoked, since no view can be automatically associated with the controller (ie, the nib is nil and PrimaryController.xib doesn't exist). The correct way to complete the process is to manually load the view in PrimaryController.loadView():

print("PrimaryController.loadView")
view = NSView() // instantiate and bind a view to the controller

Now the program gives the desired result:

false
PrimaryController.loadView
PrimaryController.viewDidLoad
true
like image 26
Nick Avatar answered Nov 17 '22 01:11

Nick