[TOC]

✅为什么要开发 Jekyll-Admin-Mac?

因为接触到使用 Jekyll构建博客十分的方便,但是 Jekyll-Admin里面的功能又差强人意。

如果修改 Jekyll-Admin里面的源码代价是巨大的,不如用自己擅长的语言来写,正好还有自动生成的 API 可以用。

对于 Jekyll-Admin-MacUI我们采用网页的配色即可。

获取 Jekyll-Admin的图标。

经过网络抓包,我们抓取到 Jekyll-Admin的图标是经过连接

../admin/847c038a8202754b465604459e16715d.png来获取的。

我们直接保存到本地,在工程里面使用。

我们新建一个 Mac的工程保存到本地名字叫做- Jekyll-Admin-Mac

我们打开终端 terminal.app

cd /Users/用户名称/Downloads
curl -o jekyll-admin-logo.png ../admin/847c038a8202754b465604459e16715d.png

⚠️这里我们用到了 curl命令,更多的想知道 curl命令可以去谷歌和百度。

我们拖拽文件 jekyll-admin-logo.png到工程 Assets.xcassets

左边功能菜单我们设置宽度为 205

我们新建一个 SideMenuView继承 NSView

现在 NSView创建的时候不允许使用 XIB,我们自己新建一个 Xib

名字叫做 SideMenuView.xib

我们设置 SideMenuView的大小为 205x1000。宽度是固定的,但是高度不固定,我们使用自动布局。

最上线显示 Logo的地方大小为 205x75。我们采用 NSImageView。我们采用如下的布局。

  • 左侧和父试图对其
  • 上侧和父试图对其
  • 宽度205
  • 高度75

⚠️我们发现我们的图片是正常的显示出来了,但是背景颜色无法显示。那是因为在 OSX开发和 iOS不太一样。对于正常的 NSView, NSImageView是无法进行设置背景颜色的。

@IBDesignable和@IBInspectable

为了可以自定义背景颜色,我们创建一个继承 NSView的子类 BaseView

@IBDesignable class BaseView: NSView {
}

我们在 BaseView新增一个属性。

@IBInspectable  var backgroundColor:NSColor! = NSColor.white {
    didSet {
        self.needsToDraw(self.bounds)
    }
}

自定义draw()

我们在 func draw(_ dirtyRect: NSRect)方法里面进行填充颜色。

override func draw(_ dirtyRect: NSRect) {
  super.draw(dirtyRect)
  self.backgroundColor.setFill()
  NSRectFill(dirtyRect)
}

关于怎么在 XIB及时预览界面可以参考下面的连接。

在Xcode6中使用IBDesignable创建自定义控件(翻译)

关于如何 NSView自定义背景颜色参考下面的连接

我们设置 NSView为继承与 BaseView 背景颜色试图。我们设置背景颜色为 rgb343434

布局参考之前 NSImageView的布局。

我们把刚才的 NSImageView作为子试图,布局设置下面。

我们拖拽 NSView一个新的试图放置在 Main.storyboard-ViewController-View上面。

我们设置刚才新建的 NSView继承我们新建的类 SideMenuView

使用 Xib 加载试图

到这里,我们新建的 NSView无法正常的显示出来。那是因为我们在 XIB进行初始化的时候走的是方法是

public init?(coder: NSCoder)

并且 SideMenuView这个类不知道从哪里加载试图。关于如何进行加载自定义的 XIB可以参考这一篇文章。

怎么让继承的类直接使用XIB的布局试图

我们新增一个绑定的属性

@IBOutlet weak var view: BaseView!

设置 XibFile's Owner类为 SideMenuView,绑定 view

我们在 SideMenuView类里面新增一个方法,用来加载自定义的试图。

func loadXibView() {
      Bundle.main.loadNibNamed("SideMenuView", owner: self, topLevelObjects: nil)
      self.view.frame = self.bounds
      self.addSubview(self.view)
 }

我们重写 init?(coder: NSCoder)方法。

required init?(coder: NSCoder) {
    super.init(coder: coder)
    self.loadXibView()
}

当我们再次的运行,我们自定义 Xib的界面已经可以出现了。

