Start by explaining that iOS uses the Cocoa Touch framework, while macOS uses the Cocoa framework! In contrast, the macOS App hierarchy is slightly more complex

Create macOS App

The default ‘main. storyboard’ has three layers: Application Scene, Window Controller Scene and View Controller Scene. Application → (WindowController→Window) → (ViewController→View)

Application → (WindowController→Window) → (ViewController→View)

Application Scene hierarchy

Application Scene

Window Controller Scene hierarchy

Window Controller Scene

View Controller Scene hierarchy

View Controller Scene

The four most commonly used instances are Window/WindowController and View/ViewController! Each Window is operated on

To make it easy to show hierarchy in code, Remove both the Window Controller Scene and View Controller Scene levels from ‘main. storyboard’ (keep the Application Scene level – the top menu bar is fine), Delete viewController.swift as well.

The entry for the project is still ‘main.storyboard’.

Rewrite the Window Controller Scene hierarchy and View Controller Scene hierarchy yourself!

NSViewController MainViewController class MainViewController class MainViewController class MainViewController class MainViewController class MainViewController

Class inheriting from NSViewController named MainViewController

Through the control IBOutlet and IBAction in the ‘.xib’ file, complete the following code configuration:

Through the ‘.xib’ file, complete the configuration of the code

In the “appdelegate. Swift” file:

