美文网首页君赏博客
Jekyll-Admin-Mac-列表

Jekyll-Admin-Mac-列表

作者: 君赏 | 来源:发表于2017-06-21 09:19 被阅读105次

    本文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方法

    5C515420-600F-480D-B9FD-6F40380DA47B

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

    参考资料:

    相关文章

      网友评论

      • z小志:请问有没有源码啊。最近在做mac 开发 这方面的资料太少了,谢谢啦
        君赏:@瞬间看见永远 干嘛要像iOS如果想像可以用TWUI
        瞬间看见永远:NSTableView [[column headerCell] setStringValue:怎么隐藏这个 headerCell 就是看着像ios的tableview
        君赏:这是源码地址[https://github.com/joserccblog/Jekyll-Admin-Mac](https://github.com/joserccblog/Jekyll-Admin-Mac) 我也是一边开发一边学习

      本文标题:Jekyll-Admin-Mac-列表

      本文链接:https://www.haomeiwen.com/subject/oejzqxtx.html