但是到目前来说我们几乎达到显示 Logo,但是我们的背景颜色设置白色不是我们所希望的,我们设置默认的为透明颜色。

我们还发现我们我们的试图并没有达到我们设置约束的大小。

我们可以点击 Xcode查看试图层次

我们看出SideMenuView试图的 View并没有达到我们随着父试图变化而变化。

设置 autoresizingMask属性

我们设置一下 autoresizingMask属性。关于 autoresizingMask一些用法可以看一下下面的资料。

iOS开发-自动布局之autoresizingMask使用详解(Storyboard&Code)

我们设置高度自适应。

self.view.autoresizingMask = .viewHeightSizable

我们设置 SideMenuViewview的背景为rgb515151,方便我们进行查看。

我的试图已经能随着变化自动改变高度了。

这个时候我们还发现了一个问题,我们的 Window可以压缩宽度最小,这样左边的侧栏已经挡着了。

修改 Window的最小显示区域

我们可以通过下面设置 window的最小值。

这样我们可以让 Window可以保持最小的尺寸是 600x500

我们修改 SideMenuViewview的试图背景颜色为 RGB444444

上面的图可以明显看出来是需要封装控件的,但是封装完毕是试图依次叠加还是使用 NSTableView。试图依次叠加不利于扩展,我们采用 NSTableView

我们拖拽一个 NSTableView的控件放置在 SideMenuView剩余的位置。布局如下。

如图所示的版本还不能达到我们的要求,有了标题,而且多了一个 Column

我们取消显示 Header和设置只有一个 Cloumn

我们发现我们剩下的只有一个 Column的宽度只有 116并不是全屏显示的。

去掉 NSTableView的边框

我们设置宽度为 205

我们现在发现了一个问题,我们本来有205的宽度的。但是我们现在只能设置最大200,并且预览显示是全屏显示了。

我们在 NSTableView的属性里面看到这个。

我们的宽度留3大小。但是就算去掉了3还是只有 203,剩下的 2跑到那里去了。

我们观察到 NSTableView的父试图已经是 203的宽度了,既然这样我们就默认使用 200

可以设置最外层 Border为没有即可。

我们发现我们刚才创建的 NSTableView显示的背景颜色是白色的,我们可以关闭 NSScrollView的绘制背景颜色和设置 NSTableView的背景颜色为透明即可。

虽然系统的 NSButton是符合图片加文字效果的,但是却无法修改文字的颜色。

我们创建一个类继承与 BaseView名字叫做 SideMenuItemView

我们按照上文所描述的方法创建一个 Xib文件。

我们设置 Xib里面的 NSView的宽度为 205,高度为 49。其实我们这个宽度和高度会随着改变的。

我们在最左侧放置一个 NSImageView布局如下。

我们在 NSImageView的右侧放置一个 NSTextFiledLabel,布局如下。

我们设置右侧 Label的字体颜色为 ebdac1,字体大小为 17px

我们利用 Xib创建下面的关联属性。

@IBOutlet weak var iconImageView: NSImageView!
@IBOutlet weak var itemTitle: NSTextField!

我们按照之前写 SideMenuView试图的方法把 Xib的对象加载进来,具体的方法可以参考上面。

我们设置 View的试图按照宽度和高度自动约束。

self.view.autoresizingMask = [.viewWidthSizable,.viewHeightSizable]

这里说明一点,可选型不是如Objective-C 那样一般用|连接,多个需要放在数组里面。

我们需要的控件已经封装好了,我们现在要做的就是设置 NSTableView的样式为 View Base

B06B6F83-FBBC-4069-802B-AFCF62389B8F

我们删除自动生成的试图,拖拽一个 NSView到 到 Column下面。我们关联 NSTableView的数据源。

4758283F-C1DD-4C44-9C51-FEA669DADDA3

我们在 SideMenuView类里面实现 NSTableView的数据源方法。

BC3FC205-DB67-4781-A977-FFC2DDFF1949

我们通过界面查看器可以看的出来,第一个 Row已经出来了,但是却因为没有设置无法显示。

OSX使用 font-awesome

左侧的图片网站采用 font-awesome框架。 OSX我们使用 FontAwesomeIconFactory框架。

使用 Cocoapods我强烈的建议使用 官方的 App使用

