成品展示

image-20210123165039936

image-20210125015513814

准备工作

首先新建一个macOS上的APP,选择SwiftUI+AppKit LifeCycle即可,因为SwiftUI的lifecycle并不支持Catalina

https://zhuanlan.zhihu.com/p/87960477

https://zhuanlan.zhihu.com/p/88235396

开始开发

使用的API

菜单栏UI

popover

准备工作中的两个链接,我们能够创建支持左右双键的应用,但是左键暂时还没有用

结合此篇文章就可以创建一个空的页面

https://juejin.cn/post/6844904101877121037

image-20210123000321403

然后拖拽两人Table View到PopoverViewController之中,双击更改Header的标题

点击TableCellView更改Identifier

image-20210123165151070

调整一下列宽后就可以开始写代码了,使用⌃⌥⌘↩,然后按住Control拖拽TableView到代码中,更改名字即可。

接下来开发过程相对iOS来说区别不大,NSTableView和UITableView的主要区别是没有了Section,但是多了Column,需要进行判断

image-20210123165612513

如图所示。

对于一个CellView,只有默认的TextField和ImageView,如果要增加其他的可以拖拽控件实现。

网络请求采用Alamofire实现,不过需要在App Sandbox中设置一下。

image-20210123171442664

详细代码可见GitHub:https://github.com/Zrzzzz/EzFund.git

菜单栏轮播基金情况

控制板UI

image-20210125014930229

有搜索功能,其中SearchField只要通过设置action即可实现自动搜索,十分方便。

Cocoa Binding

这里可以提一句Cocoa Binding,十分方便,首先在代码中定义一个变量,并使用@objc dynamic修饰

1
@objc dynamic var searchText: String = ""

在StoryBoard中设置三个地方即可,则能动态绑定数据,不过要注意不要选错了,要选择中间的Cell(NSTextField)

image-20210124234651969

界面显示和隐藏

关于界面的显示和隐藏,有几个坑点。界面我使用了Window来操作,没有用到NSWindowController。首先我们希望失去焦点后隐藏,并且不被释放掉。

1
2
window.isReleasedWhenClosed = false
window.hidesOnDeactivate = true

同时,contentViewController需要从Storyboard中取实例,不能直接初始化赋值。

然后如果在显示时只使用window.makeKeyAndOrderFront的话,在关闭一次界面后不再显示了,因为此时App是Deactive的,所以使用以下语句。

1
2
3
4
5
6
@IBAction func openBoard(_ sender: Any) {
NSApp.activate(ignoringOtherApps: true)
if let w = NSApp.windows.first(where: {$0 == window}) {
w.makeKeyAndOrderFront(self)
}
}
TableView的删除、拖拽

拖拽其实挺简单的,但是不支持[String]类型,需要自己做一点功夫,主要使用到三个函数

1
2
3
4
5
6
7
8
9
func tableView(_ tableView: NSTableView, pasteboardWriterForRow row: Int) -> NSPasteboardWriting?
func tableView(_ tableView: NSTableView,
validateDrop info: NSDraggingInfo,
proposedRow row: Int,
proposedDropOperation dropOperation: NSTableView.DropOperation) -> NSDragOperation
func tableView(_ tableView: NSTableView,
acceptDrop info: NSDraggingInfo,
row: Int,
dropOperation: NSTableView.DropOperation) -> Bool

详情见代码吧,需要使用到FilePromiseProvider。参考自https://developer.apple.com/documentation/appkit/nstableviewdatasource/supporting_table_view_drag_and_drop_through_file_promises

注意一点就是需要注册拖动类型

1
tableView.registerForDraggedTypes

不过对比FilePromiseProvider,更简单的方式是使用基金的代码(unique)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
extension BoardViewController {
func tableView(_ tableView: NSTableView, pasteboardWriterForRow row: Int) -> NSPasteboardWriting? {
return fundData[row][0] as NSString
}

func tableView(_ tableView: NSTableView, validateDrop info: NSDraggingInfo, proposedRow row: Int, proposedDropOperation dropOperation: NSTableView.DropOperation) -> NSDragOperation {
guard dropOperation == .above,
let tableView = info.draggingSource as? NSTableView else { return [] }

tableView.draggingDestinationFeedbackStyle = .gap
return [.move]
}

func tableView(_ tableView: NSTableView, acceptDrop info: NSDraggingInfo, row: Int, dropOperation: NSTableView.DropOperation) -> Bool {
guard let items = info.draggingPasteboard.pasteboardItems,
let pasteBoardItem = items.first,
let pasteBoardItemName = pasteBoardItem.string(forType: .string),
let index = fundData.firstIndex(where: {$0[0] == pasteBoardItemName}) else { return false }

let indexset = IndexSet(integer: index)
fundData.move(fromOffsets: indexset, toOffset: row)

/* Animate the move to the rows in the table view. The ternary operator
is needed because dragging a row downwards means the row number is 1 less */
tableView.beginUpdates()
tableView.moveRow(at: index, to: (index < row ? row - 1 : row))
tableView.endUpdates()

return true
}
}

删除来讲就比较简单了,不过根据苹果的Document,有使用顺序之分

This method deletes from the table the rows represented at indexes and automatically decreases numberOfRows by the count of indexes.

The row indexes should be with respect to the current state displayed in the table view, and not the final state, because the specified rows do not exist in the final state.

Calling this method multiple times within the same beginUpdates() and endUpdates() block is allowed, and changes are processed incrementally.

Changes are processed incrementally as the insertRows(at:withAnimation:), removeRows(at:withAnimation:), and the moveRow(at:to:) methods are called. It is acceptable to delete row 0 multiple times, as long as there is still a row available.

Note

NSCell-based table views must first call beginUpdates() before calling this method.

而且需要调用beginUpdate()

1
2
3
4
5
6
7
8
9
@IBAction func deleteRow(_ sender: Any) {
let row: Int = tableView.selectedRow
guard row != -1 else { return }

tableView.beginUpdates()
tableView.removeRows(at: IndexSet(integer: row), withAnimation: .effectFade)
tableView.endUpdates()
fundData.remove(at: row)
}

打包成dmg

https://www.smslit.top/2019/01/06/mac_app_to_dmg/