编译自:https://www.raywenderlich.com...
(本文原文十分值得一读,然而我翻译的略渣,有些直译不出来的,是我根据理解编的。务必各种指出错误。基于此,暂时请勿转载)
苹果公司的开发框架一直围绕着 Modal-View-Controller,提供了多种控制器对象用于管理 UI,以便于我们的代码,易于理解,便于维护。
视图控制器是 OS X 程序中一个极其重要的概念,它是 Modal 层和 View 层之间的桥梁。
本文探讨的内容较多,包括使用视图控制器构建你的应用程序、视图控制器重要的回调事件以及与窗口控制器的比较。
开始之前,你需要装好最新版的 OS X 和 Xcode。你要开发的可不是一个闹着玩的应用程序!推荐你先读读 Garbriel Miro’s 的窗口和窗口视图指南,但这不是必须的。
视图控制器介绍
视图控制器用于管理视图以及视图的子视图。 OS X 中,继承自 NSViewController
。
OS X 10.5 引入了视图控制器,那时它不是 responder chain 的一部分。这有什么影响呢?举例来说,视图控制器上面有一个按钮,然而它却不能处理按钮的事件,很尴尬吧。OS X 10.10 改进了视图控制器,从那以后,它成为一个构建复杂界面时十分有用的工具。
有了视图控制器,可以很好的规划你的窗口。视图控制器专注与视图有关的交互和事件,像调整窗口大小、关闭窗口这种与窗口有关的事件只放在窗口控制器中处理。于是代码就变的很干爽。
使用视图控制器额外的好处是易于复用。比如你有一个文件浏览器,左边部分的文件浏览视图是通过视图控制器来实现的,这时你恰好需要一个类似的视图,你能很容易的复用它。剩下的时间和精力陪女朋友逛逛街也好啊。
视图控制器和窗口控制器
那么,啥时候使用窗口控制器,啥时候使用视图控制器呢?
如果你期待的视图控制器工作行为是 UIViewController
那种,那么 OS X 10.10 Yosemite 以前的 NSViewController
会让你失望。
Apple 基于 MVC 设计了 iOS 的UIViewController
视图控制器,管理视图的生命周期、视图操作、响应控件事件等都包含其中,10.10 之后的NSViewController
加入了这些特性。在视图控制器里面完成你的视图,以及响应和视图有关的事件,然后设置窗口控制器的主 viewController、大小、标题等成为了标准流程。
经过这些改进,构建复杂交互的时候,可以良好的解耦,在多个视图控制器中完成需求,然后整合到一起。
(译注:这段实在是翻译不出来,是根据意思写出来的,请对照原文使用)
视图控制器实践
本教程将通过开发一个名为RWStore
的应用程序,用于选择查看不同raywenderlich.com store 的书籍来实践。
打开 Xcode 选择创建新工程,然后从模板中选择OS X\ Application\ Cocoa Application
,然后点击Next
将这个项目命名为RWStore
。使用 Swift 作为开发语言,同时勾选上 Use Storyboards
。勾选掉单元测试和 UI 测试的选项,你暂时还不需要他们。点击 Next
保存你的项目。
下载项目需要的资源文件。这个压缩包包含所需的图片以及书籍商品所需要的数据,他们保存在Products.plist
文件。此外你还能看到一个名为Product.swift
的源码文件。这个文件包含Product
类,它解析了 Product 对象的结构。接下来把他们添加到RWStore项目中。
从项目导航中选择选择Assets.xcassets
,将刚刚下载的图片资源拖进去。
然后将Products.plist
和Product.swift
拖到项目导航中。确保勾选了Copy items if needed
。
这时编译运行应用程序。
可以看到空白的主窗口,但不要惊慌,正常运行就是好的开始。
创建用户界面
打开Main.storyboard
,选择View Controller Scene
,拖拽一个pop-up 按钮
到 view 中,之后会用到。
通过 AutoLayout 来设置 它的位置。选择刚刚拖拽的 Pop-up 按钮,点击下方的Pin
按钮。在弹出来的窗口中,将其Leading
、Trailing
、Top
的约束值都设置为Use Standard Value
。
接着完成界面。拖拽一个 container view
放到刚刚添加的 pop-up
按钮下方。
container view
是一个占位视图,其他视图或者视图控制器可以通过它显示。
选择刚刚添加的container view
,点击下方的Pin
阿牛。添加top、bottom、trailing、leading
四个约束,将其设置为0。然后点击Add 4 constrains
按钮。
选择你 storyboard 中的视图控制器,然后点击Pin
按钮右侧的Resolve Auto Layout Issues
按钮,选择All Views in Controller/Update Frames
。这时你的界面看起来是这个样子的:
现在在代码中响应你的视图行为。打开Assistant Editor
(快捷键[alt] + [cmd] + [enter])确认 ViewCotroller.swift 已经被打开。拖拽pop-up
按钮到ViewController.swift
中,添加行为连接,命名为valueChanged
,类型是NSPopUpButton
。
刚刚创建的 Container 视图,自带了一个以 embed 方式连接的视图控制器,我们需要自定义,选择它并删除。
Tab View Controllers
现在,我们将添加一个视图控制器,用于显示 Product 信息:我们选择Tab View Controller
。它的视图包含几个选项卡,以及视图控制器。每一个选项卡对应一个视图控制器。选项卡切换的时候,对应的视图控制器被切换显示。
选择Tab View Controller
,拖拽到Storyboard
中。
将刚刚添加的Tab View Controller
与Container View
使用embed
方式连接起来:
双击左侧的选项卡,将标题改为Overview
;双击右侧的选项卡,将标题改为Details
。
编译并运行应用程序。
可以看到,刚刚我们设计的视图控制器已经能正常显示了,点击选项卡也能正常切换对应的视图控制器。因为我们还没有为其添加内容,所以两个视图控制器现在都还是空白。
Over View Controller
接下来需要创建这个。
File\New\File
,选择OS X\Source\Cocoa Class
,点击Next
。类名为OverviewController
,继承自NSViewController
,不要勾选Also Create XIB for user interface
,点击Next
创建完成并保存。
回到Main.storyboard
,选择Overview Scene
。点击视图上蓝色的按钮,选择类对象,在右侧的Identity Inspector
的 class 输入框中输入 OverviewController。
拖拽三个 label
到 OverviewController
的视图的左上方,一个接一个的排列。添加一个image view
在视图的右上角。
提示
:默认情况下,image view 没有边框,给它设置个图片,这样好找。选择Attributes Inspector
,选择games
在Image
字段。这个图片是刚刚资源文件里的,应该可以看到效果啦。
选择最最上面的标签。Attributes Inspector
里面将字体设置为System Bold
,字号设置为 19。
这时候的视图看起来是这样的:
好!让我们使用 AutoLayout 来调整一下布局。
选择 image view,点击下面的Pin
按钮。给其添加约束:top 和 trailing设置为 standard value
,width
和height
的值设置为 180。
选择最上面的标签,还是添加约束,将top、bottom、leading 和 trailing
设置为 standard value
。
选择挨着的下面的标签,添加约束:将trailing 和 leading
设置为standard value
。
选择最下面的一个标签,添加约束:将leading、trailing、bottom
设置为standard value
。点击top
约束,确认image view
是选择状态,然后选择Use the standard value
。
提示:
如果你不能看到 image view 在选择菜单,请确保 label 足够宽,且置于 image view 的下方。
点击下方区域的Resolve Auto Layout
,选择All Views in Controller/Update Frames
,你的视图看起来应该是这样的:
界面工作到现在可以告一段落了,编译运行,现在他长这样:
点击标签按按钮这时能看到视图控制器之间的差别了。我们一行代码没写就得到了一个不错的界面。
添加代码
先来把界面上的控件连接到你的代码中。
打开Assistant Editor
,选择OverviewViewController.swift
。按着Ctrl
然后拖拽到OverviewController.swift
中,命名为titleLabel
。类型为NSTextField
。
重复上面的操作,将剩余的控件都连接到代码中:
中间的标签命名为:
priceable
下面的标签命名为:
descriptionLabel
image view 命名为:
productImageView
和大多数 UI 控件一样,标签和 image view 都有子视图,选择的时候仔细一下,别选错了,比如NSImageView
选成了NSImageCell
,NSTextField
选成了NSTextFieldCell
。
点击OverviewController
,加上下面的代码:
//1
let numberformatter = NSNumberFormatter()
//2
var selectedProduct: Product? {
didSet {
updateUI()
}
}
这段代码:
number formatter
是一个NSNumberFormatter
,用于正确格式化价格。selectedProduct
对应挡圈选择的商品。每当值发生变化,didSet
里面的代码被执行,然后调用updateUI()
更新界面。
现在给OverviewController
添加updateUI
方法。
private func updateUI() {
//1
if viewLoaded {
//2
if let product = selectedProduct {
productImageView.image = product.image
titleLabel.stringValue = product.title
priceLabel.stringValue = numberformatter.stringFromNumber(product.price)!
descriptionLabel.stringValue = product.descriptionText
}
}
}
通过 viewLoaded 属性判断 NSViewController 是否已经加载,如果已经加载完毕,就可以安全的访问与视图有关的属性了。
解包
selectedProduct
确定是否已经选择了产品。然后显示正确的值。
这个方法现在已经会在产品变换的时候调用,还需要在视图加载完毕的时候调用。
视图控制器生命周期
从视图控制器具备响应视图事件能力开始,它就为视图生命的各个阶段提供了各种回调事件。比如视图从 storyboard 被加载,或者显示在屏幕上这种都属于被 Hook 的事件范围。所有这些机遇事件的方法被统称为view controller life cycle
。
视图控制器生命周期可以被划分成三个主要部分:创建、运转、终止。每一个部分都提供了可重载的方法满足你的需要。
创建
viewDidLoad()
当视图被首次完整加载的时候调用,一些只执行一次的初始化工作适合在这个时候进行,如创建数值格式化对象,注册通知,某些只需要调用一次的 API 等。viewWillAppear()
每当视图将要被显示的时候会被调用。比如我们刚刚选择 Overview 标签,每次切换它都会被调用。当数据发生变化,这是个更新到界面的好时候。viewDidAppear()
每当视图显示在屏幕上的时候,这个方法会被调用。这时适合做一些动画。
运转
视图控制器被创建之后,一些与用户交互的事件就该登场了:
updateViewConstraints()
当布局每次被改变都会被调用,比如窗口大小变化。viewWillLayout()
是布局将要发生的时候进行调用。如果你需要调整你的约束,可以在这时进行。viewDidLayout()
当布局完成之后被调用。
当重载这三个方法的时候,在其中你必须调用他们的super
。
终止
终止与创建对应:
viewWillDisappear()
当视图将要消失的时候调用。在viewDidAppear()
开始的动画这时可以结束了。viewDidDisappear()
视图消失之后这个方法被调用。一切你不需要的东西都可以在这时被干掉。比如已经无效的timer神马的。
生命周期实践
有关视图控制器生命周期重要的事情都已经告诉你了,现在进行一个小测试。
问题:你想把用户选择的产品的时候,让OverviewController
的视图显示正确的产品详情。该在啥时候去执行更新视图的代码?
打开OverviewController.swift
,添加下面的代码:
override func viewWillAppear() {
updateUI()
}
重载了viewWillAppear
,当用户看到视图之前,它会被正确更新。
数值格式化对象当前使用的是默认值,为了更好的展示,最好把它配置成货币格式。viewDidLoad()
是做这事儿的好地方。
在OverviewController
的viewDidLoad()
方法添加下面的代码:
numberformatter.numberStyle = .CurrencyStyle
用户在主界面选择不同的商品,当事件发生,我们需要通知OverviewController
。在ViewController
类中做这件事很合适,因为用户操作的弹出按钮就在这上面。打开ViewController.swift
,添加下面的代码:
private var products = [Product]()
var selectedProduct: Product!
products
是用来保存所有商品信息的数组。selectedProduct
指向当前弹出按钮所选择的商品。
找到viewDidLoad()
,添加下面的代码:
if let filePath = NSBundle.mainBundle().pathForResource("Products", ofType: "plist") {
products = Product.productsList(filePath)
}
加载本教程资源中包含所有商品信息的 plist,赋值给products
属性。接下来用这个数组初始化弹出按钮。
打开Main.storyboard
,选择View Controller Scene
,切换到Assistant Editor
。确保ViewController.swift
被选择,然后拖拽到ViewController.swift
作为一个 outlet,命名为productsButton
。确认类型为NSopUpButton
。
返回ViewController.swift
,找到viewDidLoad
添加下面的代码:
//1
productsButton.removeAllItems()
//2
for product in products {
productsButton.addItemWithTitle(product.title)
}
//3
selectedProduct = products[0]
productsButton.selectItemAtIndex(0)
这段代码做了一些微小的工作:
删除弹出按钮中所有的数据。
遍历商品数组,将所有商品的标题添加到弹出按钮。
选择数组中第一个商品。
最后,我们还需要在弹出按钮选择条目发生变化时做出响应,找到valueChanged(_:)
添加下面的代码:
if let bookTitle = sender.selectedItem?.title,
let index = products.indexOf({$0.title == bookTitle}) {
selectedProduct = products[index]
}
这段代尝试根据弹出按钮的标题在商品列表中查找对应的元素,然后把selectedProduct
指向正确的商品对象。
现在是时候来完成选择商品发生变化,通知OverViewController
的功能了。先在ViewController
添加一个OverViewController
的引用:
private var overviewViewController: OverviewController!
当 ViewController 以嵌入的形式被加载的时候,prepareForSegue(_:, sender:)
方法会被触发,我们可以在这个时候得到overViewController
的实例:
override func prepareForSegue(segue: NSStoryboardSegue, sender: AnyObject?) {
//1
let tabViewController = segue.destinationController as! NSTabViewController
//2
for controller in tabViewController.childViewControllers {
//3
if controller is OverviewController {
overviewViewController = controller as! OverviewController
overviewViewController.selectedProduct = selectedProduct
} else {
//More later
}
}
}
得到标签视图控制器的引用。
遍历子视图控制器。
找到
OverviewController
,的实例,然后设置它的selectedProduct
属性。
找到valueChanged(_:)
方法,在里面的if let
块中添加代码。
overviewViewController.selectedProduct = selectedProduct
编译运行,当选择不同的商品时候,可以看到界面已经能正常更新了。
产品详情视图控制器
我们来创建产品详情的视图控制器。
选择File\New\File...
,选择OS X\Source\Cocoa Class
,点击Next
。类名为DetailViewController
,继承自NSViewController
,不要勾选Also Create XIB for user interface
。点击Next
保存。
打开Main.storyboard
,选择Details Scene
。在Identity Inspector
中将class
改为DetailViewController
。
添加一个image view
到详情视图。选中它点击Pin
按钮创建约束。weight
和height
设置为180
,top
约束设置为standard value
。
点击Align
按钮,给视图添加一个居中约束:Horizontally in the Container
。
在刚刚添加的图像视图下方添加一个标签控件,设置字体bold
,字号19。点击Pin
按钮,添加约束:top
,leading
和trailing
,值为standard value
。
在刚刚设定的标签下方再添加一个标签。点击Pin
添加约束:top
,leading
和trailing
。值为standard value
。
拖拽一个NSBox
在标签下方。给它添加约束:top
、leading
、trailing
和bottom
,值为standard value
。
打开Attributes Inspector
,设置字体为bold
,字号14
。将title
改为Who is this Book For?
。
NSBox 用来组织一组相关联的 UI 元素很好用。而且有了标题看起来更明确。
拖拽一个标签控价在NSBox
里面,选择这个标签控件,点击Pin
按钮,添加top
,leading
,trailing
和bottom
,全部设置为standard value
。
然后更新你的界面,看起来是这样的:
激活Assistant Editor
,打开DetailsViewController.swift
。添加四个IBOutlet
,命名为:
productImageView for the NSImageView.
titleLabel for the label with the bold font.
descriptionLabel for the label below.
audienceLabel for the label in the NSBox.
在DetailviewController
添加以下代码:
// 1
var selectedProduct: Product? {
didSet {
updateUI()
}
}
// 2
override func viewWillAppear() {
updateUI()
}
// 3
private func updateUI() {
if viewLoaded {
if let product = selectedProduct {
productImageView.image = product.image
titleLabel.stringValue = product.title
descriptionLabel.stringValue = product.descriptionText
audienceLabel.stringValue = product.audience
}
}
}
这些代码和Overview
视图控制器里面的代码很类似,你应该已经很熟悉了:
定义表示当前选中商品的
selectedProduct
属性,当选择其他商品的时候更新视图。每次视图被显示的时候,强制刷新(比如切换选项卡会就会触发)。
将商品信息显示在视图上的图像视图和标签控件中(通过 updateUI)
当被选择的商品发生变化,你需要从主视图控制器通知商品详情视图控制器。打开ViewController.swift
,给商品详情视图控制器添加一个引用。在overviewViewController
属性下面增加下面的代码:
private var detailViewController: DetailViewController!
然后找到valueChanged(_:)
添加:
detailViewController.selectedProduct = selectedProduct
现在改变弹出按钮的选项,详情页会被通知到。
最后一点改变是在prepareForSegue(_:, sender:)
。找到注释//More later
,替换成下面的代码:
detailViewController = controller as! DetailViewController
detailViewController.selectedProduct = selectedProduct
当商品详情被嵌入的时候,当前选择的商品信息会正常加载。
你的应用程序已经完成!
最后的一点有的没的
你能从这里下载完整的项目。
在本教程中,你学习了以下内容:
什么是视图控制器和其与窗口控制器的区别
创建一个自定义的试图控制
连接控件到你的视图控制器
操作视图控制器
视图控制器的生命周期以及回调事件
如果想看看视图控制器里面都有啥,请移步官方文档:https://developer.apple.com/l...
另外还是推荐看一眼 tutorial on windows and window controllers。
视图控制器十分强大,而且在 OS X 应用程序开发中,它是十分有用的组件,涵盖了许多值得学习的内容,加油!本文给开了个好头,马上去开发你想要的东东吧。
欢迎在下方留言进行讨论。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。