原文地址 翻译:DeveloperLx
更新笔记: 这个macOS的教程Cocoa Bindings,已由Andy Pereira更新到Xcode 8和 Swift 3的版本。 原教程 撰写自Jake Gundersen。
Cocoa bindings,或简单的 bindings ,使你从花费大量时间撰写 粘合代码 中解放出来;那就是,当使用Model-View-Controller(MVC)模式时,在controller中创建model和view之间的连接。
Cocoa bindings有一个简单的目标:写更少的代码。你会发现,真的可以通过这么做来达到这个目的。
在这个macOS教程Cocoa Bindings中,你将创建一个app,来展示从App Store中通过iTunes API获取的搜索结果,借此你会学到如何使用Cocoa bindings来完成如下的事:
- 在Interface Builder设置一个数据模型的property和一个UI元素(例如label或button)之间的关系
- 设置默认值
- 应用格式化,例如货币或日期格式
- 改变数据结构,例如当转化数值为代表的相应颜色时
尽管你无须花费接下来的几个小时进行编码,这里仍然有相当的在 Interface Builder 中使用 Auto Layout 的工作,因此熟悉这些工具是首要的条件。
在这里 下载初始项目。
运行项目,你将看到它有一个很棒的界面 - 但是没有数据。
你也会看到有一些文件是对你有帮助的。 iTunesRequestManager.swift 包含一个带有两个静态方法的结构体。第一个方法为提供的搜索字符串向iTunes的API发送查询,并下载包含iOS App结果的JSON;而第二个方法则是用来异步下载图片的助手方法。
第二个文件, iTunesResults.swift ,定义了一个可以匹配从iTunes搜索方法下载到的数据的数据模型类。
注意
:所有在
Result
类中的变量都被定义为
dynamic
型。这是因为bindings是依赖于键值编码的,因此要求了Objective-C的运行时特性。添加
dynamic
关键字确保了访问那些属性总是通过使用Objective-C的运行时特性动态分配的。
这个类是继承自 NSObject 的;这同样是由bindings要求的。你将在后面当添加一个变量到你的视图控制器类中时,了解到它的原因。
首先,你将通过iTunes的API获取搜索的结果,并将它们添加到一个
NSArrayController
中。
打开 Main.storyboard 并查看 View Controller Scene 中的对象。注意到那些你将设置binding的对象,它们都包含标签 ‘(Bind)’ 。
NSArrayController
对象管理
NSTableView
的内容。这个内容通常表现为一个模型对象数组的形式。
注意
:
NSArrayController
提供的不仅仅是一个数组,还包括管理对象的选取、排序、和过滤。Cocoa Bindings会重度地使用这些功能。
打开
Main.storyboard
。并在
Object Library
中找到
NSArrayController
对象,并把它拖拽到document outline中的
View Controller Scene
组的对象列表的下面:
接下来,打开assistant editor,并确保你工作在文件 ViewController.swift 上。按住Control从 Storyboard 拖拽到 ViewController.swift 中的 Array Controller 对象上,并将其命名为 searchResultsController :
现在你已准备好了使用搜索框和按钮来获取一个搜索结果的列表,并添加它们到
searchResultsController
对象上。
按住Control,将storyboard中的 search button 拖拽到 ViewController.swift 的源码中来创建一个动作方法。选择来创建一个 Action 的连接并命名为 searchClicked 。
现在,添加下面的代码到
searchClicked(:_)
方法中:
//1
if (searchTextField.stringValue == "") {
return
}
//2
guard let resultsNumber = Int(numberResultsComboBox.stringValue) else { return }
//3
iTunesRequestManager.getSearchResults(searchTextField.stringValue,
results: resultsNumber, langString: "en_us") {
results, error in
//4
let itunesResults = results.map {
return Result(dictionary: $0)
} //Deal with rank here later
//5
DispatchQueue.main.async {
//6
self.searchResultsController.content = itunesResults
print(self.searchResultsController.content)
}
}
回顾一下每行代码:
- 检查文本输入框;如果它是空的,就不会发送查询到iTunes的搜索API。
- 获取下拉菜单中的值。这个数字被传递给API,并控制返回多少个结果。在下拉菜单中有一些预先配置的选项,但你也可以输入其它的数字 - 200是最大的值。
-
调用
getSearchResults(\_:results:langString:completionHandler:)
方法。它传递来自组合框的结果的数字,和你输入到文本输入框中的查询字符串。并通过完成的句柄,来返回一个Dictionary
的数组的对象,或当完成查询的时候出现了问题时,返回一个NSError
的对象。 -
这里你使用了一些Swift风格的数组进行映射,来传递字典到初始化方法,来创建
Result
对象。当完成后,itunesResults
变量就包含了一个Result
对象的数组。 -
在你设置新的数据到
searchResultsController
上前,你需要确保你正在主线程上。因此你使用DispatchQueue.main.async
来获取主线程。你尚未设置任何bindings,但一旦你有了,变更searchResultsController
上的content property将更新NSTableView
(以及潜在的其它的UI元素)在主线程上。在后台线程更新UI永远都是禁忌。 -
最后,设置
NSArrayController
中的content property。这个array controller有一些不同的方法来添加或删除它所管理的对象。每次当你搜索时,你想要清除之前的结果,并使用最新的查询的结果。此时,打印searchResultsController
的内容到控制台上,来验证每件事都如同计划中一般进行。
现在,添加下列的
ViewController
extension:
extension ViewController: NSTextFieldDelegate {
func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
if commandSelector == #selector(insertNewline(_:)) {
searchClicked(searchTextField)
}
return false
}
}
当用户在文本输入框中点击Enter时,就会调用
searchClicked(\_:)
,就如同你点击搜索按钮一样。这使得使用键盘搜索更加得快速和容易。
运行项目,输入 flappy 到搜索栏中并点击Enter按钮或单击搜索按钮。你应该会在控制台中看到一些类似下面所示的内容:
是时候来获取这个教程的精要了!
第一步是将array controller绑定到table view上。
打开 Main.storyboard 并选择标题为 Search Results Table View (Bind) 的列表。打开 Bindings Inspector :它是右侧窗格中的倒数第二个,就位于 View Effects Inspector 之前。
展开 Table Contents 头下的 Content 选项。勾选靠近 ‘Bind to ‘ 的选框并确保 Search Results Controller 展示在下拉菜单中。最后,确保 Controller Key 被设置为 arrangedObjects ,就像:
运行项目,并搜索一些你知道的事,就返回大量的结果。你会看到至少5个结果,除非你改变下拉菜单中的数字。由于bindings,这个array controller就自动地代表了table view中的内容。然后,他们都叫“Table View Cell”。
你会得到一堆重复的匹配,因为cell中的text field不清楚它们应当读取在data model中哪个的property。
打开 Main.storyboard 并前往 View Controller Scene 。展开table view中的对象知道你发现名为 Title TextField (Bind) 的文本输入框。选择这个对象并打开 Bindings Inspector 。
展开 Value 选项并绑定到 Table Cell View 对象上。确保 Model Key Path 为 objectValue.trackName 。
objectValue
是一个在table cell view上的property,它从
NSTableView
上的每个cell view对象的到table的binding中获取相应的值。
在这个case中,
objectValue
等于这行的
Result
模型对象。
通过将其值绑定到 objectValue.artistName 上,为 Publisher TextField (Bind) 重复上面的过程。
运行项目,然后再次搜索。这次,标题和publisher会展示如:
对于缺失的rank列呢?Rank并不设置在你从iTunes获取的数据模型的对象上。然而,从iTunes得到的结果的顺序,确实告诉了你当搜索iTunes时展示在设备上的顺序。
只需再多做一点工作,你就可以设置排名的值了。
在
ViewController
中
//Deal with rank here later
评论的下面,添加下列的代码:
.enumerated()
.map({ index, element -> Result in
element.rank = index + 1
return element
})
这个代码调用了
enumerated()
已获取序号和相应这个序号的对象。然后调用
map(:_)
来为每个对象设置排名的值,并返回结果的数组。
现在,回到
Main.storyboard
,选择
Rank TextField (Bind)
并打开
Bindings Inspector
。在
Value
区,绑定到
Table Cell View
。确保
Controller Key
是空的,并设置
Model Key Path
为
objectValue.rank
。
运行项目,app会在table view的第一列中展示排名:
现在,你需要将用户选择的
Result
选项绑定到UI的剩余的部分。
在table中绑定选择包含两步:
-
首先绑定
NSArrayController
到table的选择上。 -
然后,你可以绑定NSArrayController中
selection
对象的property到各自的label和其它的property。
打开 Main.storyboard 。选择 Search Results Table View (Bind) 并打开 Bindings Inspector 。
在 Table Content 区展开 Selection Indexes 选项。查看 绑定到 Search Results Controller 对象。
在
Controller Key
框中输入
selectionIndexes
。这个table有一个
selectionIndexes
property,它包含一个序号的集合,这些序号代表了用户在table中选择的内容。
在这个case中,我已设定了这个table view同时只能允许一行被选中。当然如果app需要的话,你可以允许同时选择多行,就如同Finder允许你同时选择多个文件。
NSArrayController
对象有一个
selection
property,它会返回一个对象的数组。当你把table view的
selectionIndexes
property绑定到array controller时,array controller中的
selection
property就会被相应的在table中选择的序号构成的对象所填充。
下面一步是吧label和其它的UI元素绑定到选择的对象上。
找到并选择 App Name Label (Bind) 。将他的value绑定到 Search Results Controller 上。 Controller Key 为 selection , and Model Key Path 为 trackName 。
运行项目,选择在table view中的任一app,它的title就会出现在文本输入框中:
你以及看到了,从你的模型获取数据,再放入到UI中是多么得容易。不过,如果数据需要以某种方式进行一下格式化,例如货币或日期,该怎么做呢?
幸运的是,有一系列內建的对象,使得改变指定数据展示到label上的形式变得非常得容易。
找到title为 Price Label (Bind) 的label。将它绑定到 Search Results Controller 对象上,并确保 Controller Key 为 selection 。
设置
Model Key Path
为
price
,然后,在Object Library中找到
Number Formatter
,将它拖拽到名为
Label
的
NSTextFieldCell
上,就在
Price
文本输入框的下面。
最后,选择 Number Formatter ,打开 Attributes Inspector 并改变 Style 为 Currency 。
当以上都完成后,你的storyboard和inspector应当看起来像下面这样:
运行项目,从列表中选择任一app,现在货币应当全部展示的是正确的:
注意 :数字格式化器是非常强大的。除了货币外,你也可以控制小数点后有几位数字,或者用单词拼写出数字。
已有的格式化器对象有日期,字节计数,和其它几个不太常见的情况。如果它们都满足不了你,你甚至可以定制你自己的格式化器。
接下来你将使用 Byte Count Formatter 来显示文件的大小。
找到并选择 File Size Label (Bind) ,打开 Bindings Inspector 并绑定到 Search Results Controller 上。设置 Controller Key 为 selection 以及 Model Key Path 为 fileSizeInBytes 。
然后在 Object Library 中找到 Byte Count Formatter 并附加到 NSTextFieldCell 上。这里无需进行任何的配置,字节格式化器的默认设置就可以工作得很好。
你应当在document outline看到你的字节技术格式化器就像下面这样:
运行项目,在列表中选择一个app,你就会看到使用合适单位的文件的大小,例如KB,MB和GB:
你现在已经绑定了一些东西,所以这里有一个剩余的你需要去绑定的键的短表:
- 绑定 Artist Label (Bind) 到 artistName 。
- 绑定 Publication Date (Bind) 到 releaseDate 。
- 添加一个 Date Formatter ;默认的设置就很棒。
- 绑定 All Ratings Count (Bind) 到 userRatingCount 。
- 绑定 All Ratings (Bind) 到 averageUserRating 。
- 绑定 Genre Label (Bind) 到 primaryGenre 。
全部的这些label应当被绑定到 Search Results Controller 并设置 selection 的controller key。
为了让你的UI更加精确,你也可以绑定
Description Text View (Bind)
,
Attributed String
binding到
itemDescription
Model Key Path上。确保你绑定的是
NSTextView
,它在层级中有一些深。
并不是
NSScrollView
,它在顶层。
运行项目,你会看到大部分的UI已被填充:
下面一步是为icon绑定其图像到 Icon Image View 上。这个有一点trick,因为JSON并不包含事实上的图像,而是用一个图片的URL来代替。
Result
包含了一个方法来下载图片文件,并让它成为
artworkImage
property的可用
NSImage
对象。
你并不想一次下载全部的图标 - 而是只需下载那个当前在table中选择的那个。你将在选择发生变化的时候下载一个新的icon。
添加下列的方法到 ViewController 中:
//1
func tableViewSelectionDidChange(_ notification: NSNotification) {
//2
guard let result = searchResultsController.selectedObjects.first as? Result else { return }
//3
result.loadIcon()
}
这里是详情:
-
tableViewSelectionDidChange(\_:)
会在用户每次table选择不同的行时被触发。 -
array controller的property
selectedObjects
返回了一个数组,它包含了全部的在table中选择的行的序号。在这个case中,这个table只允许单选行,因此这个数组总是只包含一个对象。你用result
对象的形式来储存它。 -
最后,你调用了
loadIcon()
。这个方法会在后台线程下载图像,然后下载完后在主线程更新Result
对象的artworkImage
property。
你的代码已经ready了,就可以去绑定image view了。
返回 Main.storyboard ,选择 Icon Image View (Bind) 对象并打开 Bindings Inspector 。
前往 Value 区并绑定到 Search Results Controller 上,设置 Controller Key 为 selection ,以及 Model Key Path 为 artworkImage 。
你是否注意到了 Value Path 和 Value URL 这里?这些绑定都仅仅是为了本地化资源。你可以将 can 它们连接到网络资源上,但这样会阻塞UI线程,直到资源完成下载之后。
运行项目,搜索 fruit ,然后选择一行。你就会在icon的图像下载完成时看到它。
在description文本view下的collection view现在看起来有一点光秃秃的。是时候用一些画面来填充它了。
首先你要将collection view绑定到
screenShots
property上,并确保
screenShots
数组被正确地填充。
选择 Screen Shot Collection View (Bind) 。打开 Bindings Inspector 并在 Content 组中展开 Content binding。
绑定到 Search Results Controller 上,设置 Controller Key 为 selection ,以及 Model Key Path 为 screenShots 。
screenShots
数组开始是空的。
loadScreenShots()
方法会下载图片文件,并以
NSImage
对象的形式填充到
screenShots
数组中。
添加下面这行代码到
ViewController.swift
的
tableViewSelectionDidChange(\_:)
方法中,就在
result.loadIcon()
的后面:
result.loadScreenShots()
它填充了截图并创建了正确数量的view。
下面你需要做的事,是设置正确的collection view项目的原型。尽管collection view项目的场景已经展示在了storyboard中,但却并未连接到collection view上。你必须在代码中创建这个连接。
添加下面的代码到
ViewController.swift
中的
viewDidLoad()
方法的末尾:
let itemPrototype = self.storyboard?.instantiateController(withIdentifier:
"collectionViewItem") as! NSCollectionViewItem
collectionView.itemPrototype = itemPrototype
既然collection view知道怎样通过原型来创建每个项目,你需要做的就是通过绑定来提供每个项目的内容。
打开 Main.storyboard 并选择 Collection View Item Scene 中的 Screen Shot Image View (Bind) 。你会发现这个浮点值挨着主视图控制器。
绑定 Value 选项到 Collection View Item 对象上。controller key应当是空的,而 Model Key Path 则应当是 representedObject 。
representedObject
property代表了在collection view数组中相应的项目;在这个case中,它就是一个
NSImage
对象。
运行项目,你会看到图像出现在了描述文本的下面:
God Job!在结束之前,只有几点Cocoa Bindings的特性需要做到了。
你的UI可以使用一些反馈。用户不喜欢在下载东西时一直盯着静态的屏幕,当对于用户的动作没有任何响应时,他们会认为那是发生了最糟糕的情况。
接下来你会在当下载图像时,向用户展示一个spinner,代替只有一个静态的图像。
你可以很轻松地绑定一个进度spinner到
ViewController
中的一个新property上。添加下列的property到
ViewController
:
dynamic var loading = false
为了正常工作,加载需要满足两件事:
dynamic
关键字,且其父类为
NSObject
的子类。Bindings依赖于KVO,一个并非继承自NSObject的Swift的类无法使用KVO。
在
searchClicked(:_)
中添加下列的代码,就在调用
getSearchResults(\_:results:langString:completionHandler:)
之前:
loading = true
在相同的方法中找到设置
on
searchResultsController
的
content
property那行。就在那行前面添加下列的代码:
self.loading = false
接下来,打开
Main.storyboard
并选择
Search Progress Indicator (Bind)
。你要去绑定进度spinner的两个property:
hidden
和
animate
。
首先,展开 Hidden 组,并绑定到 View Controller 。 Controller Key 置为空,而 Model Key Path 则为 self.loading 。
在这个case中,你想要当
loading
为
true
是
hidden
置为false,反之亦然。有一个轻松的办法来做到这点:使用
NSValueTransformer
去翻转bool的值。
NSValueTransformer
是一个类,它可以当在UI和数据模型移动的时候,帮助你转换转换形式或数据的值。
为了实现更多复杂的转换,你可以子类化这个对象,你可以在这篇教程中学到更多关于NSValueTransformer的东西: 怎样在Mac App中使用Cocoa Bindings和Core Data .
从 Value Transformer 的下拉列表中选择 NSNegateBoolean 。
绑定 Animate 的值到 View Controller 对象上。设置 Controller Key 为空, Model Key Path 为 self.loading 。
这个布尔值不需要取反。这个绑定看起来应该是这样子的:
运行项目;搜索一下,就会返回大量的结果,因此你就有时间去观察spinner来做它的事情了:
Cocoa Bindings可以做到的事,远远不止我们现在学到的这些:你可以绑定颜色和字体到label上,打开和禁用控件,甚至为label根据它们的状态设置不同的值。
运行项目,你会注意到在你全部的label之前,你会看到 No Selection 。这点对于一个用户启动它们的app时并不是很好。
找到标题为
Price Label (Bind)
的label,展开
Value
。在
No Selection Placeholder
的下面,输入
--
,如同下面这样:
运行项目,你会看到 Price label现在有了一个很好的占位的值:
设置全部的label的占位的值为 No Selection 。
注意 :如果你想让label为空,你可以设置 No Selection Placeholder 为一个 空格 。
这就是Cocoa Bindings在macOS上的基本内容。你已经看到了它是多么容易地让你连接数据和UI。
在这篇教程中,你学到了下面的事情:
- 怎样使用Interface Builder去快速而轻松地绑定对象到数据上。
- 怎样保持模型和视图和用户当前的选择同步。
- 怎样使用方法和绑定一起控制表现,以及组织数据。
- 怎样快速地搭建类似进度spinner的UI特性。
你可以在 这里 下载最终的项目。希望你可以通过采用这个技术节省很多的时间(和代码!)。
每个binding都有很多小的设置和选项,很多在这篇教程中并未提到。你可以访问苹果提供的 Cocoa bindings选项 。它覆盖了很多关于bindings窗口的选项作用的细节。
我希望你可以喜欢这个macOS的Cocoa Bindings教程,并获得一些新的技术去使用,来加速你的开发过程。你已经打开了一个全新的宇宙!