3

编译自: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保存你的项目。

kkkk

下载项目需要的资源文件。这个压缩包包含所需的图片以及书籍商品所需要的数据,他们保存在Products.plist文件。此外你还能看到一个名为Product.swift的源码文件。这个文件包含Product类,它解析了 Product 对象的结构。接下来把他们添加到RWStore项目中。

从项目导航中选择选择Assets.xcassets,将刚刚下载的图片资源拖进去。

然后将Products.plistProduct.swift拖到项目导航中。确保勾选了Copy items if needed

这时编译运行应用程序。

可以看到空白的主窗口,但不要惊慌,正常运行就是好的开始。

创建用户界面

打开Main.storyboard,选择View Controller Scene,拖拽一个pop-up 按钮到 view 中,之后会用到。

通过 AutoLayout 来设置 它的位置。选择刚刚拖拽的 Pop-up 按钮,点击下方的Pin按钮。在弹出来的窗口中,将其LeadingTrailingTop的约束值都设置为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 ControllerContainer 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。

拖拽三个 labelOverviewController 的视图的左上方,一个接一个的排列。添加一个image view在视图的右上角。

提示:默认情况下,image view 没有边框,给它设置个图片,这样好找。选择Attributes Inspector,选择gamesImage字段。这个图片是刚刚资源文件里的,应该可以看到效果啦。

选择最最上面的标签。Attributes Inspector里面将字体设置为System Bold,字号设置为 19。

这时候的视图看起来是这样的:

好!让我们使用 AutoLayout 来调整一下布局。

选择 image view,点击下面的Pin按钮。给其添加约束:top 和 trailing设置为 standard valuewidthheight的值设置为 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

重复上面的操作,将剩余的控件都连接到代码中:

  1. 中间的标签命名为:priceable

  2. 下面的标签命名为:descriptionLabel

  3. image view 命名为:productImageView

和大多数 UI 控件一样,标签和 image view 都有子视图,选择的时候仔细一下,别选错了,比如NSImageView选成了NSImageCellNSTextField选成了NSTextFieldCell

点击OverviewController,加上下面的代码:

//1
let numberformatter = NSNumberFormatter()
//2    
var selectedProduct: Product? {
  didSet {
    updateUI()
  }
}

这段代码:

  1. number formatter是一个NSNumberFormatter,用于正确格式化价格。

  2. 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
    }
  }
}
  1. 通过 viewLoaded 属性判断 NSViewController 是否已经加载,如果已经加载完毕,就可以安全的访问与视图有关的属性了。

  2. 解包selectedProduct确定是否已经选择了产品。然后显示正确的值。

这个方法现在已经会在产品变换的时候调用,还需要在视图加载完毕的时候调用。

视图控制器生命周期

从视图控制器具备响应视图事件能力开始,它就为视图生命的各个阶段提供了各种回调事件。比如视图从 storyboard 被加载,或者显示在屏幕上这种都属于被 Hook 的事件范围。所有这些机遇事件的方法被统称为view controller life cycle

视图控制器生命周期可以被划分成三个主要部分:创建、运转、终止。每一个部分都提供了可重载的方法满足你的需要。

创建

  1. viewDidLoad()当视图被首次完整加载的时候调用,一些只执行一次的初始化工作适合在这个时候进行,如创建数值格式化对象,注册通知,某些只需要调用一次的 API 等。

  2. viewWillAppear() 每当视图将要被显示的时候会被调用。比如我们刚刚选择 Overview 标签,每次切换它都会被调用。当数据发生变化,这是个更新到界面的好时候。

  3. viewDidAppear()每当视图显示在屏幕上的时候,这个方法会被调用。这时适合做一些动画。

运转

视图控制器被创建之后,一些与用户交互的事件就该登场了:

  1. updateViewConstraints()当布局每次被改变都会被调用,比如窗口大小变化。

  2. viewWillLayout()是布局将要发生的时候进行调用。如果你需要调整你的约束,可以在这时进行。

  3. viewDidLayout()当布局完成之后被调用。

当重载这三个方法的时候,在其中你必须调用他们的super

终止

终止与创建对应:

  1. viewWillDisappear() 当视图将要消失的时候调用。在viewDidAppear()开始的动画这时可以结束了。

  2. viewDidDisappear()视图消失之后这个方法被调用。一切你不需要的东西都可以在这时被干掉。比如已经无效的timer神马的。

生命周期实践

有关视图控制器生命周期重要的事情都已经告诉你了,现在进行一个小测试。

问题:你想把用户选择的产品的时候,让OverviewController的视图显示正确的产品详情。该在啥时候去执行更新视图的代码?

打开OverviewController.swift,添加下面的代码:

override func viewWillAppear() {
  updateUI()
}

重载了viewWillAppear,当用户看到视图之前,它会被正确更新。

数值格式化对象当前使用的是默认值,为了更好的展示,最好把它配置成货币格式。viewDidLoad()是做这事儿的好地方。

OverviewControllerviewDidLoad()方法添加下面的代码:

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)

这段代码做了一些微小的工作:

  1. 删除弹出按钮中所有的数据。

  2. 遍历商品数组,将所有商品的标题添加到弹出按钮。

  3. 选择数组中第一个商品。

最后,我们还需要在弹出按钮选择条目发生变化时做出响应,找到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
    }      
  }
}
  1. 得到标签视图控制器的引用。

  2. 遍历子视图控制器。

  3. 找到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按钮创建约束。weightheight设置为180top约束设置为standard value

点击Align按钮,给视图添加一个居中约束:Horizontally in the Container

在刚刚添加的图像视图下方添加一个标签控件,设置字体bold,字号19。点击Pin按钮,添加约束:topleadingtrailing,值为standard value

在刚刚设定的标签下方再添加一个标签。点击Pin添加约束:topleadingtrailing。值为standard value

拖拽一个NSBox在标签下方。给它添加约束:topleadingtrailingbottom,值为standard value

打开Attributes Inspector,设置字体为bold,字号14。将title改为Who is this Book For?

NSBox 用来组织一组相关联的 UI 元素很好用。而且有了标题看起来更明确。

拖拽一个标签控价在NSBox里面,选择这个标签控件,点击Pin按钮,添加topleadingtrailingbottom,全部设置为standard value

然后更新你的界面,看起来是这样的:

激活Assistant Editor,打开DetailsViewController.swift。添加四个IBOutlet,命名为:

  1. productImageView for the NSImageView.

  2. titleLabel for the label with the bold font.

  3. descriptionLabel for the label below.

  4. 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视图控制器里面的代码很类似,你应该已经很熟悉了:

  1. 定义表示当前选中商品的selectedProduct属性,当选择其他商品的时候更新视图。

  2. 每次视图被显示的时候,强制刷新(比如切换选项卡会就会触发)。

  3. 将商品信息显示在视图上的图像视图和标签控件中(通过 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 应用程序开发中,它是十分有用的组件,涵盖了许多值得学习的内容,加油!本文给开了个好头,马上去开发你想要的东东吧。

欢迎在下方留言进行讨论。


shiweifu
2k 声望744 粉丝