Mac OS Cocoa绑定教程
Cocoa绑定,或简易绑定,使你从花费大量时间编写绑定代码(在MVC模式下在控制器中联接模型和视图)的工作中解脱出来,
Cocoa绑定有一个简单的目标:少写代码。你将会发现掌握了这项技术之后真的可以实现这个目标。
在这个Cocoa绑定的macOS教程中,你将创建一个通过iTunes API从APP Store中获取搜索结果并显示出来的APP,并学会如何使用Cocoa绑定来完成下列工作:
- 在IB中建立模型的属性和UI控件(如标签或按钮)的联接关系
- 设置缺省值
- 设置格式,如货币或日期
- 更改数据的结构,如把数值转换成颜色来展示。
开始
虽然你不用花很多时间去编写绑定代码,但是界面的设计工作还是需要的,所以之前还是要熟悉一下IB的Auto Layout。
这里可以下载开始的项目文件
编译并运行,可以看到一个不错的界面,但是没有数据。
你可以发现这里面有几个辅助文件,iTunesRequestManager.swift 包涵了一个结构体和两个静态方法。第一个通过发送数据请求到iTunes的API,根据搜索字符串的结果,下载的包涵iOS App信息的JSON数据。第二个是帮助方法用来异步下载图片。
第二个文件 iTunesResults.swift, 定义了一个匹配iTunes搜索方法下载的数据结构的类。
注:所有的变量在结果类里都设置为
dynamic。这是因为Cocoa绑定是应用了Key-Value代码技术,需要Objective-C的运行环境。设置成dynamic可以保证,对这些属性的存取是在Objective-C的动态环境下动态调用的。这个类继承了NSObject,这也是运用Cocoa绑定的要求。稍后当你在视图控制器中添加变量的时候就知道为什么了。
用iTunes搜索
首先,你将获取到通过iTunes API 搜索到的结果并把他们加载到 一个NSArrayController。
打开 Main.Storyboard,观察一下视图控制器的场景。可以看到所有绑定的对象后面都标注了(Bind)。
设置一个 NSArrayController
NSArrayController对象管理NSTableView的内容。这些内容通常是从一个模型对象的数组获得的。
注:NSArrayController 通常比一个数组要复杂,包括选中对象管理,排序和过滤。Cocoa绑定大量使用这个功能。
打开 Main.storyboard。在组件库中拖出一个 NSArrayController,拖动到对象清单中,View Controller Scene 的下面。
接着,打开关联编辑器,保证 ViewController.swift 是在编辑状态。在storyboard中Ctrl-drag 这个Array Controller到ViewController.swift中添加一个引用,命名为searchResultsController。
添加搜索框和按钮
现在你已准备好了,可以使用搜索框和按钮去获取搜索结果的清单并把他们添加到 searchResultsController 对象中.
在storyboard中Ctrl-drag 搜索按钮到 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:) 传递进去下拉框中的返回结果数量和文本框中的搜索字符串。通过完成句柄(completion handler)进行返回,要么是字典类型的数值,要么在请求时出错产生NSError对象。注意这个方法已经实现了JSON的解析。
- 这里你使用了Swift风格的数组map方法把字典转化成了数组。完成后,itunesResults变量就存储了Result类的数组。
- 在你把新数据存储到searchResultsController之前,必须确认是在主线程中。因此你要使用DispatchQueue.main.async来得到主线程。你还未设置好绑定,一旦你设置好了,searchResultsController中的内容更改将会在当前线程中更新NSTableView(也可能是其他视图组件)。当然在后台线程中更新视图组件是不允许的。
- 最后,设置NSArrayController的内容。数组控制器可以用很多方法来添加和删除它所管理的数组。每次完成搜索后,你应该删除前面的结果,然后装入最后一次搜索的结果。现在,把搜索的结果输出到控制台,检查一下是否正确。
现在,在ViewController中添加拓展:
extension ViewController: NSTextFieldDelegate {
func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
if commandSelector == #selector(insertNewline(_:)) {
searchClicked(searchTextField)
}
return false
}
}
这段代码在文本框中按回车时调用searchClicked(_:),如同点击搜索按钮。这样就可以用键盘快速的进行搜索操作了。
编译并运行,在搜索框中输入flappy回车或点击搜索按钮。你会在控制台看到如下的结果:
你的第一次绑定
现在是本教材的主要部分了!
第一步是把数组控制器绑定到列表视图上。
打开 Main.storyboard 并且选择叫 Search Results Tables View(Bind)的列表视图。打开Binding Inspector:这是倒数第二个图标,在View Effects Inspector之前。
展开Table Contents下面的Content选项。勾选‘Bind to’旁边的选择框,并且使下拉框中的选项是‘Search Results Controller’。最后,Controller Key中设置的是 arrangedObjects,如图:
编译并运行,搜索一些你知道的东西,会返回大量的结果。除非你更改了下拉框中的数字,你会看到差不多5个结果。感谢绑定,数组控制器已经自动的在列表视图中展示自己的内容了。尽管内容都是“Table View Cell”。
由于每个单元格不知道要显示那个属性字段,所以你得到了一堆重复的提示内容。
绑定文本框和它们的属性
打开 Main.storyboard 并进入到View Controller Scene。展开列表控件下面的项,直到看到叫Title TextField(Bind)的文本框。点选这个控件后打开 Binding Inspector。
展开Value选项并绑定到Table Cell View控件上。并确保Model Key Path是OjectVaue.TrackName。
OjectValue是单元格视图的属性。是NSTableView把它绑定到每个单元格视图上的。
objectValueis,在这里,等于Result模型的一行。
重复上面的过程,把Publisher TextField (Bind)绑定到objectValue.artistName.
编译并运行,再搜索一下,现在书名和出版人显示出来了:
添加排序
如何对待缺失的顺序列?你的iTunes搜索数据模型中并没有设置顺序信息,事实上iTunes返回的搜索结果数据的顺序并不代表你要在列表视图中显示的顺序。
稍作设置我们就可以得到排序值。
在ViewController中//Deal with rank here later注释后面添加如下代码:
.enumerated()
.map({ index, element -> Result in
element.rank = index + 1
return element
})
这段代码调用 enumerated() 取得搜索结果对象的索引值,然后调用map(:_)方法把每个搜索结果对象的rank设好,并返回搜索结果对象数组。
现在,返回到 Main.storyboard,选择Rank TextField (Bind)并打开Bindings Inspector.在Value片段下面,绑定Table Cell View。确保Controllor Key是空的,并且设置 Model Key Path 为 objectValue.rank。
编译并运行,程序会在第一列显示排列顺序:
现在你需要把其他的UI控件和选中的结果进行绑定。
绑定列表视图的选中项
绑定列表中的选中项共需2步:
- 首先把NSArrayController绑定到列表的选中项。
然后可以用NSArrayController选中项的属性来绑定各自的标签和属性。
打开 Main.storyboard。选中 Search Results Table View (Bind) 并且打开Bindings Inspector。
展开Table Content下的Selection Indexes选项,钩选Bind to, 选择Search Results Controller项。
在Controller Key输入框中,输入selectionIndexes。列表对象具有selectionIndexes属性,该属性包含了用户在列表中选中的内容
在这个例子中,我们设置的是单选,你也可以更加程序的需要设置成多选。
NSArrayController 对象有一个selection属性,返回值是一个对象数组。当你把列表对象的selectionIndexes属性和数组控制器绑定时,数组控制器的selection属性就对应了列表中选中项的索引。
下一步就是把标签和其他的控件和选中对象进行绑定。
找到 App Name Label(Bind),绑定它的值到Search Results Controller。Controller Key设定为selection 并且在Model Key Path 中输入trackName。
编译运行,在列表中点击,在右边就会显示出标题信息:
你可以看到把数据和控件连接起来是多么的简单。但如果数据需要进行一些格式化的转换又该如何操作呢?比如货币或日期?
幸运的是,Cocoa自建的一些对象可以很容易的帮我们做到。
格式化绑定的数据
找到Price Label (Bind)标签. 绑定到Search Results Controller并且Controller Key设定为selection。
把 Model Key Path 设为 price,下一步在控件库里面找到Number Formatter。并把它拖到名叫 Label 的 NSTextFieldCell 下面。
最后,选中这个 Number Formatter,打开 Attributes Inspector 设置Style为Currency。
当你设置完成后storyboard和设置器将如下图所示:
运行,点击表中的任何项,货币显示都是正确的了:
注:Number formatters的功能很强大。除了货币之外还可以控制小数点位数,百分号,把数字转化成单词。
同样还有日期的、字节容量等格式化器,如果这些都不合用还可以自定义格式化器。
格式化字节
使用一个字节计数格式化器展示文件大小。
找到并选中 File Size Label(Bind) ,打开绑定设置并且绑定到Search Results Controller。设置Controller Key为selection,Model Key Path 为 fileSizeInBytes。
在控件库中拖出Byte Count Formatter 并把它加到NSTextFieldCell上。不需要额外的设置,它就能工作得很好。
storyboard的场景列表入图:
编译,测试一下,文件的大小将会按照合适的单位来显示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。
所有这些标签都要绑定到Search Results Controller 和 selection Controller Key。
为了使界面更加准确,可以把Description Text View (Bind)的Attributed String绑定到item-Description的Model Key Path. 确保绑定的是NSTextView,在NSScrollView下面好几层。
运行一下,这个界面大部分都运行起来了。
绑定图片
下一步是把图标和 Icon Image View 绑定好。这里有一点点复杂,因为JSON数据不包含图像数据,只有图像的URL信息。
我们的Result对象包含了下载图像文件的方法并且可以它转换成NSImage的artworkImage属性。
即时下载
你不需要一次下载所有的图标-只要下载当前选中项的图标就可以了。但选中项变化后再重新下载。
在ViewController中添加下列方法:
//1
func tableViewSelectionDidChange(_ notification: NSNotification) {
//2
guard let result = searchResultsController.selectedObjects.first as? Result else { return }
//3
result.loadIcon()
}
解释如下:
- 当列表中的选中项变化时都会触发tableViewSelectionDidChange(_:)。
- 数组控制器的selectedObjects属性会返回列表中选中项的索引值。在这个例子中,由于只允许单选,所以数字控制器中只有一个对象,把它存储到result中。
- 最后,调用callloadIcon(). 这个方法通过后台线程下载图片,并且在图片下载返回到主线程时更新Result对象的artworkImage属性。
绑定图片视图
现在代码已经完成了,需要绑定图片视图了。
回到Main.storyboard,选择Icon Image View(Bind)对象并打开Bindings Imspector。
在Value下,绑定Search Results Controller,Controller Key设为selection,Model Key Path设为artworkImage。
你是否注意到Value Path 和 Value URL选项?这些绑定都是用来绑定本地的资源的,你也可以连接到网络上,但这会阻挡界面的进程。
运行,搜索fruit任选一行,你可以看到照片了。
现在开始完成图集视图
填充图集视图
首先,把图集视图绑定到screenShots属性,确保screenShots数组被正确填充了。
选中Screen Shot Collection View (Bind). 打开Bindings Inspector,展开Content组下面的Content绑定
绑定到Search Results Controller, Controller Key为selection,Model Key Path为screenShots。
screenShots一开始是空的,loadScreenShots()方法下载了图片文件并向数组中填充NSImage对象。
在ViewController.swift中,在tableViewSelectionDidChange(_:)中, result.loadIcon():之后添加下面的代码:
result.loadScreenShots()
这将会获取屏幕截图的图片并且建立相应数量的视图。
下一个步骤,把图集视图中的子视图的原型设置正确。虽然storyboard中图集显示了子视图,但是这是没有连接的,需要用代码手动设置。
在viewDidLoad()后面添加下面的代码:
let itemPrototype = self.storyboard?.instantiateController(withIdentifier:"collectionViewItem") as! NSCollectionViewItem
collectionView.itemPrototype = itemPrototype
现在图集视图知道如何通过原型来创建每个项目了,我们需要通过绑定告诉它每个图片的内容。
打开 Main.storyboard 并点击 Screen Shot Image View (Bind),在Collection View Item Scene下面.
绑定 Value 项到 Collection View Item. Controller key 为空,Model Key Path为representedObject。
representedObject属性是代表了图集视图数组中的一个子对象,本例中,是NSImage对象。
运行,可以看到截屏出现了:
干得好,在总结之前Cocoa绑定还有几个特性要讲一下。
绑定其他属性
你的用户界面可以增加一点用户的反馈。用户不习惯在程序进行加载操作时,界面是静止的,他们会怀疑是否当机了。
所以我们要在调用图片时增加一个旋转进度标记来代替静止的屏幕。
设置旋转进度标记
绑定旋转进度标记很简单,在ViewController中添加如下属性:
dynamic var loading = false
加载成功需要两点:dynamic修饰和NSObject的继承。绑定是基于KVO的,Swift的类如果不继承NSObject的话,就无法使用KVO。
在searchClicked(:)中,在调用getSearchResults(:results:langString:completionHandler:)之前,添加:
loading = true
在设定searchResultsController的content属性之后,添加:
self.loading = false
接着,打开Main.storyboard,选中Search Progress Indicator (Bind). 你将要绑定旋转进度指示器的两个属性:hidden和animate.
首先,展开Hidden组,绑定到 View Controller. Controller Key为空,Model Key Path为self.loading。
在这个情况下,你希望hidden是false的时候loading是true,反之亦然。我们可以用NSValueTransFormer来反转布林值。
NSValueTransformer是一个帮助你在控件和模型之间转换格式或者数据的类。
你可以继承这个类来做一些复杂的转换,你可以在这篇教程里获得更多关于NSValueTransformers的内容:How to Use Cocoa Bindings and Core Data in a Mac App.
在Value Transformer的下拉清单中,选择NSNegateBoolean。
绑定Animate值到 View Controller。设定Controller Key为空,并且设定Model Key Path为self.loading。
这个布林值不需要反转,绑定如下:
运行,搜索点东西,会返回大量数据,可以看到旋转进度指示器了。
增加一些细节
Cocoa绑定还可以有更多的功能:你可以绑定标签的颜色和字体,控件的可否激活,还可以根据它们的状态给标签设置不同的值。
运行,你会发现在没有选中项时,App看上去不太好。
找到Price Label (Bind),展开Value。 在No Selection Placeholder, 输入“–”:
运行,可以看到Price标签有一个很好的占位符了。
设置所有的标签为No Selectionto。
注:如果希望标签为空,可以在No Selection Placeholder输入空格。
总结
这是MacOS Cocoa绑定的基础内容,你可以非常容易的绑定数据和控件。
通过本教程,你学会了:
- 如何用IB快速绑定控件和数据。
- 如何在用户点击列表时,保持模型和视图的同步。
- 如何使用方法和绑定协同工作来控制控件和组织数据。
- 如何快速的创建旋转进度器这样的界面特征。
你可以在这里下载最终的项目文件。
每个绑定都有很多的设置和选项,本教程没有全部涉及。请翻阅Apple提供的关于Cocoa绑定的资料。获取更多信息。