macOS开发中NSWindow, NSWindowController, NSView, NSViewController的关系

    macOS使用的Cocoa框架,的确没有iOS使用的Cocoa Touch那么智能好用。有些地方逻辑很奇怪,还有一些看似很正常的功能它却没有提供,还需要自定义。这里就有一个很头疼的问题,关于这四个类的问题,他们之间到底是什么关系,如果摆脱了storyboard如何用代码实现?今天就来简单介绍一下。

    Xcode所提供的默认模板包括一个WindowController,还有一个ViewController,在ViewController中还有一个View,我们的控件一般都写在这个View中。而起始,storyboard把一个逻辑给简化了,关于Window,WindowController,View和ViewController,这四个类可以说是相互依存的。

    如果我们不使用storyboard,那么程序就会去读取AppDelegate中的代码(如果是用默认模板的话,把storyboard删除之后要记得在设置中把默认storyboard删除)。我们应用程序显示的第一个窗口就需要在此定义。由于Cocoa框架严格遵守着MVC模式,因此,要想在屏幕上显示一个窗口,那么一定就要拥有模型,视图和对应的控制器。那么,既然是要生成一个窗口,我们就需要一个NSWindow或其子类的实例。NSWindow有这样一个初始化函数:

public convenience init(contentViewController: NSViewController)

这里的意思是说,我们要一个窗口,那么窗口里究竟显示什么东西,是需要一个ViewController说了算的,所以我们还需要一个ViewController,而ViewController有这样一个构造函数:

public init?(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)

    既然有了视图控制器,那一定是用来显示视图的,那视图在哪里呢?一般是用xib文件(编译之后就成为nib文件)来编辑的,所以调用这个方法就可以加载nib文件。当然,如果你的View是用代码定义的,那在这里给两个参数传空就可以了,然后操作NSViewController的一个属性来改变它的视图:

open var view: NSView

    之后,有了window,我们还得需要一个控制器来把这个窗口显示在屏幕上(目前为止所有的数据还都是内存数据而已,我们还需要调用显示方法),所以就用到了NSWIndowController,它提供了一个构造函数:

public init(window: NSWindow?)

    这样就齐全了,我们可以看到,NSWindowController里会包含一个它要控制的NSWindow,而NSWindow需要一个NSViewController来管理具体显示的视图,最后还需要一个具体的NSView。当我们准备齐全这些以后,就可以调用NSWindowController的一个方法,显示窗口:

@IBAction open func showWindow(_ sender: Any?)

    关于这里的sender,官方的解释是动作的发起者,一般是应用程序代理,但是本人尝试,其实填什么都好像不影响结果,哪怕是nil,也可以正常显示。具体的含义还有待继续摸索。

    还有就是关于storyboard的建议,其实在做macOS开发的时候,storyboard并不好用,不像在iOS开发时那样得心应手,所以还是建议把视图的设计用xib,然后关于控制器的部分用代码来书写。但是也并不建议直接把storyboard删掉,因为用它来编辑状态栏的下拉菜单还是非常方便地,所以本人的做法是在storyboard中只留一个file menu,把其他的视图和控制器都删除掉。当然这样的话,在项目设置处的入口storyboard就必须还得是Main才行。

    接下来用一个具体的例子来说明上面的这一系列问题。我们制作一个简单的应用程序,它的主界面有两个按钮,当点击第一个按钮的时候创建一个新的窗口,当点击第二个按钮的时候也创建一个新的窗口,同时还关闭主窗口。

    分析上面的要求,我们肯定是需要3套内容,每一套里都应该含有一个WIndowController,一个Window,一个ViewController和一个View。

    首先,创建一个Cocoa工程

    然后建立工程:

    删除storyboard里的视图和控制器:

    再删除已经作废的ViewController源文件:

    之后,我们创建三个ViewController以及xib文件,Command+N,选择Cocoa Class,输入mainViewController,勾选xib文件:

    然后用同样的方法生成sub1ViewController和sub2ViewController:

    对于sub1和sub2,我们只是能够区分就好,所以在xib里随便拖个控件什么的,能认清楚就行,而对于mainViewController.xib,我们需要两个按钮,并且还要关联到mainViewController.swift中点击方法,这里不再赘述,完成之后的效果如下:

    我们来重点编辑这两个函数,这里有一点需要注意的是,WindowController必须持久存在,否则会造成窗口闪退的现象,所以,我们要确保WindowController时刻存在一个引用,这样它才不会被ARC回收掉,那么最好的办法就是让他成为一个成员变量,这样就可以保持引用。而至于其他的对象,在WindowController内部会保持连接,所以只要WindowController在,它们就一定在,所以我们用局部变量来作为一个“接力手”就可以了。

    比如说我们要在btn1Click(_:)方法中显示视图1,那么首先要有一个ViewController来加载对应的xib文件,然后要创建一个窗口关联它,再把它给到WindowController中就可以了,具体代码如下:

//

//  mainViewController.swift

import Cocoa

class mainViewController: NSViewController {

    override func viewDidLoad() {

        super.viewDidLoad()

        // Do view setup here.

    }

    open var windowController: NSWindowController?

    var sub1WindowController: NSWindowController?

    @IBAction func btn1Click(_ sender: NSButton) {

        // 创建视图控制器,加载xib文件

        let sub1ViewController = NSViewController(nibName: "sub1ViewController", bundle: Bundle.main)

        // 创建窗口,关联控制器

        let sub1Window = sub1ViewController != nil ? NSWindow(contentViewController: sub1ViewController!) : nil

        // 初始化窗口控制器

        sub1WindowController = NSWindowController(window: sub1Window)

        // 显示窗口

        sub1WindowController?.showWindow(nil)

    }

    var sub2WindowController: NSWindowController?

    @IBAction func btn2Click(_ sender: NSButton) {

        // 同上

        let sub2ViewController = NSViewController(nibName: "sub2ViewController", bundle: Bundle.main)

        let sub2Window = sub2ViewController != nil ? NSWindow(contentViewController: sub2ViewController!) : nil

        sub2WindowController = NSWindowController(window: sub2Window)

        sub2WindowController?.showWindow(nil)

        // 加上一句关闭当前窗口

        windowController?.close()

    }

    

}

    需要说明的一点是,由于关闭窗口是WindowController管的,所以要想在ViewController里操作的话,就需要传入进来才行,所以这里的成员windowController就是如此。

    虽然我们这个逻辑实现了,但是现在打开应用程序还是没有窗口的,因为我们主窗口还没有显示出来,所以我们还需要在应用程序加载完毕后加载主窗口,所以还要在AppDelegate中对mainView实现一个相同的功能:

//

//  AppDelegate.swift

import Cocoa

@NSApplicationMain

class AppDelegate: NSObject, NSApplicationDelegate {

    var mainWindowController: NSWindowController?

    func applicationDidFinishLaunching(_ aNotification: Notification) {

        // Insert code here to initialize your application

        let mainViewController_ = mainViewController(nibName: "mainViewController", bundle: Bundle.main)

        let mainWindow = mainViewController_ != nil ? NSWindow(contentViewController: mainViewController_!) : nil

        mainWindowController = NSWindowController(window: mainWindow)

        mainViewController_?.windowController = mainWindowController

        mainWindowController?.showWindow(nil)

    }

    func applicationWillTerminate(_ aNotification: Notification) {

        // Insert code here to tear down your application

    }

}

    其实说来说去,这几行代码都是完全一样的,只是用在了不同的地方而已,我们有三个窗口,所以就需要三套这样体系的文件,当然也就需要三套用于加载的代码。主窗口要一开始就显示,所以写在应用程序代理中,而两个子窗口是点击按钮以后显示,所以写在的按钮的实现文件中。

    关于这四个类的简单说明基本就到这里,当然本实例只是为了说明用法,所以代码风格上来说并不规范,在实际开发的时候,我们还是应该对这些代码进行更高层次的封装,也要对相对应的初始化函数进行改写,但是说到底实现的功能都是这些,本质上是不变的。


版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/fl2011sx/article/details/73252859