我们设置刚才我们封装的 SideMenuItemViewNSImageView的子类为 NIKFontAwesomeImageView

解决 Cocoapods不能使用 IBDeisgnable

我们在使用 Cocoapods时候不能使用 IBDeisgnable的解决办法。

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings['CONFIGURATION_BUILD_DIR'] = '$PODS_CONFIGURATION_BUILD_DIR'
    end
  end
end

很不幸的是在另外的 Xib使用 SideMenuItemView报下面的错误。

E6E2C7AD-960E-4B7E-B418-AB327F3144AC

我们在 Debug IBDeisgnable时候发现抱错下面的代码。

80C7068F-07EB-448D-BBBD-1B42358BEE81

因为我们绑定是对象属于 !类型,但是我们此时还不存在这个变量。故而强行当做存在的使用崩溃了。

到目前为止,我不清楚这个对象没有初始化是为什么导致的。但是只是在 Xib进行初始化 IBDeisgnable抱错,但是可以正常运行的。

但是这样可能不能满足我的要求,我们尽量解决就解决。我们之前的方法里面可以接受一个数组的指针。

我们看看数组里面元素如何。

var views:NSArray = NSArray()
Bundle.main.loadNibNamed("SideMenuItemView", owner: self, topLevelObjects: &views)
96D49D78-164D-4735-80F5-A92558454117

数组里面是有元素的,我们尝试从这里面的元素获取试一下。

func loadXibView() {
    guard let xibView = self.getXibView(nibName: "SideMenuItemView") else {
        return
    }
    xibView.autoresizingMask = [.viewWidthSizable,.viewHeightSizable]
    xibView.frame = self.bounds
    self.addSubview(xibView)
}
    
func getXibView(nibName:String) -> NSView? {
    var views:NSArray = NSArray()
    Bundle.main.loadNibNamed(nibName, owner: self, topLevelObjects: &views)
    var xibView:NSView?
    for any in views {
        guard let view = any as? NSView else {
            continue
        }
        xibView = view
    }
    return xibView
}

我们发现之前报的错误果然消失了。我们可以采用这一种方式来加载试图,我们可以封装一下,方便我们用。

如何在 Swift3获取类名字符串。

NSStringFromClass(type(of:self))
extension NSView {
    func loadXibView() {
        guard let xibView = self.getXibView(nibName: NSStringFromClass(type(of:self))) else {
            return
        }
        xibView.autoresizingMask = [.viewWidthSizable,.viewHeightSizable]
        xibView.frame = self.bounds
        self.addSubview(xibView)
    }
    
    func getXibView(nibName:String) -> NSView? {
        var views:NSArray = NSArray()
        Bundle.main.loadNibNamed(nibName, owner: self, topLevelObjects: &views)
        var xibView:NSView?
        for any in views {
            guard let view = any as? NSView else {
                continue
            }
            xibView = view
        }
        return xibView
    }
}

但是发现竟然加载不出来任何数据,原来我们发现自动生成的类名带有工程前缀。

"Jekyll_Admin_Mac.SideMenuView"

我们可以采用分割字符串使用最后一个。

FE9DB89D-DB70-4F71-B280-8A65921752C2

我们将 SideMenuItemView改成继承与 NIKFontAwesomeImageView

NIKFontAwesomeImageViewIBDeisgnable不能在 Xib预览的。

我们设置 NIKFontAwesomeImageView属性如下。

  • icon Hex : f02d

  • Size : 17

    生成的图片是正方形,并不能和网站的样式可以设置宽度和高度。

  • Color : EBDAC1

我们运行一下发现已经可以正常的运行了。

D18987B2-B0A1-47F5-BBFB-682D44D61367

面向对象设计

我们配置一下 NSTableView的数据源如下:

let menuItemDict = [
    "文章":"F02D",
    "页面":"F15C",
    "数据":"F1C0",
    "文件":"F15B",
    "配置":"F013",
]

我们设置一下 NSTableView数据代理。

public func numberOfRows(in tableView: NSTableView) -> Int {
    return menuItemDict.keys.count
}