var mainWC: NSWindowController? / / add the window controller func applicationDidFinishLaunching (_ aNotification: Notification) {//let mainVC = NSViewController()//❌❌ -[NSNib _initWithNibNamed:bundle:options:] could not load the nibName: NSViewController in bundle (null). // You must use the NSViewController class in 'NSNib' form -- //let mainVC = MainViewController() let with 'xib' mainVC = MainViewController(nibName: "MainViewController", bundle: Bundle.main) let window = NSWindow(contentViewController: mainVC) mainWC = NSWindowController(window: Window) // mainvc. myWC = mainWC // The corresponding window is set to optional mainWC? .showWindow(nil) }Copy the code

Note: A. You must use an instance of the NSViewController class in ‘NSNib’ form (as in ‘xib’), Otherwise run error “- [NSNib _initWithNibNamed: bundle: options:] could not load the nibName: NSViewController in bundles (null).” ! B. Set the **contentViewController property to **NSWindow; When creating a mainWC(window controller) with NSWindowController(window: window), determine the connection between mainWC(window controller) and Window (window) **! C. Show mainWC(window controller) via the.showwindow (nil) method!

Effect: Show the view controller (MainViewController class instance), click the button to respond accordingly ~

Now you can use custom view controllers (window controllers can be custom too)!



Window layer use (NSWindow, NSWindowController)

The basic properties of NSWindow and NSWindowController are as follows:

@ objc func clickOneButton () {/ / (window style) NSWindow. StyleMask - first: borderless / / (storage type) NSWindow. BackingStoreType - first: retained let widnow = NSWindow(contentRect: NSMakeRect(100, 100, 300, 200), styleMask: NSWindow.StyleMask.titled, backing: NSWindow.BackingStoreType.buffered, defer: false) print("create a window") DispatchQueue.main.asyncAfter(deadline: Dispatchtime.now () + 2.0) {print("delay after 2s"," delay after 2s") widnow.contentView? .wantsLayer = true widnow.contentView? .layer? .backgroundColor = NSColor .cyan.cgColor DispatchQueue.main.asyncAfter(deadline: Dispatchtime.now () + 2.0) {print("delay after 4s"," delay after 4s") widnow.center ( DispatchQueue.main.asyncAfter(deadline: Dispatchtime.now () + 2.0) {print("delay after 6s"," set ") widnow.title = "title" //if #available(OSX 11.0, dispatchtime.now () + 2.0) {print("delay after 6s"," set ") widnow. *) { // widnow.subtitle = "subtitle 123456" //} } } } let myWC = NSWindowController(); myWC.window = widnow //let myWC = NSWindowController(window: Func addOneClickButton() {let oneBtn = NSButton(frame: NSMakeRect(100, 100, 100, 100)) self.view .addSubview(oneBtn) oneBtn.target = self; oneBtn.action = #selector(clickOneButton) } override func viewDidLoad() { super.viewDidLoad() // Do any additional setup  after loading the view. self .addOneClickButton() }Copy the code

Effect: After running the project, click the ‘Button’ Button — create a window with frame of **(100, 100, 300, 200), delay for 2s and set the background color of contentView to Cyan, then delay for 2s so that the window is in the center of the screen. Set the title of the window to **” title” after 2s delay! Every time you click the ** ‘Button’ Button again, continue to repeat the above series of actions (create window, delay for 2s, delay for 2s again, delay for 2s again)!

More detailed information about NSWindow and NSWindowController basic attribute, please refer to: NSWindow:developer.apple.com/documentati… NSWindowController:developer.apple.com/documentati…

Window object:www.macdev.io/ebook/windo…




Special case of the Windows Controller layer

  • 1. The NSViewController used to create the NSWindow must be an instance of the NSViewController class using the form ‘NSNib’ (such as ‘xib’). Otherwise run error “- [NSNib _initWithNibNamed: bundle: options:] could not load the nibName: NSViewController in bundles (null).” !

    Storyboard: : ViewController: : ViewController: : ViewController: : ViewController: : ViewController: : ViewController: : ViewController

    Complete the configuration of the code

    Create and display an instance of a window controller (NSWindowController) using the following code: contentViewController property is of type NSViewController!

    @IBAction func leftBtnClick(_ sender: Any) {let sub1VC = NSViewController () / / ❌ / / [General] - [NSNib _initWithNibNamed: bundle: options:] could not load the nibName: NSViewController in bundle (null). let sub1Window = NSWindow(contentViewController: sub1VC) sub1WC = NSWindowController(window: sub1Window) sub1WC? .window? . Title = "Left Window"// Set title sub1WC? .showWindow(nil) }Copy the code

    Running, click on the ‘Left’ button complains: [General] – [NSNib _initWithNibNamed: bundle: options:] could not load the nibName: NSViewController in bundle (null).

    Create and display the NSWindowController instance with the following code: contentViewController property is custom but does not use the ‘NSNib’ form of WrongSub1ViewController type!

    Unchecked ‘Also create XIB file for user interface’

    @IBAction func leftBtnClick(_ sender: Any) {let sub1VC = WrongSub1ViewController () / / ❌ / / [General] - [NSNib _initWithNibNamed: bundle: options:] could not load the nibName: MacDealWindow.WrongSub1ViewController in bundle (null). let sub1Window = NSWindow(contentViewController: sub1VC) sub1WC = NSWindowController(window: sub1Window) sub1WC? .window? . Title = "Left Window"// Set title sub1WC? .showWindow(nil) }Copy the code

    Running, click on the ‘Left’ button complains: [General] – [NSNib _initWithNibNamed: bundle: options:] could not load the nibName: MacDealWindow.WrongSub1ViewController in bundle (null).

    Solutions:

    You must use an instance of type NSViewController in the form of ‘NSNib’ as a contentViewController for the window controller. The window controller contentViewController property is custom and uses the form ‘NSNib’ Sub1ViewController type!

    Select ‘Also create XIB file for user interface’

    @IBAction func leftBtnClick(_ sender: Any) { let sub1VC = Sub1ViewController(nibName: "Sub1ViewController", bundle: Bundle.main) //let sub1VC = Sub1ViewController()//✅ sub1VC) sub1WC = NSWindowController(window: sub1Window) sub1WC? .window? . Title = "Left Window"// Set title sub1WC? .showWindow(nil) }Copy the code

    Effect: While running, click the ‘Left’ button – 1. Create a Window titled “Left Window”, 2. No errors!


    Use an instance of the NSWindowController type ‘NSNib’ as a window controller. Create and display an instance of a window controller (NSWindowController) using the following code: The window controller is custom and uses the ‘NSNib’ form **LeftWindowController type **!

    Select ‘Also create XIB file for user interface’

    The window controller type – LeftWindowController:

    ‘LeftWindowController. Swift’ file

    ‘LeftWindowController. Xib file

    @IBAction func leftBtnClick(_ sender: Sub1WC = NSWindowController(windowNibName: "LeftWindowController") //sub1WC = NSWindowController() // System default window controller //sub1WC = LeftWindowController2()// No reaction // Not using 'NSNib' window controller sub1WC? .window? . Title = "Left Window"// Set title sub1WC? .showWindow(nil) }Copy the code

    Effect: While running, click the ‘Left’ button – 1. Create a Window titled “Left Window”, 2. No errors!

    For “//sub1WC = LeftWindowController2()// no response // no window controller in the form of ‘NSNib'” code, The custom **LeftWindowController2 window controller does not use the ** ‘NSNib’ form!

    Unchecked ‘Also create XIB file for user interface’

    Effect of more properties, accessed and set ~ via ‘.window’


  • 2. Use NSWindow delegate (continue with the above code logic) to implement the code as follows:

    //MARK:NSWindowDelegate func windowWillMove(_ notification: Notification) { print("notification.object:\(String(describing: notification.object))") let nowWindow = notification.object; print("windowWillMove nowWindow:\(nowWindow as Any)") } func windowWillMiniaturize(_ notification: Notification) { print("windowWillMiniaturize") } func windowDidBecomeMain(_ notification: Notification) { print("windowDidBecomeMain") } func windowDidResignMain(_ notification: Notification) { print("windowDidResignMain") } var sub1WC: NSWindowController? @IBAction func leftBtnClick(_ sender: Sub1WC = NSWindowController(windowNibName: "LeftWindowController") sub1WC? .window? .delegate = self// set agent sub1WC?. ShowWindow (nil) print("window:\(sub1WC?.window as Any)")}Copy the code

    Effect: While running, click the ‘Left’ button – create a Window with the title “Left Window”! 1. When moving a window, it will call back the **windowWillMove method, 2. Select the project default ViewController window and the window respectively **, When the window window is selected – call back the windowDidBecomeMain method/when the window window is unselected – call back the windowDidResignMain method, 3. Click the minimize button on the left side of the window – call back the windowWillMiniaturize method and windowDidResignMain method, and select the Window window to maximize in the Dock – call back the windowDidBecomeMain** method!

    Note: (_ notification: notification) is returned by the proxy method. Using notification, you can get the window (the window) of the current response operation — notification.object

    let nowWindow = notification.object; // Gets the window for the current response actionCopy the code

    More, please reference: NSWindowDelegate — developer.apple.com/documentati…


  • 3. Use modal – Windows other than the specified window are not operable! (Continue with the above code logic)

    Core method: nsapplication.shared. runModal(for: Window object) – start mode, NSApplication. Shared. StopModal modal ~ () – end in a ‘LeftWindowController. Swift’ file:

    import Cocoa class LeftWindowController: NSWindowController { override func awakeFromNib() { super .awakeFromNib() print("LeftWindowController awakeFromNib") Override func windowDidLoad() {super.windowdidLoad ()// Implement this method to handle any initialization after your window controller's window has been loaded from its nib file. print("LeftWindowController WindowDidLoad ") self.addonebtn ()} func addOneBtn() {let BTN = NSButton(frame: NSMakeRect(100, 100, 100, 100)) btn.target = self; btn.action = #selector(clickBtn) self.window? .contentView! .addSubView (BTN)} @objc func clickBtn() {print("clickBtn") 'DispatchQueue. Main. AsyncAfter' delay action not taken ~ / / DispatchQueue. Main asyncAfter (deadline: Dispatchtime.now () + 2.0) {// print("LeftWindowController ") ") //} print("LeftWindowController -- end mode \(NSDate())") nsApplication.shared.stopmodal ()// End mode}}Copy the code

    In the ‘ViewController. Swift’ :

    import Cocoa class ViewController: NSViewController ,NSWindowDelegate { //MARK:NSWindowDelegate func windowWillClose(_ notification: Notification) {print("windowWillClose") print("windowWillClose end mode \(NSDate())") nsApplication.shared StopModal () modal} / / / / close the window closed because sub1WC response - performed 'window window. Close ()' method (file) in LeftWindowController. Swift override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. } var sub1WC: LeftWindowController? @IBAction func leftBtnClick(_ sender: Sub1WC = LeftWindowController(windowNibName: "LeftWindowController") //sub1WC = LeftWindowController()//❌// No window is created sub1WC? .window? .delegate = self; sub1WC? .showWindow(self) //sub1WC? LoadWindow ()/print/loading window window (sub1WC as Any) DispatchQueue. Main. AsyncAfter (deadline: Dispatchtime.now () + 2.0) {[self] in print(" start mode ") nsApplication. shared. runModal(for: (sub1WC?.window)!) Modal - not to the other Windows / / / / operation after the modal, will continue to implement "DispatchQueue. Main. AsyncAfter 'delay DispatchQueue. Main. AsyncAfter (deadline: Dispatchtime.now () + 2.0) {print(" End mode \(NSDate())") nsApplication.shared.stopmodal ()// end mode}}} @ibAction func rightBtnClick(_ sender: Any) { } override var representedObject: Any? { didSet { // Update the view, if already loaded. } } }Copy the code

    Effect 1: After the operation is successful, the default window A can operate normally. Click the ‘Left’ button to create A new window B, and window A can still operate normally, but after 2s delay, only the new window B can operate (default window A cannot operate). Click the ‘Button’ Button to end the mode and the default window A can be operated again!

    Call ‘Nsapplication.shared.stopmodal ()// End modeThe method of ‘

    Note: ‘LeftWindowController. Swift file after the modal, (the default) in’ ViewController. Swift ‘will continue to implement “DispatchQueue. Main. AsyncAfter’ delay!

    Effect 2: After the operation is successful, the default window A can operate normally. Click the ‘Left’ button to create A new window B, and window A can still operate normally, but after 2s delay, only the new window B can operate (default window A cannot operate). Click the ‘Close’ button in the upper left of the new window B and then end the mode in the **windowWillClose callback method.

    Proxy methodwindowWillClose, call ‘Nsapplication.shared.stopmodal ()// End modeThe method of ‘

    Corresponding OC code:

    [[NSApplication sharedApplication] runModalForWindow:self.userLoginWinodwC.window]; // Start mode - cannot operate to other Windows [[NSApplication sharedApplication] stopModal]; NSModalSession sessionCode = [[NSApplication sharedApplication] beginModalSessionForWindow:self.userLoginWinodwC.window]; //[[NSApplication sharedApplication] endModalSession:sessionCode];Copy the code


  • 4. Null-avoid creating Windows twice! (Continue with the above code logic)

    — 4-1. No null processing — in ‘viewController.swift’ :

    import Cocoa class ViewController: NSViewController ,NSWindowDelegate { override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. } var sub1WC: LeftWindowController? Func createAndShowWindow() {// sub1WC = LeftWindowController(windowNibName: "LeftWindowController") sub1WC? .showWindow(nil) print(sub1WC as Any) } @IBAction func leftBtnClick(_ sender: Any) { self .createAndShowWindow() } @IBAction func rightBtnClick(_ sender: Any) { } override var representedObject: Any? { didSet { // Update the view, if already loaded. } } }Copy the code

    Effect: Click on the ‘Left’ button to create a new window B and click on it multiple times to create a new window B(2/3/4/5…) — Different objects!

    Note: Multiple new window LeftWindowController objects are created

    In ‘viewController.swift’ :

    import Cocoa class ViewController: NSViewController ,NSWindowDelegate { override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. } var sub1WC: LeftWindowController? Sub1WC = sub1WC! Func createAndShowWindow() {sub1WC = sub1WC! = nil ? Sub1WC: LeftWindowController(windowNibName: "LeftWindowController")// Void to avoid creating sub1WC again? .showWindow(nil) print(sub1WC as Any) } @IBAction func leftBtnClick(_ sender: Any) { self .createAndShowWindow() } @IBAction func rightBtnClick(_ sender: Any) { } override var representedObject: Any? { didSet { // Update the view, if already loaded. } } }Copy the code

    Effect: Click the ‘Left’ button the first time to create a new window B, and click multiple times to show only that new window B – same object!

    Note: Multiple new window LeftWindowController objects are not created


  • 5. Customize a prompt window!

    Reference: macOS Warning/prompt View – NSAlert, Custom WindowController


  • 6. Set the size and position of the window using the window’s.setContentSize() and.setFrameOrigin() methods ~

    Initial default size: 480×270 (as shown below, in “main.storyboard”)

    Main.storyboard

    In the ‘viewController.swift’ file:

    override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. DispatchQueue. Main. AsyncAfter (deadline: now () + 3.0) {(self) in / / set the window size, location, the self. The window? .setContentSize(NSMakeSize(300, 300)) self.view.window? .setFrameOrigin(NSMakePoint(100, 100)) //self.view.window? FrameRect (forContentRect: NSMakeRect (100, 100, 300, 300)) / / ❌ ❌ DispatchQueue. Main. AsyncAfter (deadline: .now() + 3.0) {[self] in self.view.window?. SetContentSize (NSMakeSize(500, makesize); self.view.window? 500)) self.view.window?.setFrameOrigin(NSMakePoint(200, 200)) //self.view.window?.frameRect(forContentRect: NSMakeRect(200, 200, 500, 500))//❌❌}}Copy the code

    Effect: After starting the App, the window size is the default 480×270! After 3s delay, the size changes to 300×300 and the starting point coordinates are (100, 100)! After 3s delay, the size becomes 500×500 and the starting point coordinate is (200, 200)!





Use of the VIewController layer (NSVIewController, corresponding View)

The Window layer contains its own VIewController layer, that is, an NSVIewController in a Window. Then you can layout the view on the corresponding self.view

There is another case where you can have more than one NSVIewController~ in a Window

Use the initialized instance of the system’s NSViewController directly:

Let testVC = NSViewController()//❌//Failed to set (contentViewController) user defined inspected property on (NSWindow):  -[NSNib _initWithNibNamed:bundle:options:] could not load the nibName: NSViewController in bundle (null). testVC.view.wantsLayer = true testVC.view.layer? .backgroundColor = NSColor.red.cgColorCopy the code

If you directly use the NSViewController of the system, an error message is displayed: Failed to set (contentViewController) user defined inspected property on (NSWindow): – [NSNib _initWithNibNamed: bundle: options:] could not load the nibName: NSViewController in bundles (null).” And cannot use the view controller!

Customize a view controller (inherited from the NSViewController class) that does not use the ‘NSNib’ form :(GYHNoXibViewController)

Unchecked ‘Also create XIB file for user interface’

Reinitialize the instance to perform the operation:

Let testVC = GYHNoXibViewController()//❌//Failed to set (contentViewController) user defined inspected property on (NSWindow): -[NSNib _initWithNibNamed:bundle:options:] could not load the nibName: MacDealViewController.GYHNoXibViewController in bundle (null). testVC.view.wantsLayer = true testVC.view.layer? .backgroundColor = NSColor.red.cgColorCopy the code

Error: Failed to set (contentViewController) user defined inspected property on (NSWindow): -[NSNib _initWithNibNamed:bundle:options:] could not load the nibName: MacDealViewController. GYHNoXibViewController in bundles (null).” And cannot use the view controller!



This is similar to the case discussed in the Window layer above, where you must use an instance of the NSViewController class in the form of ‘NSNib’ (including ‘xib’ below) :

Use ‘NSNibIn the form oftheNSViewController classInstance –

Customize a view controller (inherited from the NSViewController class) that uses the form ‘NSNib’ :(GYHHasXibViewController)

Select ‘Also create XIB file for user interface’

Reinitialize the instance to perform the operation:

let testVC = GYHHasXibViewController() testVC.view.wantsLayer = true testVC.view.layer? .backgroundColor = NSColor.red.cgColoCopy the code

Conclusion: Using a custom view controller in the form of ‘NSNib’ is error free and usable!

Use a custom view controller that uses the form ‘NSNib’ — (GYHHasXibViewController)

import Cocoa let Button_TAG = 1000 class ViewController: NSViewController { var orderVC: NSViewController? var settingVC: NSViewController? var userVC: NSViewController? var currentVC: NSViewController? Func override func viewDidLoad() {super.viewdidload () // Do any additional setup after loading the view. // Initialize view controller self.orderVc = GYHHasXibViewController() self.ordervc? .view.wantsLayer = true self.orderVC? .view.layer? .backgroundColor = nscolor.red. CgColor // red self.settingVc = GYHHasXibViewController() self.settingVC? .view.wantsLayer = true self.settingVC? .view.layer? Self.uservc = GYHHasXibViewController() self.uservc? .view.wantsLayer = true self.userVC? .view.layer? .backgroundcolor = nscolor.blue. CgColor // blue // addChild view controller VC self.addchild (self.ordervc!) self .addChild(self.userVC!) self .addChild(self.settingVC!) Self.currentvc = self.orderVC// Self.view.addSubView (self.currentVC! .view) let margin: CGFloat = 10.0 let btn_W: CGFloat = 100.0 let btn_H: CGFloat = 30.0 let arr = [" order ", "setup "," personal info "] for I in 0.. <arr.count { let btn = NSButton(frame: NSMakeRect(margin, margin + (margin + btn_H)*CGFloat(i), btn_W, btn_H)) btn.title = arr[i]; btn.tag = i + Button_TAG self.view .addSubview(btn) btn.target = self; btn.action = #selector(clickButton) } } @objc func clickButton(btn: NSButton) {// Click each button switch (btn.tag-button_tag) {case 0: do {//" order "self. new_prensentToVC(toVc: self.orderVC as! GYHHasXibViewController)} Case 1: do {//" set "self. new_prensentToVC(toVc: self.settingVC as! GYHHasXibViewController)} case 2: do {//" personal info "self. new_prensentToVC(toVc: self.userVC as! GYHHasXibViewController) } default: break } } func new_prensentToVC(toVc: GYHHasXibViewController) {if (self.currentVc == toVc) {return GYHHasXibViewController) {if (self.currentVc == toVc) { print("self.currentVC:\(self.currentVC as Any), tovc:\(toVc)") self .transition(from: self.currentVC! , to: toVc, options: NSViewController. TransitionOptions ()) {self. CurrentVC = toVc / / the currently selected VC}} to override the var representedObject: Any? { didSet { // Update the view, if already loaded. } } }Copy the code

Effect: a. Select the corresponding “order “/” Settings “/” Personal information” button, it will turn to the corresponding interface (red/green/blue)! B. Open func transition(from fromViewController: NSViewController, to toViewController: NSViewController, options: NSViewController.TransitionOptions = [], completionHandler completion: (() -> Void)? = nil) ‘method)!

View View hierarchy in “Debug View Hierarchy “:

Tips: Inside the self.view, you can use a child view (as a container view) to put the above custom view controller! (This is similar to how code works in iOS.)

In the code above

self.view .addSubview(self.currentVC! .view)Copy the code

Change for

self.view .addSubview(self.currentVC! .view)// Can be annotated, not annotated // a separate (container) view, let containerV = NSView(frame: NSMakeRect(150, 10, 300, 220)) self.view .addSubview(containerV) containerV .addSubview(self.currentVC! .view)Copy the code

Effect: a. The corresponding custom view controller is placed in the subview (as container view). B. Click the corresponding “Order “/” Settings “/” Personal Information” button to switch to the corresponding interface (red/green/blue)! C. Open func transition(from fromViewController: NSViewController, to toViewController: NSViewController, options: NSViewController.TransitionOptions = [], completionHandler completion: (() -> Void)? = nil) ‘method)!

View View hierarchy in “Debug View Hierarchy “:








goyohol’s essay