Skip to content

Latest commit

 

History

History
1887 lines (1885 loc) · 77.2 KB

File metadata and controls

1887 lines (1885 loc) · 77.2 KB

OS X View Controllers教程

macOS view controllers tutorial

使用这个macOS View Controller的教程,来学习怎样控制UI!

在撰写任意代码的时候,对关注点做出清晰的分割是非常重要的 - 功能应当被分为恰当的较小的类。这可以让代码易于维护且容易理解。苹果在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 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?

你可能想要知道,什么时候该使用仅仅一个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来构建负责的用户界面,并使用它们,如构建的块来组成完整的用户界面。

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。你应会看到你的应用的主窗口。

window-empty

现在它是空的,但不要担心 - 这只是开始。

创建用户界面

打开 Main.storyboard ,选择 View Controller Scene ,并拖拽一个 pop-up button 到view上。你将使用这个 pop-up button 来展示产品的列表。

add-popup

使用自动布局来设置它的位置。这个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按钮的底部。

add-container

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应当看起来像这样:

FinishedVC

现在你将创建一个动作,它将在这个按钮的选择发生变化时被调用。打开 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 键。

delete-viewcontroller

Tab View Controller

现在你已经添加了用来展示产品信息的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 并将其拖拽到画布上。

drag-tabcontroller

现在这个tab controller已经在storyboard上了,但到现在都没有用过。使用一个embed segue来将它连接到container view;从container view 拖拽 到Tab View Controller.

control-drag-tabview

在弹出的菜单中选择 embed 。
Embed

这样变化之后,当app运行container view时,container view的区域被替换为Tab View Controller的view。双击Tab View左侧的那个tab,并将它重命名为 Overview 。重复同样的操作,将右边的tab重命名为 Details 。

rename-tabs

运行app

现在这个tab view controller已经展示出来了,你可以使用tab来在两个不同的view controller之间进行选择。但现在还不是显而易见的(noticeable),因为这两个view恰好是相同的,但当你选择了一个tab时,在内部tab view controller确实是将他们替换了的。

Overview的View Controller

接下来你需要为 Overview tab创建相应的view controller。

前往 File/New/File… ,选择 macOS/Source/Cocoa Class ,并单击 Next 。将类命名为 OverviewController ,使其成为 NSViewController 的子类,确保 Also Create XIB for user interface 未被选中。单击 Next 并保存。

add-overview

返回 Main.storyboard 并选择 Overview Scene 。单击view上的蓝色的圈,并将右侧的 Identity Inspector 中的class更改为 OverviewController 。

OverviewVC

拖拽三个 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:

  1. 中间的label: priceLabel
  2. 底部的label: descriptionLabel
  3. 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()
  }
}

一步一步来看代码:

  1. numberFormatter 是一个 NumberFormatter 对象,用来展示加个的值,以货币的形式格式化。
  2. 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
    }
  }
}
  1. 检查这个view是否已加载。 isViewLoaded 是 NSViewController 的一个property,当这个view已被加载到内存中时,它的值就变为true。当这个view被加载完毕后,访问全部的view相关的property,例如label,就都会是安全的了。
  2. Unwrap selectedProduct ,来查看是否这里有一个产品。之后,label和image就被更新,来展示适当的值。

这个方法已在产品发生变化时被调用,但也需要在这个view准备要被展示时被调用。

View Controller的生命周期

由于view controller是负责管理view的,因此它会暴露让你可以hook住相应的view的事件的方法。例如当这些view要从storyboard加载到屏幕上时,或这些view将要出现在屏幕上时。这些基于事件的方法的集合,被称作 view controller生命周期 。

一个view controller的生命周期被分为三个主要的部分:创建,使用期(lifetime),还有终止。每个部分都有你可以重写,以添加额外工作的方法。

创建

  1. viewDidLoad 会在这个view被全部加载时被调用,可以用来一次性地初始化例如,配置一个数的formatter,注册通知,或调用仅仅需要被执行一次的API。
  2. viewWillAppear 会在每次view将要展示在屏幕上时被调用。在我们的应用中,它会在每次你选择Overview的tab时被调用。这是一个更新你的UI或刷新你的数据模型的好时机。
  3. viewDidAppear 会在view被展示到屏幕上时被调用。在这里你可以启动一些花哨的(fancy)动画。

使用期

一旦创建了一个view controller,它就进入了一个可以用来处理用户交互的时期。它有三个方法针对这个时期:

  1. updateViewConstraints 会在每次布局发生变化时被调用,例如window的大小被调整时。
  2. viewWillLayout 会在view controller的view的 layout 方法被调用前调用。例如,你可以使用这个方法来调整约束。
  3. viewDidLayout 会在 layout 方法被调用之后调用。

终止

这些是对应的方法:

  1. viewWillDisappear 会在view消失之前被调用。在这里你可以停止你在 viewDidAppear 启动的那些花哨的动画。
  2. 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)

这些代码做了如下的事:

  1. 移除了pop-up按钮中全部的项目,去除了Item1和Item2的记录。
  2. 为每个产品添加了一个项目,展示它的标题。
  3. 选择第一个产品和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
  }
}

这个代码完成了下列的事:

  1. 在可以做到的情况下,获取对Tab View controller的引用。
  2. 遍历它全部的子view controller。
  3. 检查当前的view controller是否是 OverviewController 的实例,如果是的话,设置它的 selectedProduct property。

现在,在 valueChanged 方法的 if let 代码块中添加下面这行代码。

overviewViewController?.selectedProduct = selectedProduct

运行项目,查看当你选择了一个不同的产品时,UI是怎么更新的。

Detail View Controller

现在你将为Details这个tab创建view controller。

前往 File/New/File… ,选择 macOS/Source/Cocoa Class ,单击 Next 。将类命名为 DetailViewController ,作为 NSViewController 的子类,取消勾选 Also Create XIB for user interface 。单击 Next 并保存。

create-detailviewcontroller

打开 Main.storyboard 并选择 Details Scene 。在 Identity Inspector 中将类改变为 DetailViewController 。

detail-vcname

添加一个 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,为他们命名如下:

  1. NSImageView命名为 productImageView 。
  2. label名为 titleLabel ,字体为粗体。
  3. 下面的label名为 descriptionLabel 。
  4. 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的实现非常相似。这个代码:

  1. 定义了一个 selectedProduct property并在它发生变化时更新UI。
  2. 当view出现的时候(也就是当detail view的tab被选中时)更新UI。
  3. 使用适当的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的知识 - 这正是你现在应该做的!