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

    }

}

    其實說來說去,這幾行代碼都是完全一樣的,只是用在了不同的地方而已,我們有三個窗口,所以就需要三套這樣體系的文件,當然也就需要三套用於加載的代碼。主窗口要一開始就顯示,所以寫在應用程序代理中,而兩個子窗口是點擊按鈕以後顯示,所以寫在的按鈕的實現文件中。

    關於這四個類的簡單說明基本就到這裏,當然本實例只是爲了說明用法,所以代碼風格上來說並不規範,在實際開發的時候,我們還是應該對這些代碼進行更高層次的封裝,也要對相對應的初始化函數進行改寫,但是說到底實現的功能都是這些,本質上是不變的。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章