原文地址 翻译:DeveloperLx
window是相应于全部OS X app的UI的“容器”。它们定义了你的app当前负责的屏幕的区域,用户可以使用容易理解的多任务范式(multi-tasking paradigm)来进行交互。OS X app总是落入如下种类中的一种:
- 单窗口的工具 ,就像计算器
- 单窗口,库风格的“鞋盒 ,就像Photos
- 多窗口基于文档的 ,就像TextEdit
不管是哪种类型的app,几乎每种OS X app都利用了 MVC (Model-View-Controller),一种核心的设计模式。
在Cocoa中,window是
NSWindow
类的一个实例,相应的控制器对象则是
NSWindowController
类的实例。在一个有着很好设计的app中,你通常(typically)都会使用一对一关系的window和它的controller。这个模型层会根据你app类型和设计各有不同。
在这个OS X教程中的window和window controller,你将创建 BabyScript ,一个多窗口的基于文档的app,它的灵感来自于TextEdit。在这个过程中,你将学到:
- Window 和 window controller
- 文档的架构
- NSTextView
- 模型window
- 菜单栏和菜单项
这篇教程是面向新手的。话虽如此,它要求下列的基础知识:
- Swift
- Xcode,特别的,storyboards
- 创建一个简答的 Mac (OS X) app
如果你对上面的任意一项都不熟悉,你可能要复习(brush up)一些这个网站上的其它教程:
- Swift语言教程
- macOS X 初学者开发教程 第一部分 Xcode介绍
- macOS X 初学者开发教程 第二部分 OS X App剖析
- macOS X 初学者开发教程 第三部分 你的第一个OS X App
运行 Xcode ,并选择 File / New / Project… 。选择 OS X / Application / Cocoa Application ,然后点击 Next 。
在下一屏,填写如下所示的字段,但输入你自己的名字(或超级英雄的别名),而不要像我一样。
点击 Next 并保存你的项目。
运行项目,你将看到:
为了打开更多的文档,选择 File / New 。全部的文档都位于 同一位置 ,因此你只有在点击和拖拽他们时,才会看到顶部的文档。这不是理想的效果,所以把它添加到你的to-do列表中,但先不要钻进去。
也是可以使用 Windows 菜单来讲window带到前台。
现在你已经看到了它在行动,让我们花几分钟来看看它的工作原理。
文档是在内存中的数据的容器,你可以在window中查看它。最终,它可以从磁盘或iCould中写入和读出。从程序上讲,文档是一个
NSDocument
类的实例,充当数据对象的控制器 - 也就是说 - 相应于文档的模型。
在文档架构中的其它的两个主要的类是
NSWindowcontroller
和
NSDocumentController
。这些是它们的角色:
-
NSDocument
:穿件,展示及保存文档数据 -
NSWindowController
:管理展示文档的那个window -
NSDocumentController
:管理app中全部的文档对象
还是可视化比较好,所以这里有一个表,来展示这些类如何在一起工作:
文档的建构也提供了文档的保存和打开机制。
在
Document.swift
中,你会发现实现为空的
dataOfType
,它是为了写入的,还有
readFromData
,是为了读出的。保存和打开文档是在这个教程的范围之外的,因此你要做出一些变化来避免令人困惑的行为的出现。
在
Document.swift
中,
移除
autosavesInPlace
:
override class func autosavesInPlace() -> Bool { return true }
现在你将禁掉全部关于打开和保存的菜单项,但是在你行动之前,注意到全部你所期望的功能早已在这里了。例如,选择 File / Open 并find的对话框,包括控制器,侧边栏,工具栏等等已经都在这里了:
一个菜单项,当没有定义动作时是无用的。相同的禁用的效果同样也发生在,当响应者链中没有相应的能够响应那个动作selector的对象时。
因此,你将断开需要禁用的,为菜单项定义的动作。
在storyboard中,在 Main Menu 的 Application Scene ,选择 File / Open 。
选择
Connections Inspector
并点击
Open
。正如你看到的,它通过
openDocument
selector连接到了第一响应者,也就是在响应者链中,第一个响应这个selector的对象。就像下面这样,通过点击
x
来删除这个连接:
对 Save , Save As 和 Revert to Saved 重复该步骤。
运行项目,切换到 Open 菜单,并检查它是否看起来像下面这样:
当你运行BabyScript的时候,window会在靠近左边,但稍低于屏幕中心的的位置打开。
为何它选择了这个位置?
找到storyboard,在 outline view 中选择 Window ,然后选择 Size Inspector 。运行BabyScript - 或将它带到前面 - 你现在看到的屏幕应该是下面这样的:
在 Initial Position 下输入 X 和 Y 的数值。这是设置window的位置的一种方法。你也可以通过拖动下面灰色的矩形直观地(graphically)设置它。
注意 :在Cocoa中,一个可见对象(window, view, control等)的原点位于 左下 角。在向上和向右的方向上,坐标值会增加。
相反地,在很多图形环境下,尤其是iOS,其原点是位于 上 左角的,则在向下和向右的方向上,坐标值会增加。
假设你期望你的window打开的位置,是从左上角水平和垂直偏移200个点。你可以用Xcode在window中的 Size Inspector 来设置它,或依靠编程来实现。
基本可以打赌,用户会在各种尺寸的屏幕上运行BabyScript。由于你的app没有一个水晶球,能在编译时看到它将打开在一个怎样的屏幕尺寸上,因此Xcode使用了一个虚拟的屏幕尺寸,和一个类似于自动布局的概念,来在运行时决定window的位置。
为了设置这个位置,你将用到 Initial Position 下的两个值 X 、 Y ,和两个下拉式(drop-down)菜单。
找到 Initial Position ,依据屏幕坐标系来设置window的打开位置。给 X 和 Y 都输入200,并在下拉式菜单中选择 Fixed from Left 和 Fixed from Bottom 。这就设置了window在x和y的方向上都是200个点的偏移。
运行项目,你应当看到:
跟随下面的步骤来让window定位到左上角:
- 在预览图中拖拽灰色的矩形到虚拟屏幕的左上角 - 这就改变了初始的位置。
- 在 X 处输入200,在 Y 这里,输入最大的值“负200”,在这个case中就是557.
- 在底部的下拉菜单中选择 Fixed from Top 。
下面右侧的图片展示了你应该输入的地方和值:
注意 :OS X在每次启动时会记忆window的位置。为了查看你做的变化,你需要确保关掉了app的window - 而不仅仅是重新运行。
关掉所有的窗口,然后运行项目。
现在你要完成和刚刚在Interface Builder中所完成的一样的工作,但这一你要通过编程来完成。
采取“硬的方式”(hard way)的原因有两个。第一,你可以对
NSWindowController
有一个更好的理解。第二,它是一种更灵活和很直接的方式。
在运行时,app将在得知屏幕尺寸之后执行窗口最终的位置。
在 Project Navigator 中选择 BabyScript 组,然后选择 File / New / File.. 。从弹出的对话框中,选择 OS X / Source / Cocoa Class 并点击 Next. 。
创建一个名叫
WindowController
的新的类,让它作为
NSWindowController
的子类。不要勾选
XIB
,将
Language
设为
Swift
。
选择一个位置来保存新的未见。之后,你会看到一个新的名叫 WindowController.swift 的文件出现在了 BabyScript 组中。
找到storyboard,在
Outline View
中从
Window Controller Scene
选择
Window Controller
。选择
Identity Inspector
,再从
Class
下拉菜单中选择
WindowController
。
当
windowDidLoad
方法被调用时,这个window就完成了从storyboard加载的过程,因此这是你的任何配置都会覆盖在storyboard中的设置。
打开
WindowController.swift
并用下列代码替换
windowDidLoad
方法:
override func windowDidLoad() { super.windowDidLoad() if let window = window, screen = window.screen { let offsetFromLeftOfScreen: CGFloat = 20 let offsetFromTopOfScreen: CGFloat = 20 let screenRect = screen.visibleFrame let newOriginY = CGRectGetMaxY(screenRect) - window.frame.height - offsetFromTopOfScreen window.setFrameOrigin(NSPoint(x: offsetFromLeftOfScreen, y: newOriginY)) } }
这个逻辑将window的位置定位在了其左上角距离屏幕左上角的位置上。
正如你看到的,
NSWindowController
有一个
window
property,而
NSWindow
一个
screen
property。你可以使用这两个property来访问window和屏幕的几何信息(geometry)。
在确定了(ascertaining)屏幕的高度之后 ,你的window的frame就被减去了期望的偏移。记住Y的值是随着你在屏幕中向上移而增大的。
visibleFrame
排除了被dock和菜单栏占用的区域。如果你不把这个纳入考量,你可能最终会让dock模糊掉了你的window的一部分。
当你隐藏了dock和菜单的时候,
visibleFrame
可能仍然会比
frame
小,因为系统会持有了一个小的边界区域来当展示dock时进行检测。
运行项目,这个window应该被设定为,距离屏幕左上角的每个方向都是20个点的位置上。
为了进一步提升你的window的位置,你将采用 Cascading Windows ,意思是整理多个互相重叠的window的排列,让每个window的标题栏可以被看到。
在
WindowController.swift
中,
WindowController
定义下面添加下列代码:
required init?(coder: NSCoder) { super.init(coder: coder) shouldCascadeWindows = true }
通过覆盖
NSWindowController
的
required init
方法,你将
NSWindowController
的
shouldCascadeWindows
property设置为true。
运行app,然后打开5个window。你的屏幕看起来应该就更友好些了:
现在到了这个教程最令人兴奋的部分。只需
两
行代码并添加一个
NSTextView
的控制到你window的
contentView
上,你将添加激动人心(blow your mind)的功能!
一个window在创建时,自动地创建了两个view:一个不透明的带有边界、标题栏等的框架view,和一个通过window的
contentView
property访问的透明content view。
content view是一个window的view图层的根,你可以用一个定制的view来替换这个默认的。注意,你必须使用
NSWindow
的
setContentView
方法来定位content view - 你不可以用标准的
NSView
的
setFrame
方法来定位它。
注意
:如果你是一个iOS的开发者,请注意在Cocoa中,
NSWindow
不是
NSView
的子类!在
iOS
中,
UIWindow
是
UIView
的一个特殊的子类。
UIWindow
它自己是view图层的根,就由它来扮演content view的角色。
在storyboard的
contentView
中,通过选择并按
delete
键,
移除
那个说“Your document contents here”的text field。
为了创建将要构成你的UI的主要部分的新的
NSTextField
,跟随下列的说明:
- 在storyboard中,打开 Object Library 。
- 搜索 nstextview 。
- 拖动 Text View 并将其放到content view上。
- 调整text view的尺寸,让它的每边都距离content view相应边20个点。
- 在 Outline View 中,选择 Bordered Scroll View 。注意到text view是被嵌入到了 Clip View 中,Clip View则被嵌入到了一个scroll view中。
- 选择 Size Inspector 。输入 X 和 Y 均为20, Width 为440, Height 为230。
运行项目 - 你应该看到如下这样:
看起来那么友好,闪烁的文字插入点邀请你输入你的文本!开始你的宣言(manifesto),或仅仅让它保持简单的“Hello World”,然后选择文本。用 File / Copy 或 command – C 来拷贝它,然后粘贴几次,感受这个app!
探索 Edit 和 Format 菜单来获得有用的idea。你可能会注意到 Font / Show Fonts 是被禁用的。现在你就来打开它!
在故事版中,找到主菜单,点击 Format 菜单,然后是 Font ,然后点击 Show Fonts 。
找到 Connections Inspector ,你会发现这个菜单项中没有动作被定义。这就解释了为何菜单项被禁用了,但是你该将它连接到何处?
显然,这个动作早已被定义在了Xcode没有直接导入的代码中,你只是需要连接一下。
右击
Show Fonts
并将其拖动到
Application Scene
的
First Responder
上,然后释放鼠标。一个带有全部动作的小型的可滚动的列表window就会弹出。找到并选择
orderFrontFontPanel
。你也可以输入这个名称以便更快地找到它。
现在,选择
Show Fonts
来看一下
Connections Inspector
。你会看到,现在这个菜单已经被连接到了在响应者链中响应
orderFrontFontPanel
这个selector的第一响应者。
运行app,然后输入一些文本并选择它。选择 Format / Font / Show Fonts 来打开字体面板。调整(Play with)字体面板右侧的垂直的滑动条,观察文本的大小如何实时地变化。
等等,你甚至都没有输入一行关于text view的代码,它就有了改变文本大小的能力。你太棒了!
为了看到这个app全部的能力,请从 这里 下载一些格式化的文本,并使用它作为这个text view的初始化文本。
使用TextEdit打开它,全选并拷贝到剪贴板中。找到storyboard,选择Text View,然后 Attributes Inspector ,然后将文本粘贴到 Text Storage 中。
运行项目,你应该会看到:
你确实可以滚动不适合当前window大小的文本,但尝试一下改变window的尺寸吧。
哎呀!text view不能随着window尺寸的改变而改变。
使用 自动布局 ,这是一个非常简单的修复。
注意 :自动布局在你的Cocoa和iOS app的UI中都可以使用。它创建了一套规则来定义元素之间的几何关系,而你以约束的形式来定义这些关系。
使用自动布局,你可以创建一个动态的界面,以恰当地相应屏幕大小、window大小、设备方向和本地化的变化。
关于自动布局还有更多的内容,但为了本教程的缘故,所有你需要做的只是跟随下面的步骤 - 你可以在之后学习更多关于Auto Layout的内容。这里有一些很好的教程供查看:在iOS 7中开始自动布局教程, 第一部分 和 第二部分 。
在storyboard的 Outline View 中,选择 Bordered Scroll View ,点击画布右下角的 Pin 按钮。
分别点击四个小红色的条的约束;那个“破碎的凋谢的红色”就会转变成“实的红色”。点击底部的叫做 Add 4 Constraints 的按钮。
运行项目,观察window和text view怎样一起改变大小:
为了在window打开时自动地展示尺子,你需要在代码中有一个
IBOutlet
。从菜单中选择
Format / Text / Show Ruler
。在
ViewController.swift
中,添加一行代码方法
toggleRuler
到
viewDidLoad
中,并添加一个
IBOutlet
在这个方法之上,就像下面这样:
@IBOutlet var text: NSTextView! override func viewDidLoad() { super.viewDidLoad() text.toggleRuler(nil) }
现在你将在storyboard中,连接text view到view controller上。
在storyboard中,右击
ViewController
,不放手并拖拽到text view直到它变得高亮,然后放开鼠标。你将会看到一个带有
Outlets
列表的小的window。选择
text
的outlet:
运行项目,现在这个window就会默认id展示尺子:
就像我承诺的,仅靠两行代码和storyboard,你就创建了一个小的迷你文字处理器 - 致敬(Chapeau),苹果!
你可以让window运行在模态方式下。这个window仍然使用app的标准时间循环,但输入仅限于模态window。
有两种方式来利用模态窗口。你可以调用
NSApplication
的方法
runModalForWindow
。这种方式为指定的窗口垄断(monopolizes)了事件,直到它得到了一个你可以通过
stopModal
、
abortModal
或
stopModalWithCode
来调用的请求才停止。
对于这个case,你将使用
stopModal
。另一种方式被称作
模态session
,不会包含在本教程中。
你可以添加一个模态窗口来在活动的window中统计字数和段落数。由于会关联于一个指定的窗口和指定的状态,它必须是模态的。
从 Object Library 中,拖拽一个新的 window controller 到画布上。它创建了两个新的场景:一个 window controller scene 和一个 view controller scene :
从新的 window controller scene 中选择 Window ,并使用 Size Inspector 来设定它的宽为300,高为150。从新的 view controller scene 中选择 View 并调整它的大小来匹配这个window:
由于字数统计是模态的,它的标题栏上如果有关闭、最小化和重新调整大小的按钮就会显得很奇怪,并违反了 HIG (苹果的人机交互指南)。
对于
关闭
按钮,它还会引入一个严重的bug,因为点击它就会关闭window,但没有调用
stopModal
。这样,这个app就会永远停留在“模态状态”下。
在storyboard中,选择 Word Count window并选择 Attributes Inspector 。取消勾选 Close , Minimize 和 Resize 。同时改变 Title 为 Word Count 。
现在,你将从Object Library中添加四个label控件和一个push button到Word Count window的
contentView
上。
选择 Attributes Inspector 。相应地改变Title为 Word Count , Paragraph Count , 0 和 0 。同时两个0的label的 alignment 为右对齐。将push button的Title改为 OK 。
接下来为Window Count ViewController创建一个子类。选择
File / New / File..
,选择
OS X / Source / Cocoa Class
。在
Choose Options
的对话框中,在
Class
后输入
WordCountViewController
,而在
Subclass of
后输入
NSViewController
。
点击 Next 创建新的文件。确认 WordCountWindowControll.swift 现在已经在project navigator中了。
找到storyboard. 为文字计数在
view controller scene
的view controller中选择proxy图标。打开
Identity Inspector
, 从
Class
的下拉菜单中选择
WordCountViewController
。
注意在画板和
Outline View
上的名字是如何从通用的改变成
Word Count View Controller
的。
现在你可以为两个label创建outlet了,让它们来展示统计的值 - 就是那两个0 label。在 WordCountViewController.swift 中的类的定义下,添加下列代码:
@IBOutlet weak var wordCount: NSTextField! @IBOutlet weak var paragraphCount: NSTextField!
在storyboard中,右击
word count view controller
的proxy图标,拖拽到0 label,并当它高亮时释放鼠标。从弹出的
Outlets
列表中,选择
wordCount
。
为下面那个0 label重复同样的操作,但是这次选择
paragraphCount
。查看
Connections Inspector
中的每一个label,让它们的
Outlets
都像下面这么连接:
很快,你就会添加代码来加载count window controller。这要求它有一个 storyboard ID 。从storyboard的 word count window 中选择 window controller 。选择 Identity Inspector ,在 Storyboard ID 中输入 Word Count Window Controller :
现在到了展示模态window的基本逻辑了。在文档window的view controller中,找到并选择
ViewController.swift
,在
viewDidLoad
下面添加如下代码:
@IBAction func showWordCountWindow(sender: AnyObject) { // 1 let storyboard = NSStoryboard(name: "Main", bundle: nil) let wordCountWindowController = storyboard.instantiateControllerWithIdentifier("Word Count Window Controller") as! NSWindowController if let wordCountWindow = wordCountWindowController.window, textStorage = text.textStorage { // 2 let wordCountViewController = wordCountWindow.contentViewController as! WordCountViewController wordCountViewController.wordCount.stringValue = "\(textStorage.words.count)" wordCountViewController.paragraphCount.stringValue = "\(textStorage.paragraphs.count)" // 3 let application = NSApplication.sharedApplication() application.runModalForWindow(wordCountWindow) } }
一步一步来看:
- 使用你之前指定的storyboard ID初始化word count window controller。
- 设置检索自text view的相应的值到字数统计window中
- 模态地展示字数统计window
注意
:在第二步,你在两个view controller间传递了数据。这就类似于当transition涉及到一个segue时,你经常在
prepareForSegue
中做的一样。由于展示模态window是直接通过调用
runModalForWindow
完成的,没有涉及到segue,你只需在调用之前传递数据。
现在你将添加代码来让字符统计window消失。在
WordCountViewController.swift
中,添加下列代码到
paragraphCount
outlet的下边:
@IBAction func dismissWordCountWindow(sender: NSButton) { let application = NSApplication.sharedApplication() application.stopModal() }
这是当用户点击 字符统计window 中的 OK 时,应当被调用的 IBAction 。
找到storyboard,右击
OK
,并拖动带
word count view controller
的proxy图标上。释放鼠标并在出现的列表中选择
dismissWordCountWindow:
:
剩下的要来展示window的唯一一件事,就是添加UI并去调用它了。找到storyboard,在 Main Menu 中点击 Edit 。从 Object Library 中拖出一个 Menu Item 到 Edit 菜单的底部。选择 Attributes Inspector 并设置title为 Word Count 。
花几分钟来通过输入 command – K 创建一个快捷键,作为 key equivalent 。
现在你将在
ViewController.swift
中连接新的菜单项到
showWordCountWindow
方法上。
找到storyboard,右击
Word Count
菜单项,拖拽到
application scene
的
First Responder
上。从列表中选择
showWordCountWindow
。
注意
:你可能会奇怪为什么你要连接菜单项到first responder,而不是直接到
showWordCountWindow
这里。这是因为文档view的主菜单和view controller是在不同的storyboard的scene中,因此,不能直接连接。
运行app,选择 Edit / Word Count ,瞧(voila),字数统计window展示了它自己。
单击OK关闭window。
这里是BabyScript的 最终版本 。
在这个OS X的window和window controller教程中,你已经覆盖了很多基本的内容!但它对于你可以利用window和window controller做的事来说,只是冰山的一角。
你已经覆盖了:
- 动作的MVC设计模式
- 怎样创建一个多window的app
- OS X app的典型app架构
- 怎样使用Interface Builder 和 编程方式来定位和排列window
- 使用自动布局来来根据window调整view的大小
- 使用模态window来展示附加的信息
还有更多!
我强烈地建议你探索苹果在 El Capitan’s Mac 开发者库 中提供的的大量文档。特别的,参考 Window编程指南 .
为了更好地理解Cocoa app的设计,以及如何使用文档开头提到的app的类型,请查看 Mac App编程指南 。这个文档还扩展了基于多窗口文档的应用程序的概念,你会在这里找到改善BabyScript的idea。