本文5500
字,大约阅读 15
分钟。 6
月底会停止在 简书
更新,最新的博客地址
关于 NSTableView的使用
解决 NSTableView的 Header在 Xib无法正常显示
设置 NSTableView的 Header背景颜色。
NSView如何 sizeThatFits:
再次激活 App
请求 Jekyll的 Post文章的列表。
关于泛型参数
关于 @escaping
精简请求子类
cannot override with a stored property
请求文章列表
@IBOutlet Property cannot have non-‘@objc’ class type
中文系统格式化时间显示英文字符
给 NSView添加 NSGestureRecognizer时间
删除文章
关于 NSAlert
OSX平台代码打开一个地址
deinit方法
关于 NSTableView
的使用
接下来我们需要就是做出这个列表数据,我们可以使用 NSTableView
来做出这个效果。
我们拖拽一个 NSTableView
放在 BaseListView.xib
的试图上面。
设置布局如下。
C99F8039-E33D-47D0-98EA-459D0AF3E336解决 NSTableView
的 Header
在 Xib
无法正常显示
有的时候我们发现 NSTableView
在 Xib
被隐藏了,但是我们显示 Header
的选项是开启的。
2A43EA90-E3FB-400F-A286-11F990C01A24我们只要重新勾选
Hader
选项即可显示出来。
我们可以看出来我们的列表分为三部分 标题
时间
操作
,我们就设置 NSTableView
有 3
个 Column
。
因为名字的长度是不固定的,我们就设置 NSTableView
的第一个 Column
的宽度随着 NSTableView
的宽度变化。
我们设置其余的 Column
的宽度固定为 100
。
我们的基本结构已经出现了,现在我们要设置 Header
的背景颜色为黑色。
我们关联一下 Xib
上面的 NSTableView
控件。
设置 NSTableView
的 Header
背景颜色。
参考资料:
1F191849-C907-46C8-B64C-E96BB867D78F⛔️这里遇到了一个棘手的问题,如果使用
NSTableHeaderView
的子类,在Draw
绘制虽然颜色是设置了,但是标题已经被覆盖掉了。如果我们使用下面的方法进行设置的话
public func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { if let headerCell = tableColumn?.headerCell { headerCell.drawsBackground = true headerCell.backgroundColor = NSColor.black } return nil }
如果数据源为0就无法设置,并且还有下面的问题。
如果就算有数据也是这样的状态。
76FE2C1F-AA2D-4335-AB34-B0236959D216中间有间隙并没有完全的黑掉。
我们暂时没有找到合适设置背景颜色的方案,我们暂时使用系统自带的。
展示列表分为三种样式。
- 第一种是图标加上文字并且是可以点击的
- 第二种是文字只做展示
- 第三种是两个按钮
我们设置 NSTableView
的 Cell
的高度为 83
。
我们新建一个类 IconTitleTableCellView
继承与 NSTableCellView
。我们在 IconTitleTableCellView.xib
上面拖拽一个 NSView
继承于 SideMenuItemView
。
布局如下。
E0C1A309-B229-41B0-990B-E4ECED5D0988 9E630761-F95A-4122-BBA0-6196F851AEA1 FC3C5113-C10B-42FD-8554-AF842A147602我们先暂时设置宽度为 100
,因为标题不知道长度,所以我们需要动态改变长度。
为了设置默认的字体颜色,我们设置normalColor
为 var
的变量。
NSView
如何 sizeThatFits:
为了让标题显示完全,我们绑定一下设定宽度的约束。
@IBOutlet weak var itemViewWidthConstraint: NSLayoutConstraint!
我们发现 sizeThatFits
并不是 NSView
只有 NSControl
或者子类才可以使用。但是对于我们的需求已经够了。
我们给 SideMenuItemView
写一个 sizeThatFits
方法。
func sizeThatFits(_ size: NSSize) -> NSSize {
let labelSize = self.itemTitle.sizeThatFits(size)
let sizeWidth = size.height + 10 + labelSize.width + 10
return NSSize(width: sizeWidth, height: size.height)
}
我们通过计算出 SideMenuItemView
的宽度。
func configurationView() {
let configuration = SideMenuItemConfiguration(title: "这是测试标题", iconHex: "F0F6", hidden: true, selected: false, normalColor: NSColor(red:0.267, green:0.267, blue:0.267, alpha:1.000))
self.itemView.menuItemConfiguration = configuration
let size = self.itemView.sizeThatFits(NSSize(width: Int.max, height: 20))
self.itemViewWidthConstraint.constant = CGFloat(size.width)
}
此时我们已经正常可以显示标题了。
F1BB28D0-A970-4CC5-BCDA-FD302A0A1E21再次激活 App
我们现在的 App
运行,假设一个应用遮挡着我们的应用,我们点击 App
图标是无法再次显示出来 App
面板的。
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
for window in sender.windows {
window.makeKeyAndOrderFront(self)
}
return true
}
}
13
此时我们已经可以再次点击 App
图标让界面显示最前面了。
我们再创建一个 DateTableCellView
继承与 NSTableCellView
。
我们拖拽一个 Label
到 DateTableCellView.xib
布局设置如下。
我们让 cloumn
第二个使用 DateTableCellView
。
我们新建一个类 ActionTableCellView
继承于 NSTableCellView
。
我们在 ActionTableCellView.xib
上面拖拽一个 NSView
继承与 SideMenuItemView
。布局设置如下:
我们再在右边放置一个按钮,布局如下。
B2AE1F40-E784-4A65-AAE9-EAC2DA474681 25950EC1-D246-42F1-844C-0D21B62A24A5 AF0179FB-FFBE-4C2F-9FAE-546C5C017044我们 Column
第三个为 ActionTableCellView
。
我们设置按钮的 Cloumn
的宽度为 200
。
显示效果似乎还是不足,原因是 80的宽度不足以正常的显示出来。
设置 ActionTableCellView
中按钮的宽度都为 100
。
我们给 DateTableCellView
连接 label
的属性用于设置时间。
@IBOutlet weak var dateLabel: NSTextField!
我们分别给 ActionTableCellView
两个自定义控件设置圆角和背景颜色。
@IBOutlet weak var deleteItemView: SideMenuItemView!
@IBOutlet weak var lookItemView: SideMenuItemView!
02DC506D-3B0A-49A7-BAF1-4AC5ED6137DC
我们的列表的样式已经基本上搭建完毕了。
请求 Jekyll
的 Post
文章的列表。
我们新建一个 GetPostListApi
类用于获取文章页列表。
我们新建一个类 PostDetail
用于显示文章的信息详情。
class PostDetail: Mappable {
var path:String?
var url:String?
var id:String?
var collection:String?
var relativePath:String?
var draft:Bool = false
var categories:[String] = []
var title:String?
var date:String?
var slug:String?
var ext:String?
var tags:[String] = []
var layout:String?
var httpURL:String?
var apiURL:String?
var name:String?
required init?(map: Map) {
}
func mapping(map: Map) {
path <- map["path"]
url <- map["url"]
id <- map["id"]
collection <- map["collection"]
relativePath <- map["relative_path"]
draft <- map["draft"]
categories <- map["categories"]
title <- map["title"]
date <- map["date"]
slug <- map["slug"]
ext <- map["ext"]
tags <- map["tags"]
layout <- map["layout"]
httpURL <- map["http_url"]
apiURL <- map["api_url"]
name <- map["name"]
}
}
我没有找到 ObjectMapper直接转成 模型数组的,应该需要自己单独封装添加数组里面,但是却无意发现了这个。
30C53E57-9AE4-43C3-B8E3-E29F9349F8A9官方建议我们使用 AlamofireObjectMapper
这个库,看了文档确实比较简单,我们就用这个库替换掉 Alamofire
和 ObjectMapper
。
class GetPostListApi {
func loadRequest(success:GetPostListApiSuccessCompletionHandle?, failure:GetPostListApiFailureCompletionHandle?) {
let URL = "http://localhost:4000/_api/collections/posts/entries"
Alamofire.request(URL).responseArray { (response:DataResponse<[PostDetail]>) in
if let list = response.value {
self.completionHandle(success: success, failure: nil, postList: list, error: nil)
} else {
self.completionHandle(success: nil, failure: failure, postList: nil, error: response.error)
}
}
}
func completionHandle(success:GetPostListApiSuccessCompletionHandle?, failure:GetPostListApiFailureCompletionHandle?, postList:[PostDetail]?, error:Error?) {
if let success = success , let postList = postList {
success(postList)
} else if let failure = failure {
failure(error)
}
}
}
写到这里,我们会发现 GetPostListApi
这个类和 GetConfigurationApi
有太多的相似代码。我们不妨创建一个 BaseRequestApi
的请求子类去掉一些多余的代码。
我们现在请求的地址是基于 http://localhost:4000/_api/
这个地址,大部分的 Jekyll
本地都是 4000
端口也可能是其他的。
我们就在 BaseRequestApi
定义一个 URL
的变量默认为 http://localhost:4000/_api/
。
为了能够请求到数据,我们创建一个发起请求的方法。
我们发起请求需要完整的请求地址我们新建一个方法传递 http://localhost:4000/_api/
的后缀。
func URLPath() -> String {
return ""
}
我们新建一个方法用于拼接完整的请求地址。
func URLFullPath() -> String {
guard self.URLPath().characters.count > 0 else {
return self.URL
}
return "\(self.URL)/\(self.URLPath())"
}
当后缀是空字符串的时候我们不拼接。
关于泛型参数
对于 泛型参数
在 OC
和 Swift
一直没有明白过来,也一直掌握精髓,到现在都不会用。
现在要封装请求,对于代理回调应该需要用上 泛型参数
,研究一下。
参考资料:
我们获取数据主要分为两种,一种是对象类型,一种是数组对象类型。
我们新建一个请求协议。
protocol BaseRequestProtocol {
associatedtype R:BaseMappable
func loadObjectRequest(success:BaseRequestResponseObjectCompletionHandle<R>, failure:BaseRequestFailureCompletionHandle)
func loadArrayRequest(success:BaseRequestResponseArrayCompletionHandle<R>, failure:BaseRequestFailureCompletionHandle)
}
typealias BaseRequestResponseObjectCompletionHandle<T:BaseMappable> = (_ model:T) -> Void
typealias BaseRequestResponseArrayCompletionHandle<T:BaseMappable> = (_ models:[T]) -> Void
typealias BaseRequestFailureCompletionHandle = (_ error:Error) -> Void
我们让请求的基类 BaseRequestApi
实现 BaseRequestProtocol
的协议。
class BaseRequestApi<T:BaseMappable>: BaseRequestProtocol
我们实现一下 BaseRequestProtocol
的方法。
func loadObjectRequest(success: @escaping (T) -> Void, failure: @escaping (Error?) -> Void) {
Alamofire.request(self.URLFullPath()).responseObject { (response:DataResponse<R>) in
guard let value = response.value else {
failure(response.error)
}
success(value)
}
}
我们返回确保返回的对象存在,当不存在就返回错误信息。
public var error: Error? { return result.error }
因为 error
可能不存在,我们就回调 BaseRequestFailureCompletionHandle
设置可选型。
关于 @escaping
我们在网络请求完成之后进行回调编译器会提示我们加上 @escaping
。关于 @escaping
我们可以参考下面资料。
参考资料: swift3.0中@escaping 和 @noescape 的含义。
看过资料我们可以知道,系统默认是 @noescape
。只要被 @noescape
标记的 闭包
我们都是不需要关心内存管理的。
但是如果在方法执行完毕才执行 闭包
我们就需要用 @escaping
标识,这样系统自动在调用时候提示用户对于直接使用 self
进行内存管理。
func loadArrayRequest(success: @escaping ([T]) -> Void, failure: @escaping BaseRequestFailureCompletionHandle) {
Alamofire.request(self.URLFullPath()).responseArray { (response:DataResponse<[R]>) in
guard let value = response.value else {
failure(response.error)
return
}
success(value)
}
}
func loadObjectRequest(success: @escaping (T) -> Void, failure: @escaping (Error?) -> Void) {
Alamofire.request(self.URLFullPath()).responseObject { (response:DataResponse<R>) in
guard let value = response.value else {
failure(response.error)
return
}
success(value)
}
}
我们现在的请求基类基本上已经可以正常的运行了,我们已经迫不及待的准备尝试一下。
精简请求子类
我们设置 GetConfigurationApi
父类为 BaseRequestApi
。
class GetConfigurationApi: BaseRequestApi<JekyllConfiguration> {
override func URLPath() -> String {
return "configuration"
}
}
我们此时子类的代码就变成这么的简单。但是现在有一个问题就是我们配置的数据在子数据里面。
我们需要使用 Path
进行获取,我们就为 BaseRequestApi
设置一个属性可以让外接设置 Path
。
var responseKeyPath:String?
class GetConfigurationApi: BaseRequestApi<JekyllConfiguration> {
override func URLPath() -> String {
return "configuration"
}
var responseKeyPath: String? = "content"
}
此时我们会受到编译器通知我们的错误。
795C05DA-D33F-4B3E-96E1-7661DE196969cannot override with a stored property
参考资料:
override var responseKeyPath: String? {
get {
return "content"
}
set {
self.responseKeyPath = newValue
}
}
我们此时在 ViewController
的请求代码可以设置如下。
let getConfigurationApi = GetConfigurationApi()
getConfigurationApi.loadObjectRequest(success: { [weak self] (configuration) in
guard let title = configuration.title else {
return
}
self?.navigationBar.blogMenuItem.itemTitle.stringValue = title
}, failure: { (error) in })
我们就可以请求到数据了,是不是代码更加的简洁了呢?
请求文章列表
我们配置 GetPostListApi
类的代码如下。
class GetPostListApi: BaseRequestApi<PostDetail> {
override func URLPath() -> String {
return "collections/posts/entries"
}
}
我们在 PostsView
新写一个方法用于获取文章列表。
func loadData() {
let api = GetPostListApi()
api.loadArrayRequest(success: { (list:[PostDetail]) in
}) { (error) in }
}
有了数据我们需要在列表里面展示出来。
BaseListView
作为列表的基类,我们的数据源的结构可能不太一样,我们不可能让我们自定义的数据源传入 BaseListView
。
这个时候我们的 泛型参数
又可以登场了。
我们给 BaseListView
新建一个泛型参数,必须是 BaseMappable
的子类。
class BaseListView<M:BaseMappable>
我们新建一个属性存储 M
数组,当用户重新设置就刷新表格。
var models:[M] = [] {
didSet {
self.tableView.reloadData()
}
}
@IBOutlet Property cannot have non-‘@objc’ class type
此时我们已经收到了一个错误信息。
参考资料:
查了很多的资料,这个技术难点倒是没有找到合适的方法解决。是因为 @IBOutlet
在 OC
里面使用的运行时,但是运行时不允许 @IBOutlet
绑定一个泛型的对象。
我还尝试过在 BaseListView
使用其他的泛型类间接代理,但是依然无法解决我们的问题。
我现在唯一能够想到的方案就是所谓的协议,用协议声明泛型参数。
我们希望别人继承我们的协议可以把数据转换成我们想要的数据。
protocol BaseListViewDataSource {
associatedtype M:BaseMappable ///< 泛型类型
static func converModels(models:[M]) -> [BaseListViewDataModel] ///< 将其他类型对象数组转换成BaseListViewDataModel对象数组
static func converModel(model:M) -> BaseListViewDataModel ///< 将其他类型转换成BaseListViewDataModel对象
}
extension BaseListViewDataSource {
static func converModels(models:[M]) -> [BaseListViewDataModel] {
var datas:[BaseListViewDataModel] = []
for model in models {
let data = self.converModel(model: model)
datas.append(data)
}
return datas
}
}
class BaseListViewDataModel {
var title:String? ///< 显示标题
var date:String? ///< 显示时间
}
我们 PostDetail
实现我们刚才的协议 BaseListViewDataSource
。
static func converModel(model: PostDetail) -> BaseListViewDataModel {
let data = BaseListViewDataModel()
data.title = model.title
data.date = model.date
return data
}
typealias M = PostDetail
我们在 loadData
方法实现我们刚才的方法。
func loadData() {
let api = GetPostListApi()
api.loadArrayRequest(success: { (list:[PostDetail]) in
self.listView.models = PostDetail.converModels(models: list)
}) { (error) in }
}
36426B52-B1F6-4C1F-BE2E-77806B868657
我们已经可以发现我们的界面已经可以正常的显示我们数据条数,现在剩下做的就是给我们界面正确的赋值了。
public func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
guard let identifier = tableColumn?.identifier else {
return nil
}
let model = self.models[row]
let view = tableView.make(withIdentifier: identifier, owner: self)
if let iconTitle = view as? IconTitleTableCellView, let title = model.title {
}
return view
}
我们将 IconTitleTableCellView
中 configurationView
方法修改如下。
func configurationView(title:String) {
let configuration = SideMenuItemConfiguration(title: title, iconHex: "F0F6", hidden: true, selected: false, normalColor: NSColor(red:0.267, green:0.267, blue:0.267, alpha:1.000))
self.itemView.menuItemConfiguration = configuration
let size = self.itemView.sizeThatFits(NSSize(width: Int.max, height: 20))
self.itemViewWidthConstraint.constant = CGFloat(size.width)
}
public func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
guard let identifier = tableColumn?.identifier else {
return nil
}
let model = self.models[row]
let view = tableView.make(withIdentifier: identifier, owner: self)
if let iconTitle = view as? IconTitleTableCellView, let title = model.title {
iconTitle.configurationView(title: title)
}
return view
}
E7D6285D-3D63-4C92-9BFF-B8DE754E48C3
我们的界面就可以正常的显示标题了。同样我们我们赋值一下时间。
if let dateView = view as? DateTableCellView, let date = model.date {
dateView.dateLabel.stringValue = date
}
97394D1F-3DFC-4939-9111-283A3A18A7CB
我们发现时间显示的格式不正确。我们给 DateTableCellView
写一个转换时间格式的方法。
func configuration(dateString:String) {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd hh:mm:ss zzzz"
guard let date = formatter.date(from: dateString) else {
return
}
formatter.dateFormat = "MMM dd,yyyy"
self.dateLabel.stringValue = formatter.string(from: date)
}
2D3D00B6-EC09-4A1B-9FFA-57298DBCDDBD
我们看到显示竟然是中文六月
,不是我们希望看到的 Jun
。
中文系统格式化时间显示英文字符
formatter.locale = Locale(identifier: "en_US")
我们还是按照默认的比较好,我们中文用起来比较方便。
现在要做的就是 删除
查看
两个方法了。我们封装的 SideMenuItemView
控件是无法响应我们的事件的。
给 NSView
添加 NSGestureRecognizer
时间
参考资料:
15D2ADAE-B1B7-495C-AA6A-E7C9677CFE0D一共有五个 NSGestureRecognizer
的子类可以使用。我们使用 NSClickGestureRecognizer
来处理点击。
func addClick() {
let click = NSClickGestureRecognizer(target: self, action:#selector(self.clickAction))
self.addGestureRecognizer(click)
}
func clickAction() {
}
我们的方法无法告诉外接什么时候点击了,如果有一个回调就好了。
typealias SideMenuItemViewClickCompletionHandle = (_ view:SideMenuItemView) -> Void
func addClick(completionHandle:@escaping SideMenuItemViewClickCompletionHandle) {
self.clickCompletionHandle = completionHandle
let click = NSClickGestureRecognizer(target: self, action:#selector(self.clickAction))
self.addGestureRecognizer(click)
}
func clickAction() {
guard let completionHandle = self.clickCompletionHandle else {
return
}
completionHandle(self)
}
var clickCompletionHandle:SideMenuItemViewClickCompletionHandle?
删除文章
参考资料:
我们新建一个类 DeletePostDetail
继承与我们 BaseRequestApi
。
class DeletePostDetail: BaseRequestApi<DeletePostDetailResponse> {
override func URLPath() -> String {
return "collections/posts/{name}"
}
}
class DeletePostDetailResponse: BaseMappable {
func mapping(map: Map) {
}
}
这样是不符合我们请求的标准的,我们的地址需要一个真实的 name
。
我们就给 DeletePostDetail
初始化带一个 name
的参数。
override func URLPath() -> String {
return "collections/posts/\(self.name)"
}
let name:String
init(name:String) {
self.name = name
}
我们删除的请求是 delete
请求,我们底层封装的默认为 Get
请求,我们还需要稍微的修改一下。
func requestMethod() -> HTTPMethod {
return HTTPMethod.get
}
Alamofire.request(self.URLFullPath(), method:self.requestMethod())
这样我们父类默认是 Get
请求,子类如果需要 delete
请求,我们只需要重写这个方法即可。
我们需要点击删除的按钮提示用户是否要删除这个文章,所以我们需要传入一个文章的文件名称。
///BaseListViewDataModel类
var fileName:String? ///< Markdown 的文件名称
///PostDetail类
static func converModel(model: PostDetail) -> BaseListViewDataModel {
let data = BaseListViewDataModel()
data.title = model.title
data.date = model.date
data.fileName = model.name
return data
}
///ActionTableCellView
var fileName:String? ///< 用来知道要删除那个文件
关于 NSAlert
对于弹出框我们可以使用 NSAlert
控件
参考资料:
在
ActionTableCellView
类增加代码如下
self.deleteItemView.addClick { (view) in
guard let fileName = self.fileName, let window = NSApplication.shared().keyWindow else {
return
}
let alert = NSAlert()
alert.messageText = "确定要删除\(fileName)"
alert.beginSheetModal(for: window, completionHandler: { (response) in
})
}
2C2B5648-FEF8-4467-B32B-121AE71C5ABF在
BaseListView
的public func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView?
方法 增加代码如下if let actionView = view as? ActionTableCellView { actionView.fileName = model.fileName }
此时只有一个确定,没有取消按钮,到时候误删就 GG 了。
self.deleteItemView.addClick { (view) in
guard let fileName = self.fileName, let window = NSApplication.shared().keyWindow else {
return
}
let alert = NSAlert()
alert.messageText = "确定要删除\(fileName)"
alert.addButton(withTitle: "删除")
alert.addButton(withTitle: "取消")
alert.beginSheetModal(for: window, completionHandler: { (response) in
})
}
当我们点击删除按钮我们需要执行删除的请求。
if response == NSAlertFirstButtonReturn {
self.deletePost(fileName: fileName)
}
func deletePost(fileName:String) {
let api = DeletePostDetail(name: fileName)
api.loadObjectRequest(success: { (response) in
}) { (error) in
}
}
当我们删除完毕我们需要刷新我们的表格,我就给 ActionTableCellView
新写一个回调用于删除完毕更新表格的内容。
typealias ActionTableCellViewDeleteSuccessCompletionHandle = (_ view:ActionTableCellView) -> Void
var deleteSuccessCompletionHandle:ActionTableCellViewDeleteSuccessCompletionHandle?
func deletePost(fileName:String) {
let api = DeletePostDetail(name: fileName)
api.loadObjectRequest(success: { (response) in
guard let completionHandle = self.deleteSuccessCompletionHandle else {
return
}
completionHandle(self)
}) { (error) in
}
}
我们发现我们的表格并没有刷新,因为对于 Delete
请求是没有任何信息回调的。我们只用知道状态吗是200
就可以知道成功了。
func loadObjectRequest(success: @escaping (T?) -> Void, failure: @escaping (Error?) -> Void) {
Alamofire.request(self.URLFullPath(),method:self.requestMethod()).responseObject(keyPath:self.responseKeyPath) { (response:DataResponse<R>) in
guard let code = response.response?.statusCode, code == 200 else {
failure(response.error)
return
}
success(response.value)
}
}
当我们当识别状态吗为 200
果然成功了。
OSX
平台代码打开一个地址
我们做完 删除
功能,还剩下一个 查看
功能,当用户点击 查看
按钮。
我们给 ActionTableCellView
新增一个方法用于配置 查看
按钮的点击方法。
func addLookView() {
self.lookItemView.addClick { (view) in
guard let urlString = self.httpURL, let url = URL(string: urlString) else {
return
}
NSWorkspace.shared().open(url)
}
}
界面上面的搜索功能,说简单不简单,说复杂不复杂。那要你需要实现的搜索到什么程度。
参考资料:
我们做先做一个简单版本的,就直接匹配就好了。
我们给 BaseListView
增加一个搜索过滤之后的数组。
private var filterModels:[BaseListViewDataModel] = []
我们用 filterModels
来作为我们暂时数据的数据源。
我们给 ContentHeaderValue1
关联一下搜索输入框。
@IBOutlet weak var searchFiled: NSTextField!
我们设置一下 searchFiled
代理对象为 BaseListView
。
@IBOutlet weak var header: ContentHeader! {
didSet {
guard let headerValue1 = self.header.headerContent as? ContentHeaderValue1 else {
return
}
headerValue1.searchFiled.delegate = self
}
}
经过研究如果要监听输入框文字变化需要用通知。我们声明一个方法监听输入框通知变化。
func searchFiledTextChanged(notification:Notification) {
guard let filed = notification.object as? NSTextField else {
return
}
guard let headerValue1 = self.header.headerContent as? ContentHeaderValue1 else {
return
}
guard filed == headerValue1.searchFiled else {
return
}
}
我们新建一个方法处理字符串改变过滤数据源。
func filterDataModels(filter:String) {
self.filterModels.removeAll()
if filter.characters.count == 0 {
self.filterModels.append(contentsOf: self.models)
} else {
for model in self.models {
if let _ = model.title?.range(of: filter) {
self.filterModels.append(model)
}
}
}
self.tableView.reloadData()
}
我们在 searchFiledTextChanged
方法里面调用我们刚才的过滤的方法。
func searchFiledTextChanged(notification:Notification) {
guard let filed = notification.object as? NSTextField else {
return
}
guard let headerValue1 = self.header.headerContent as? ContentHeaderValue1 else {
return
}
guard filed == headerValue1.searchFiled else {
return
}
self.filterDataModels(filter: filed.stringValue)
}
因为我们初始化的时候,我们还没有输入任何的搜索字符串,设置 models
我们要初始化我们的 filterDataModels
数组。
我们新建一个方法用于初始化 filterDataModels
。
func settingFilterModels() {
guard let headerValue1 = self.header.headerContent as? ContentHeaderValue1 else {
return
}
let filterText = headerValue1.searchFiled.stringValue
self.filterDataModels(filter: filterText)
}
我们在设置 models
时候进行重新设置 filterModels
。
我们在 header
的方法 didSet
进行注册通知。
我们的搜索功能已经可以用了。
deinit
方法
我们在 Objective-C
开发里面经常在 dealloc
注销通知,减少资源消耗。我们在 Swift
里面可以使用 deinit
函数。
参考资料:
deinit {
NotificationCenter.default.removeObserver(self)
}
刚才无意间发现下面系统自带的方法
extension NSObject {
open func controlTextDidBeginEditing(_ obj: Notification)
open func controlTextDidEndEditing(_ obj: Notification)
open func controlTextDidChange(_ obj: Notification)
}
这是 NSObject
的扩展,我们去掉我们注册的通知,用 controlTextDidChange
方法试一下。
参考资料:
网友评论