原文地址 翻译:DeveloperLx
在撰写任意代码的时候,对关注点做出清晰的分割是非常重要的 - 功能应当被分为恰当的较小的类。这可以让代码易于维护且容易理解。苹果在macOS上,围绕Model-View-Controller模式设计了可用的框架,并提供了各种各样的负责管理UI的controller对象。
View controller负责连结model层到view层,在你的macOS app的架构中扮演了难以置信的重要的角色。
在这个macOS view controller的教程中,你将看到广泛的功能被“烘焙”成了“香草味”的view controller,并学习怎样创建你自己的view controller的子类,来以容易被理解的方式构建你的app。你将了解到生命周期的方法,如何hook到你的app的重要的事件,还有关于view controller和window controller之间的比较。
为了跟进这个教程,你需要安装最新版本的macOS和Xcode。这次没有起始项目 - 你将从0开始构建一个很赞的项目!在着手这个view controller教程之前,你可以首先阅读一下Gabriel Miro的 window和window controller的教程 ,但这不是必须的。
开场白足够了 - 让我们从一些理论开始吧!
view controller是用来负责管理一个view和它的子view的。在
macOS
中,view controller是由
NSViewController
的子类实现的。
View controller已经存在有一定的时间了(苹果是在OS X 10.5时引入的这个),但在OS X 10.10之前,它并不在相应者链中的一部分。这就意味着,例如,如果你的view controller的view上有一个button,这个view controller是无法接受到它的事件的。然而,在OS X 10.10之后,view controller作为负责的用户界面的构建块,已变得非常有用。
View controller使你将window的内容切分为逻辑的单元。view controller来照顾那些更小的单元,就如同window controller,处理类似改变大小或关闭window之类的指定的任务。这就让你的代码变得更容易组织。
另一个好处是view controller是比较容易在其它应用中重用的。如果在一个文件浏览器中,左侧带有一个被单独的view controller控制的层级的视图,你就可以将它用到另一个需要类似的view的应用中。你就可以用省下的时间和精力去喝啤酒了!
你可能想要知道,什么时候该使用仅仅一个window controller,什么时候又该去实现多个的view controller。
在OS X 10.10 Yosemite之前,
NSViewController
并不是一个非常有用的类。它没有提供任何你所期待的view controller的功能 - 例如,可以在
UIViewController
中找到的。
由于引入的变化,诸如view的生命周期,以及view controller被包含到响应者链中来接收从它的view中而来的事件,苹果正在推进它正在iOS开发中实习的Model View Controller(MVC)模式。你应当使用view controller来处理你的view、子view以及用户界面的全部的功能。使用window controller来实现相应于应用window的功能,例如设置根view controller,调整大小,改变位置,设置title等。
这个教程,将通过将UI的不同部分为几个view controller来构建负责的用户界面,并使用它们,如构建的块来组成完整的用户界面。
在这个教程中,你将要写一个叫做 RWStore 的应用,你可以在其中选择来自 raywenderlich.com store 的不同的书。 让我们开始吧!
打开Xcode选择创建一个新的Xcode的项目,并从模板菜单中选择 macOS/Application/Cocoa Application 。单击 Next 。
为你的项目起名 RWStore 。在选择这屏中,确保选择 Swift 语言,并勾选 Use Storyboards 选择框。你不需要Unit和UI Tests,因此取消相应的勾选框。单击 Next 并保存你的项目。
下载项目 资源 。这个压缩文件包含书的图片,和一个 Products.plist 的文件,包含有产品信息的字典,例如名称、描述和价格。你也将找到一个名叫 Product.swift 的源码文件。这个文件包含了 Product 类,它会从那个plist文件中读取产品的信息。你将添加这些到RWStore中。
在 Project Navigator 中选择 Assets.xcassets 目录,并拖拽下载的图片到包含有app icon的列中。
将 Products.plist 和 Product.swift 拖拽到左侧的 Project Navigator 中。确保 Copy items if needed 已选中。
运行app。你应会看到你的应用的主窗口。
现在它是空的,但不要担心 - 这只是开始。
打开 Main.storyboard ,选择 View Controller Scene ,并拖拽一个 pop-up button 到view上。你将使用这个 pop-up button 来展示产品的列表。
使用自动布局来设置它的位置。这个pop-up button应当占据这个view全部的宽度,并贴近在顶部,因此选择这个pop-up,点击位于底部的 Add New Constraints 按钮。
在出现的pop-up中,选择trailing约束并设置它的值为 Use Standard Value 。对top和leading约束重复同样的操作,并单击 Add 3 Constraints 。
为了完成这个UI,添加一个view去展示产品的细节。选择 container view 并将它拖拽到pop-up按钮的底部。
container view 是为了另外的view的占位符,并伴随着它自己的View Controller。
现在来设置这个view的自动布局的约束。选择container view并单击 Add New Constraints 按钮。添加 top , bottom , trailing 和 leading 约束,值为 0 。单击 Add 4 Constraints 按钮。
选择你view controller的view,并点击那几行约束按钮中的 Update Frames 按钮。你的view应当看起来像这样:
现在你将创建一个动作,它将在这个按钮的选择发生变化时被调用。打开 Assistant Editor (你也可以使用快捷键 Command-Option-Return ),并确定 ViewController.swift 已打开。从 pop-up button 拖拽 到 ViewController.swift 中来添加一个Action连接。在弹出的视图中,确认connection是 Action ,Name为 valueChanged ,Type为 NSPopUpButton 。
在画布中,现在有了一个新的view controller通过 embed segue 被连接到了container view上。你的app将使用一个定制的view controller,因此你可以删除自动生成的那个:选择相应于container view的那个view controller并按 delete 键。
现在你已经添加了用来展示产品信息的view controller:
Tab View Controller
。
Tab View Controller
是
NSTabViewController
的子类;它包含一个带有两个或更多的项目的tab view和一个container view。在每个tab的背后是另一个view controller,它的内容被用来填充container view。每次当一个新的tab被选择时,这个Tab View Controller就用相应的view controller替换了它的内容。
选择 Tab View Controller 并将其拖拽到画布上。
现在这个tab controller已经在storyboard上了,但到现在都没有用过。使用一个embed segue来将它连接到container view;从container view 拖拽 到Tab View Controller.
这样变化之后,当app运行container view时,container view的区域被替换为Tab View Controller的view。双击Tab View左侧的那个tab,并将它重命名为 Overview 。重复同样的操作,将右边的tab重命名为 Details 。
运行app
现在这个tab view controller已经展示出来了,你可以使用tab来在两个不同的view controller之间进行选择。但现在还不是显而易见的(noticeable),因为这两个view恰好是相同的,但当你选择了一个tab时,在内部tab view controller确实是将他们替换了的。
接下来你需要为 Overview tab创建相应的view controller。
前往 File/New/File… ,选择 macOS/Source/Cocoa Class ,并单击 Next 。将类命名为 OverviewController ,使其成为 NSViewController 的子类,确保 Also Create XIB for user interface 未被选中。单击 Next 并保存。
返回 Main.storyboard 并选择 Overview Scene 。单击view上的蓝色的圈,并将右侧的 Identity Inspector 中的class更改为 OverviewController 。
拖拽三个 label 到 OverviewController 的view上。将这些label放到这个view顶部的左边,一个在另一个下面地摆放。添加一个 image view 到这个view的右上角。
注意: 默认情况下,image view是没有边界的,要在view中找到它会有点困难。为了方便布局,你可以设置一个图片。选中image view,打开 Attributes Inspector 并在 Image 这里选择 2d_games 。这个图片在教程的代码中,在运行时,将被合适的产品图片所替换。
选择顶部的label。在 Attributes Inspector 中,改变font为System Bold及size为19。现在你需要调整label的大小,来看到全部的文本。
这个view现在看起来应该是这样的:
是时候使用自动布局的超能力,来让这个view看起来更好了。
选择这个image view并单击底部的 Add New Constraints 按钮。使用standard value来添加top和trailing的约束,将width和height的约束的值设置为180.
选择顶部的label,使用standard value来添加bottom,top,leading和trailing约束。
选择下面的label,并使用standard value来添加trailing和leading约束。
扩宽最后一个label,让它到达image的底部,然后使用standard value添加leading,trailing和bottom的约束。对于top约束,确保这个image被选中(以便它的顶部可以和image view对齐),然后使用standard value添加。
选择这个view,并在底部的栏中单击 Update Frames 按钮。你的view现在看起来应该是这样子的:
毕竟你努力工作在界面上,现在是最终的时间来查看结果了,运行项目。
单击tab,并查看tab view controller是怎样展示恰当的view controller的。在没有一行代码的情况下,它已正确地运行。
是时候亲自动手,添加一些代码,来在这个view上展示产品的详细信息了。为了在代码中引用这些label和image view,你需要对它们的每一项添加 IBOutlet 。
首先,打开
Assistant Editor
,确保选中
OverviewViewController.swift
。从顶部的label
拖拽
到
OverviewController.swift
并添加一个名为
titleLabel
的outlet。确定其type为
NSTextField
。
对另外的两个label和image view重复相同的操作,来使用下列的名称创建剩余的outlet:
-
中间的label:
priceLabel
-
底部的label:
descriptionLabel
-
image view:
productImageView
就像大多数的UI元素,label和image view是由多个子view构成的,所以,确保你选择的view是正确的。你可以查看outlet的class:对于image view,它必须是 NSImageView ,而不是 NSImageCell 。对于label,它必须是 NSTextField ,而不是 NSTextFieldCell 。
为了在overview的tab中展示产品信息,打开 OverviewController.swift ,并添加下列的代码到类的实现中:
//1 let numberFormatter = NumberFormatter() //2 var selectedProduct: Product? { didSet { updateUI() } }
一步一步来看代码:
-
numberFormatter
是一个NumberFormatter
对象,用来展示加个的值,以货币的形式格式化。 -
selectedProduct
持有了当前选中的产品。每次当值发生变化时,didSet
就被执行,也就是执行updateUI
。
现在添加
updateUI
方法到
OverviewController.swift
中。
private func updateUI() { //1 if isViewLoaded { //2 if let product = selectedProduct { productImageView.image = product.image titleLabel.stringValue = product.title priceLabel.stringValue = numberFormatter.string(from: product.price) ?? "n/a" descriptionLabel.stringValue = product.descriptionText } } }
-
检查这个view是否已加载。
isViewLoaded
是NSViewController
的一个property,当这个view已被加载到内存中时,它的值就变为true。当这个view被加载完毕后,访问全部的view相关的property,例如label,就都会是安全的了。 -
Unwrap
selectedProduct
,来查看是否这里有一个产品。之后,label和image就被更新,来展示适当的值。
这个方法已在产品发生变化时被调用,但也需要在这个view准备要被展示时被调用。
由于view controller是负责管理view的,因此它会暴露让你可以hook住相应的view的事件的方法。例如当这些view要从storyboard加载到屏幕上时,或这些view将要出现在屏幕上时。这些基于事件的方法的集合,被称作 view controller生命周期 。
一个view controller的生命周期被分为三个主要的部分:创建,使用期(lifetime),还有终止。每个部分都有你可以重写,以添加额外工作的方法。
-
viewDidLoad
会在这个view被全部加载时被调用,可以用来一次性地初始化例如,配置一个数的formatter,注册通知,或调用仅仅需要被执行一次的API。 -
viewWillAppear
会在每次view将要展示在屏幕上时被调用。在我们的应用中,它会在每次你选择Overview的tab时被调用。这是一个更新你的UI或刷新你的数据模型的好时机。 -
viewDidAppear
会在view被展示到屏幕上时被调用。在这里你可以启动一些花哨的(fancy)动画。
一旦创建了一个view controller,它就进入了一个可以用来处理用户交互的时期。它有三个方法针对这个时期:
-
updateViewConstraints
会在每次布局发生变化时被调用,例如window的大小被调整时。 -
viewWillLayout
会在view controller的view的layout
方法被调用前调用。例如,你可以使用这个方法来调整约束。 -
viewDidLayout
会在layout
方法被调用之后调用。
这些是对应的方法:
-
viewWillDisappear
会在view消失之前被调用。在这里你可以停止你在viewDidAppear
启动的那些花哨的动画。 -
viewDidDisappear
会在view不再在屏幕上时被调用。在这里你可以抛弃所有你不在需要的东西。例如,你可以废弃你用来周期性更新你的数据模型的定时器。
在所有的这些方法中,你应当在某个时刻调用 super 的实现。
既然你已了解了有关view controller的生命周期的最重要的事,是时候来做一个简短的测试了!
问题:每次当
Question: Every time
OverviewController
出现时,你想要更新UI,以便当Details的tab被选择时,顾及用户选择了一个产品。哪个方法会比较适合?
解决办法 | |
---|---|
有两个可能的方法:
viewWillAppear
和
viewDidAppear
。最好的解决方案是
viewWillAppear
,这样用户就可以在view一出现时,来更新UI了。使用
viewDidAppear
会导致一个用户将在数据更新前,首先看到UI的出现。
|
打开 OverviewController.swift 并添加这个代码到类的实现中:
override func viewWillAppear() { super.viewWillAppear() updateUI() }
它覆盖了
viewWillAppear
方法,以在view可见之前更新用户的界面。
数的formatter当前使用的是默认的值,这并不符合你的需求。你将配置它成为货币的值的格式;由于你只需要做一次,一个很好的地方就是在方法
viewDidLoad
中。
在
OverviewController
添加下列的代码到
viewDidLoad
中:
numberFormatter.numberStyle = .currency
下一步,主view controller需要响应产品的选择,然后通知给
OverviewController
这个变化。这个最好的地方是在
ViewController
类中,因为这个controller控制着pop-up按钮。打开
ViewController.swift
,并添加这些property到
ViewController
类的实现中:
private var products = [Product]() var selectedProduct: Product?
第一个property
products
,是一个用来持有全部产品的引用的数组。第二个
selectedProduct
,则持有了被pop-up按钮选中的产品。
找到
viewDidLoad
方法,并添加下列代码到里面:
if let filePath = Bundle.main.path(forResource: "Products", ofType: "plist") { products = Product.productsList(filePath) }
它使用
Product
类从plist文件中加载了产品的数组(Product类是在教程一开始时被添加的),并保存到了
products
的propert中。现在你可以使用这个数组来填充pop-up按钮了。
打开
Main.storyboard
,选择
View Controller Scene
,并切换到
Assistant Editor
。确保选中
ViewController.swift
,从pop-up按钮
拖拽
到
ViewController.swift
来创建一个名为
productsButton
的outlet。确认type为
NSPopUpButton
。
返回
ViewController.swift
并添加下列的代码到
viewDidLoad
方法的末尾:
//1 productsButton.removeAllItems() //2 for product in products { productsButton.addItem(withTitle: product.title) } //3 selectedProduct = products[0] productsButton.selectItem(at: 0)
这些代码做了如下的事:
- 移除了pop-up按钮中全部的项目,去除了Item1和Item2的记录。
- 为每个产品添加了一个项目,展示它的标题。
- 选择第一个产品和pop-up按钮中的第一个项目。确保了所有的事始终如一。
问题的最后一部分,是响应pop-up按钮选择的变化。找到
valueChanged
并添加下列的行:
if let bookTitle = sender.selectedItem?.title, let index = products.index(where: {$0.title == bookTitle}) { selectedProduct = products[index] }
这个代码尝试获取被选择的书的标题,并在产品中搜索这个标题的产品的的序号。使用这个序号,来设置
selectedProduct
为正确的产品。
现在,你只需要在选择的产品发生变化时,通知
OverviewController
。为此你需要一个指向
OverviewController
的引用。你可以在代码中获取引用,但首先你必须添加另一个property到
ViewController.swift
中来持有这个引用。添加下列的代码到
ViewController
的实现中:
private var overviewViewController: OverviewController?
你可以在
prepare(for:sender:)
方法中获得
OverviewController
的实例,它会在当view controller被嵌入到container view时被调用。添加下列的方法到
ViewController
的实现中:
override func prepare(for segue: NSStoryboardSegue, sender: Any?) { guard let tabViewController = segue.destinationController as? NSTabViewController else { return } for controller in tabViewController.childViewControllers { if let controller = controller as? OverviewController { overviewViewController = controller overviewViewController?.selectedProduct = selectedProduct } // More later } }
这个代码完成了下列的事:
- 在可以做到的情况下,获取对Tab View controller的引用。
- 遍历它全部的子view controller。
-
检查当前的view controller是否是
OverviewController
的实例,如果是的话,设置它的selectedProduct
property。
现在,在
valueChanged
方法的
if let
代码块中添加下面这行代码。
overviewViewController?.selectedProduct = selectedProduct
运行项目,查看当你选择了一个不同的产品时,UI是怎么更新的。
现在你将为Details这个tab创建view controller。
前往 File/New/File… ,选择 macOS/Source/Cocoa Class ,单击 Next 。将类命名为 DetailViewController ,作为 NSViewController 的子类,取消勾选 Also Create XIB for user interface 。单击 Next 并保存。
打开
Main.storyboard
并选择
Details Scene
。在
Identity Inspector
中将类改变为
DetailViewController
。
添加一个
image view
到detail view上。选择它并单击
Add New Constraints
按钮来创建约束。设置
width
和
height
约束的值为
180
,top
top
约束的值为
standard
value。正如你在
OverviewController
做的一样,给它一张图片来让它更容易被看到。
单击底栏的 Align 按钮,并添加一个 Horizontally in the Container 的约束来让view居中。
在image view下面添加一个label。将字体改为bold,大小为19,然后单击 Add New Constraints 按钮来使用 standard values添加 top , leading ,和 trailing 的约束。
添加另一个label到之前的label下面。选择它,并单击 Add New Constraints 按钮来使用 standard values添加 top , leading 和 trailing 的约束。
使view变得更高,然后拖拽一个 Box 到上一个label的下面。选择它,并使用 standard value添加 top , leading , trailing 和 bottom 约束。
打开 Attributes Inspector 并将box的字体改变为 System Bold ,size改变为 14 。将title改变为“Who is this Book For?”。
NSBox
是一个很好的组织相关的UI元素的方式,给他们一个名字,你可以在Xcode的
Document Outline
中看到它们。
为了完成UI,拖拽一个label,到
NSBox
的内容中。选择这个label并单击
Add New Constraints
按钮来添加
top
,
leading
,
trailing
,和
bottom
约束,全部使用
standard
value。
更新frame之后,UI看起来应该像是这样:
为了给那些控件创建outlet,打开 Assistant Editor ,并确保 DetailViewController.swift 已打开。添加四个IBOutlet,为他们命名如下:
-
NSImageView命名为
productImageView
。 -
label名为
titleLabel
,字体为粗体。 -
下面的label名为
descriptionLabel
。 -
NSBox中的label名为
audienceLabel
。
通过恰当的outlet,添加实现来展示产品的细节。添加下列的代码到
DetailViewController
类的实现中:
// 1 var selectedProduct: Product? { didSet { updateUI() } } // 2 override func viewWillAppear() { super.viewWillAppear() updateUI() } // 3 private func updateUI() { if isViewLoaded { if let product = selectedProduct { productImageView.image = product.image titleLabel.stringValue = product.title descriptionLabel.stringValue = product.descriptionText audienceLabel.stringValue = product.audience } } }
你可能早已熟悉这个代码,因为它和Overview view controller的实现非常相似。这个代码:
-
定义了一个
selectedProduct
property并在它发生变化时更新UI。 - 当view出现的时候(也就是当detail view的tab被选中时)更新UI。
-
使用适当的outlet,来设置在label和image view上的产品信息(通过
updateUI
)。
当产品的选择发生变化时,你需要在detail view controller中改变选择的产品,来更新它的UI。打开
ViewController.swift
并添加一个property来持有到detail view controller的引用。在
overviewViewController
property的下方,添加下列代码:
private var detailViewController: DetailViewController?
找到
valueChanged
,并添加下列的代码到里面:
detailViewController?.selectedProduct = selectedProduct
这会在pop-up的选择发生变化时,更新view controller的selectedProduct的property。
最后一个改变位于
prepare(for:sender:)
中。找到评论
// More later
并用下列代码将其替换:
else if let controller = controller as? DetailViewController { detailViewController = controller detailViewController?.selectedProduct = selectedProduct }
这会在detail view被嵌入时,更新
selectedProduct
。
运行项目,并enjoy你完成的应用!
你可以在 这里 下载最终的项目。
在这个macOS view controller的教程中,你学到了下面的东西:
- 什么是view controller,以及它和window controller之间的比较。
- 怎样创建一个定制的view controller的子类。
- 怎讲将你的view中的元素,连接到view controller。
- 怎样从view controller中操纵view。
- view controller的生命周期,以及怎样hook到不同的事件中。
除了你添加到定制的view controller的子类的功能,苹果还为你提供了很多內建的子类。为了查看有哪些可用的view controller,请查看 文档 。
如果你还没有读过它,你应该看一下Gabriel Miro的杰出的 window和window controller教程 .
View controller是构建一个macOS app的最有力和最有用的方面之一,有足够的更多的东西可以去学。然而,你已经具备了走出这里并玩转构建app的知识 - 这正是你现在应该做的!