Skip to content

Latest commit

 

History

History
1887 lines (1885 loc) · 77.2 KB

OS X View Controllers Tutorial.md

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的知识 - 这正是你现在应该做的!