func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
    guard let view = tableView.make(withIdentifier: "SideMenuItemView", owner: self) as? SideMenuItemView else {
        return nil
    }
    view.itemTitle.stringValue = Array(menuItemDict.keys)[row]
    view.iconImageView.iconHex = Array(menuItemDict.values)[row] as NSString
    return view
}

⚠️对于 Swift3里面的 Dictionary的属性 Keys无法作为正常的 Array使用,我们需要用 Array()对其进行初始化。

68A0CE15-89BA-4E79-A705-A431DCDF765C

上图是我们运行起来的效果。但是呢和我们网页的看起来还是有写差别的。

我们在 SideMenuItemView.xib上面的底部添加一条线。布局如下:

54D99DC5-BD43-44EC-8F37-83D156C2C01C

线继承与 BaseView,我们设置颜色为 424242

777EFD44-93A6-4A80-BD5E-92FB76E8A426

虽然线是出来了,但是我们不想让全部出现。

我们在 SideMenuItemView关联刚才的线。

@IBOutlet weak var lineView: BaseView!

我们修改配置如下。

func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
    guard let view = tableView.make(withIdentifier: "SideMenuItemView", owner: self) as? SideMenuItemView else {
        return nil
    }
    view.itemTitle.stringValue = Array(menuItemDict.keys)[row]
    let values = Array(menuItemDict.values)[row]
    if let hexString = values[0] as? NSString {
        view.iconImageView.iconHex = hexString
    }
    if let hidden = values[1] as? Bool {
        view.lineView.isHidden = hidden
    }
    return view
}

⚠️因为字典的取值是无序的,所以我们这样的写法会导致我们的显示出现问题。

我们修改我们的数据源为一个 Array数组。

let menuItems = [
    ["文章", "F02D", false],
    ["页面", "F15C", true],
    ["数据", "F1C0", false],
    ["文件", "F15B", true],
    ["配置", "F013", false],
]

我们需要修改对应数据赋值。

func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
    guard let view = tableView.make(withIdentifier: "SideMenuItemView", owner: self) as? SideMenuItemView else {
        return nil
    }
    let values = menuItems[row]
    guard values.count == 3 else {
        return nil
    }
    if let title = values[0] as? String {
        view.itemTitle.stringValue = title
    }
    if let hexIcon = values[1] as? NSString {
        view.iconImageView.iconHex = hexIcon
    }
    if let hidden = values[2] as? Bool {
        view.lineView.isHidden = !hidden
    }
    return view
}

我们运行此时显示如下。

B97289BE-AD47-4616-BAF0-0A62F77DE986

我们给 NSTableView绑定一个方法事件。

@IBAction func didClickRow(_ sender: NSTableView) {
}

我们给 NSTableView新增一个属性是否被选中。然而现在一个问题已经出现,现在这么多的配置需要配置岂不是很麻烦。

这就涉及到面向对象思想,但是我们可以在 Swift中使用 Struct作为我们的配置数据源。

struct SideMenuItemConfiguration {
    let title:String ///< 标题
    let iconHex:String ///< icon 的十六进制字符串
    let hidden:Bool ///< 是否隐藏底部线
    let selected:Bool ///< 是否被选中
}

我们修改我们的数据源:

let menuItems = [
//        ["文章", "F02D", false],
//        ["页面", "F15C", true],
//        ["数据", "F1C0", false],
//        ["文件", "F15B", true],
//        ["配置", "F013", false],
    SideMenuItemConfiguration(title: "文章", iconHex: "F02D", hidden: true, selected: false),
    SideMenuItemConfiguration(title: "页面", iconHex: "F15C", hidden: false, selected: false),
    SideMenuItemConfiguration(title: "数据", iconHex: "F1C0", hidden: true, selected: false),
    SideMenuItemConfiguration(title: "文件", iconHex: "F15B", hidden: false, selected: false),
    SideMenuItemConfiguration(title: "配置", iconHex: "F013", hidden: true, selected: false),
]

再次修改我们的赋值代码。

func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
    guard let view = tableView.make(withIdentifier: "SideMenuItemView", owner: self) as? SideMenuItemView else {
        return nil
    }
    let configuration = menuItems[row]
    view.itemTitle.stringValue = configuration.title
    view.iconImageView.iconHex = configuration.iconHex as NSString
    view.lineView.isHidden = configuration.hidden
    return view
}

