[TOC]
✅为什么要开发
Jekyll-Admin-Mac
?因为接触到使用
Jekyll
构建博客十分的方便,但是Jekyll-Admin
里面的功能又差强人意。如果修改
Jekyll-Admin
里面的源码代价是巨大的,不如用自己擅长的语言来写,正好还有自动生成的 API 可以用。对于
Jekyll-Admin-Mac
的UI
我们采用网页的配色即可。
获取 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
命令可以去谷歌和百度。
设置左侧的 Logo
我们拖拽文件 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
为继承与 BaseView
背景颜色试图。我们设置背景颜色为 rgb343434
。


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


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

我们设置刚才新建的 NSView
继承我们新建的类 SideMenuView
。
使用 Xib 加载试图
到这里,我们新建的 NSView
无法正常的显示出来。那是因为我们在 XIB
进行初始化的时候走的是方法是
public init?(coder: NSCoder)
并且 SideMenuView
这个类不知道从哪里加载试图。关于如何进行加载自定义的 XIB
可以参考这一篇文章。
我们新增一个绑定的属性
@IBOutlet weak var view: BaseView!
设置 Xib
的 File'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
我们设置 SideMenuView
的view
的背景为rgb515151,方便我们进行查看。

我的试图已经能随着变化自动改变高度了。
这个时候我们还发现了一个问题,我们的 Window
可以压缩宽度最小,这样左边的侧栏已经挡着了。
修改 Window
的最小显示区域
我们可以通过下面设置 window
的最小值。

这样我们可以让 Window
可以保持最小的尺寸是 600x500
。
我们修改 SideMenuView
的 view
的试图背景颜色为 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
的右侧放置一个 NSTextFiled
的 Label
,布局如下。


我们设置右侧 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
。

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

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

我们通过界面查看器可以看的出来,第一个 Row
已经出来了,但是却因为没有设置无法显示。
在 OSX
使用 font-awesome
左侧的图片网站采用 font-awesome
框架。 OSX
我们使用 FontAwesomeIconFactory
框架。
使用
Cocoapods
我强烈的建议使用 官方的App
使用
我们设置刚才我们封装的 SideMenuItemView
的 NSImageView
的子类为 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
报下面的错误。

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

因为我们绑定是对象属于 !
类型,但是我们此时还不存在这个变量。故而强行当做存在的使用崩溃了。
到目前为止,我不清楚这个对象没有初始化是为什么导致的。但是只是在 Xib
进行初始化 IBDeisgnable
抱错,但是可以正常运行的。
但是这样可能不能满足我的要求,我们尽量解决就解决。我们之前的方法里面可以接受一个数组的指针。
我们看看数组里面元素如何。
var views:NSArray = NSArray()
Bundle.main.loadNibNamed("SideMenuItemView", owner: self, topLevelObjects: &views)

数组里面是有元素的,我们尝试从这里面的元素获取试一下。
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
}
我们发现之前报的错误果然消失了。我们可以采用这一种方式来加载试图,我们可以封装一下,方便我们用。
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"
我们可以采用分割字符串使用最后一个。

我们将 SideMenuItemView
改成继承与 NIKFontAwesomeImageView
。
NIKFontAwesomeImageView
的IBDeisgnable
不能在Xib
预览的。

我们设置 NIKFontAwesomeImageView
属性如下。
-
icon Hex
:f02d
-
Size
:17
生成的图片是正方形,并不能和网站的样式可以设置宽度和高度。
-
Color
:EBDAC1
我们运行一下发现已经可以正常的运行了。

面向对象设计
我们配置一下 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()
对其进行初始化。

上图是我们运行起来的效果。但是呢和我们网页的看起来还是有写差别的。
我们在 SideMenuItemView.xib
上面的底部添加一条线。布局如下:

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

虽然线是出来了,但是我们不想让全部出现。
我们在 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
}
我们运行此时显示如下。

我们给 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
标记。
我们设置选中的颜色为 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
}

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

此时我们的初始化配置恢复了正常,但是我们点击了没有任何的变化。让我们找一下出现这种现象原因是怎么导致的。
⚠️因为结构体没有被引用,所以便利出来的临时变量属于一个新的地址。我们需要修改临时变量之后替换掉之前数组里面的。
@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()
}
‼️此时需要注意的是我们需要修改我们的
menuItems
为var
类型。

此时我们的效果已经达到了,我们觉得默认启动显示的第一个界面是0元素。
我们绑定界面的元素 NSTableView
到 SideMenuView
。
@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)
}