NSWindowController窗口控制器主要用于管理xib/storyboard文件中加载的NSWindow对象:1、创建一个基于xib或storyboard的NSWindowController子类会自动创建一个NSWindow;2、如果手工创建NSWindow对象,则需要维护NSWindowController和NSWindow之间的双向绑定关系。
本节内容可参考 :https://korgs.blog.csdn.net/article/details/142673123
NSWindow与NSControllerWindow
方案1:使用UI设计器
创建一个Cocoa class,然后选择 Also create XIB file for user interface
,父类选择NSWindowController
;
方案2:切换UI设计器实现
可单独创建一个xib或storyboard视图,然后创建NSWindowController的子类,再手工绑定。
NSWindow 窗口自定义
编程实现NSWindow窗口
首先需要了解下NSWindow的调用过程,显示window的过程大概如下:
- 初始化:NSApplication加载xib或stroyboard文件,创建window对象;
- 显示:调用window.showWindow()方法显示窗口,再调用makeKey()方法使当前的窗口标识为keyWindow;也可以调用makeKeyAndOrderFront()方法,makeKeyAndOrderFront方法相当于orderFront()+makeKey()的组合;
- 关闭:最后不需要时,可调用window.close()方法,内部执行 orderOut() 方法;
代码实现
//创建一个子类
class CustomWindow: NSWindow {override init(contentRect: NSRect, styleMask style: NSWindow.StyleMask, backing bufferingType: NSWindow.BackingStoreType, defer flag: Bool) {super.init(contentRect: contentRect, styleMask: style, backing: bufferingType, defer: flag)}deinit {Swift.print("release object")}override func orderFront(_ sender: Any?) {super.orderFront(sender)}override func orderOut(_ sender: Any?) {super.orderOut(sender)}override func makeKeyAndOrderFront(_ sender: Any?) {super.makeKeyAndOrderFront(sender)}override func makeKey() {super.makeKey()}
}
使用自定义NSWindow窗口
下列代码运行程序后会存在一个小问题,也是在本系列专题中第二篇文章中遗留的一个问题,即编码创建的Window只能创建和关闭一次,再次创建时会被crash,原因是关闭后App中的对象不再持有此NSWindow实例的引用被GC掉了,这个过程人为干预不了,是框架底层的机制,但是可以修复。
lazy var myWindow: CustomWindow = {let frame = CGRect(x: 0, y: 0, width: 400, height: 280)let style : NSWindow.StyleMask = [NSWindow.StyleMask.titled,NSWindow.StyleMask.closable,NSWindow.StyleMask.resizable]//创建windowlet window = CustomWindow(contentRect:frame, styleMask:style, backing:.retained, defer:true)//创建双向绑定关系//window.windowController = self.myWindowController//self.myWindowController.window = window;window.title = "New Create Window"return window}()@IBAction func createWindow(_ sender: NSButton) {self.myWindow.makeKeyAndOrderFront(self)self.myWindow.center()}
所以为了修复上面的问题,需要绑定双方的关系,即需要把上面注释的两行代码打开。
lazy var myWindowController: NSWindowController = {let wondowVC = NSWindowController()return wondowVC}()
NSWindow 窗口显示切换
如果有多个NSWindow应如何控制显示和切换呢?比如下面的例子,首先设计UI,把storyboard id分别设置成LoginMVC和MainAppMvc
默认Window设置
这个功能同 visible at Lanunch 一样。只不过这里换代码来实现;
@main
class AppDelegate: NSObject, NSApplicationDelegate {//定义当前的windowControllervar loginWVC: NSWindowController?func applicationDidFinishLaunching(_ aNotification: Notification) {//得到Main.storyboard对象,显示初始化窗口let sb = NSStoryboard(name: NSStoryboard.Name(rawValue: "Main"), bundle: nil)loginWVC = sb.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier(rawValue: "LoginWVC")) as? NSWindowControllerloginWVC?.showWindow(self)}func applicationWillTerminate(_ aNotification: Notification) {// Insert code here to tear down your application}func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {return true}
}
切换Window显示
点击 switch window 按钮后会隐藏当前窗口,显示新窗口。
class ViewController: NSViewController {var mainAppWVC: NSWindowController?override func viewDidLoad() {super.viewDidLoad()}override var representedObject: Any? {didSet {}}@IBAction func loginOk(_ sender: NSButton) {//得到Main.storyboard对象,let sb = NSStoryboard(name: NSStoryboard.Name(rawValue: "Main"), bundle: nil)self.view.window?.close() //因为默认是双向绑定了,所以可以直接取mainAppWVC = sb.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier(rawValue: "MainAppWVC")) as? NSWindowControllermainAppWVC?.showWindow(self)}
}
如果这两个窗口来回切换时,有时会有选择状态的保持功能,默认是不开启的,如果发生焦点丢失情况,可以注册NSWindowDidBecomeMain或NSWindowDidBecomeKey通知事件,然后调用window.makeFirstResponder方法重新获得响应者,比如:
selft.makeFirstResponder(self.tableView)
Storyboard工程结构解析
xib工程的设计模式大概如下:
采用了二层设计结构:
- AppDelegate:负责代理Application;
- NSWindowController:负责代理NSWindow;
上面这种设计是把所有的控制全部交给了AppDelegate代理,从架构角度来讲AppDelegate只对Application负责就可以了,不需要拥有那么多的职责,所以优化一下,可进行如下拆分为三层结构。
三层设计结构,同时拆分controller和view采用MVC设计模式实现:
- AppDelegate:负责代理Application;
- NSWindowController:负责代理NSWindow;
- NSViewController:负责代理NSView(后面章节会讲);
因此一个XIB工程,代码可以这样来改造一下:
AppDelegate.swift
包含了 NSWindowController
对象。
class AppDelegate: NSObject, NSApplicationDelegate {//引用一个自定义的NSWindowControllerlazy var windowController: AppMainWindowController = {let wondowVC = AppMainWindowController()return wondowVC}()func applicationDidFinishLaunching(_ aNotification: Notification) {self.windowController.showWindow(self)}
}
AppWindowController.swift
包含了 NSViewController
对象。这个文件创建时要关联一个xib/storyboard,也可以用代码实现。
class AppMainWindowController: NSWindowController {//引用一个自定义的NSViewControllerlazy var viewController: AppViewController = {let vc = AppViewController()return vc}()override func windowDidLoad() {super.windowDidLoad()self.contentViewController = self.viewController}//返回window的xib文件名override var windowNibName: NSNib.Name? { return NSNib.Name("AppMainWindowController") }
}
AppViewController.swift
这个文件创建时要关联一个xib/storyboard,也可以用代码实现。
class AppViewController: NSViewController {override func viewDidLoad() {super.viewDidLoad()// Do view setup here.}
}
最后对XIB进行一些改造;
上述这种结构就是storyboard的原理,所以我们创建工程时建议使用storyboard而不是xib。Xcode快捷创建窗口如下图所示:
实际开发时,下述三个类一般都需要自定义扩展。
NSApp对象简介
这个对象是个全局对象,比如一些以下操作
点击Dock 需要显示窗口
//--AppDelegate/**点击Dock 需要显示窗口,要实现这个方法*/func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {if flag == false {// 1. 获取到我们的App 窗口let win = NSApp.window(withWindowNumber: windowNumber)// 2. 让窗口显示win?.makeKeyAndOrderFront(nil)return true}return !flag}
ViewController控制App
// 关闭应用@IBAction func closeApp(_ sender: Any) {NSApp.terminate(self)}//显示App的数字提醒@IBAction func showAppNumber(_ sender: Any) {NSApp.dockTile.badgeLabel = "20"}//docker跳动@IBAction func jump(_ sender: Any) {/**criticalRequest // 多次跳动App Dock 上的图标,直到用户选中App为活动状态informationalRequest // 一次跳动App Dock 上的图标*/// 这个方法只能在当前App 不是处于非活动时才有效DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5) { NSApp.requestUserAttention(.informationalRequest)}}// 隐藏Dock 上的App 图标@IBAction func hideDockIcon(_ sender: Any) {// 隐藏App 图标,会自动隐藏窗口NSApp.setActivationPolicy(.accessory)DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()) { NSApp.unhideWithoutActivation()}}// 显示Dock 上的App 图标@IBAction func showDockIcon(_ sender: Any) {NSApp.setActivationPolicy(.regular)}
全屏设置
guard let screenSize = NSScreen.main?.frame.size else { return }// view.layer?.backgroundColor = NSColor.red.cgColorview.frame = CGRect(x: 0, y: 0, width: screenSize.width, height: screenSize.height-22.0)label.frame = view.bounds;
}