我们的代码比之前要精简一些。

我们在点击 NSTableView点击方法获取选中的 Row,之后让选中数据源状态被选中,其他取消选中。

@IBAction func didClickRow(_ sender: NSTableView) {
    let row = sender.selectedRow
    for (index, configuration) in menuItems.enumerated() {
        configuration.selected = index == row
    }
    sender.reloadData()
}

‼️这段代码会被抱错,因为我们修改了被 let标记的常量,我们修改成 var即可。

而且我们 enumerated()出来的竟然是也是 Let标记的,我们用 var标记。

81D12FF2-A02E-496A-80D9-BC3994745199

我们设置选中的颜色为 ff9900。默认的颜色为 EBDAC1

我们在 SideMenuItemConfiguration新增默认颜色和选中颜色的属性。

let normalColor:NSColor = NSColor(red:1.000, green:0.600, blue:0.000, alpha:1.000) ///< 默认状态颜色
let selectedColor:NSColor = NSColor(red:0.922, green:0.855, blue:0.757, alpha:1.000) ///< 选中的颜色

我们设置默认值这样 之前的代码也可以 正常的编译通过。

我们需要根据选中状态设置图标的颜色还有文字的颜色,这样就要增加一下逻辑。这些都是修改 SideMenuItemView类的内容,为啥不采用赋值,让 SideMenuItemView内部处理呢?

我们说做就做。

var menuItemConfiguration:SideMenuItemConfiguration? {
    didSet {
        guard let configuration = self.menuItemConfiguration else {
            return
        }
        self.itemTitle.stringValue = configuration.title
        self.iconImageView.iconHex = configuration.iconHex as NSString
        self.lineView.isHidden = configuration.hidden
        let color = configuration.selected ? configuration.selectedColor : configuration.normalColor
        self.iconImageView.color = color
        self.itemTitle.textColor = color
    }
}

我们给 SideMenuItemView类新增 menuItemConfiguration属性,当给这个属性设置值的时候我们做出对应处理。

我们现在可以给我们 NSTableView的代码精简如下:

func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
    guard let view = tableView.make(withIdentifier: "SideMenuItemView", owner: self) as? SideMenuItemView else {
        return nil
    }
    let configuration = menuItems[row]
    view.menuItemConfiguration = configuration
    return view
}
F042453B-FB43-481D-8CC0-07A50CD88F2B

但是我们运行起来,却发现全部都是选中的颜色,原来是我们默认颜色和选中颜色配置反了导致,我们修改过来即可。

1F549B40-E8F1-4069-9B3A-7F0ADD6B87C3

此时我们的初始化配置恢复了正常,但是我们点击了没有任何的变化。让我们找一下出现这种现象原因是怎么导致的。

⚠️因为结构体没有被引用,所以便利出来的临时变量属于一个新的地址。我们需要修改临时变量之后替换掉之前数组里面的。

@IBAction func didClickRow(_ sender: NSTableView) {
    let row = sender.selectedRow
    for (index, var configuration) in menuItems.enumerated() {
        configuration.selected = index == row
        menuItems[index] = configuration
    }
    sender.reloadData()
}

‼️此时需要注意的是我们需要修改我们的 menuItemsvar类型。

11

此时我们的效果已经达到了,我们觉得默认启动显示的第一个界面是0元素。

我们绑定界面的元素 NSTableViewSideMenuView

@IBOutlet weak var tableView: NSTableView!

我们把 didClickRow逻辑封装成下面的对象。

func changeTabeleViewState(row:Int, tableView:NSTableView) {
    for (index, var configuration) in menuItems.enumerated() {
        configuration.selected = index == row
        menuItems[index] = configuration
    }
    tableView.reloadData()
}

我们修改 didClickRow的调用。

@IBAction func didClickRow(_ sender: NSTableView) {
    let row = sender.selectedRow
    changeTabeleViewState(row: row, tableView: sender)
}

我们修改 required init?(coder: NSCoder)的代码如下:

required init?(coder: NSCoder) {
    super.init(coder: coder)
    self.loadXibView()
    changeTabeleViewState(row: 0, tableView: self.tableView)
}