Jekyll-Admin-Mac-列表

讲述了 NSTableView基本用法,泛型参数,关于 NSAlert等知识碎片

Imagem de capa

[TOC]

关于 NSTableView的使用

接下来我们需要就是做出这个列表数据,我们可以使用 NSTableView来做出这个效果。

我们拖拽一个 NSTableView放在 BaseListView.xib的试图上面。

DFDA8143-EEE0-43A3-A863-CFCDDCF41B14

设置布局如下。

C99F8039-E33D-47D0-98EA-459D0AF3E336

解决 NSTableViewHeaderXib无法正常显示

有的时候我们发现 NSTableViewXib被隐藏了,但是我们显示 Header的选项是开启的。

我们只要重新勾选 Hader选项即可显示出来。

2A43EA90-E3FB-400F-A286-11F990C01A24

我们可以看出来我们的列表分为三部分 标题 时间 操作,我们就设置 NSTableView3Column

6E7EFC46-9753-4F4A-B497-1CE2C5FCBCD2

因为名字的长度是不固定的,我们就设置 NSTableView的第一个 Column的宽度随着 NSTableView的宽度变化。

69975981-2F29-4E1B-BCD8-165C1D033148

我们设置其余的 Column的宽度固定为 100

FE8BFBD3-51D7-42C9-92F1-749290356794

1804A265-0CAF-40DE-8F1B-7711A4F3E340

我们的基本结构已经出现了,现在我们要设置 Header的背景颜色为黑色。

我们关联一下 Xib上面的 NSTableView控件。

设置 NSTableViewHeader背景颜色。

参考资料:

⛔️这里遇到了一个棘手的问题,如果使用 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

中间有间隙并没有完全的黑掉。

我们暂时没有找到合适设置背景颜色的方案,我们暂时使用系统自带的。

1F191849-C907-46C8-B64C-E96BB867D78F

展示列表分为三种样式。

我们设置 NSTableViewCell的高度为 83

我们新建一个类 IconTitleTableCellView继承与 NSTableCellView。我们在 IconTitleTableCellView.xib上面拖拽一个 NSView继承于 SideMenuItemView

布局如下。

E0C1A309-B229-41B0-990B-E4ECED5D0988

9E630761-F95A-4122-BBA0-6196F851AEA1

FC3C5113-C10B-42FD-8554-AF842A147602

我们先暂时设置宽度为 100,因为标题不知道长度,所以我们需要动态改变长度。

为了设置默认的字体颜色,我们设置normalColorvar的变量。

30CFC0BB-A3E7-4599-A0D5-02F9B82C36BF

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

我们拖拽一个 LabelDateTableCellView.xib布局设置如下。

D82D8855-6B02-4B37-A27A-FB329FE29B4E

我们让 cloumn第二个使用 DateTableCellView

我们新建一个类 ActionTableCellView继承于 NSTableCellView

我们在 ActionTableCellView.xib上面拖拽一个 NSView继承与 SideMenuItemView。布局设置如下:

8872C43C-B65D-42A2-A6F5-220CE643BECE

22DC95EE-D8B2-405F-819A-5158E4D0E592

1CF68D9F-647B-4F5A-94F0-11794CEE3002

我们再在右边放置一个按钮,布局如下。

B2AE1F40-E784-4A65-AAE9-EAC2DA474681

25950EC1-D246-42F1-844C-0D21B62A24A5

AF0179FB-FFBE-4C2F-9FAE-546C5C017044

我们 Column第三个为 ActionTableCellView

676001A0-FB2F-4A91-9AD6-57B5120C0F9C

我们设置按钮的 Cloumn的宽度为 200

5353EFD4-E07D-4053-BADB-68D761AB8A43

显示效果似乎还是不足,原因是 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

我们的列表的样式已经基本上搭建完毕了。

请求 JekyllPost文章的列表。

获取 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这个库,看了文档确实比较简单,我们就用这个库替换掉 AlamofireObjectMapper

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())"
}

当后缀是空字符串的时候我们不拼接。

关于泛型参数

对于 泛型参数OCSwift一直没有明白过来,也一直掌握精髓,到现在都不会用。

现在要封装请求,对于代理回调应该需要用上 泛型参数,研究一下。

参考资料:

我们获取数据主要分为两种,一种是对象类型,一种是数组对象类型。

我们新建一个请求协议。

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-7661DE196969

cannot 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

此时我们已经收到了一个错误信息。

参考资料:

查了很多的资料,这个技术难点倒是没有找到合适的方法解决。是因为 @IBOutletOC里面使用的运行时,但是运行时不允许 @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
}

我们将 IconTitleTableCellViewconfigurationView方法修改如下。

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

    })
}

BaseListViewpublic func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? 方法 增加代码如下

if let actionView = view as? ActionTableCellView {
    actionView.fileName = model.fileName
}

2C2B5648-FEF8-4467-B32B-121AE71C5ABF

此时只有一个确定,没有取消按钮,到时候误删就 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进行注册通知。

201706201728

我们的搜索功能已经可以用了。

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方法试一下。

参考资料: