近两周闲下来的时间都在写一个新的App,都没时间写字和写文章。昨天美工大哥问了我一个问题,MacOS到底有哪些系统图标可用?图标文件夹的确是一个解决方案,但保证可用还是得额外加进项目的素材库,和外加的图标没有区别。真的内置的图标的话,这个问题换句话说是NSImage.Name里面到底有哪些图标。本文用看着也越来越重要的SwiftUI,抓取开发者文档的数据展示可用的图标和尺寸。

已有资料

开发者文档里有专门的一页,列出了目前可用的系统图标。但只有名字,没有相应的图片放在边上,也看不到尺寸。

github上发现了一个项目,可惜几年不更新了,图标数据也是写死在代码里的。项目名字叫fucking_nsimage_syntax,不知道是不是也在吐槽开发者文档里不放图和尺寸。

如果自己写一个的话,基本是,网页抓取数据->数据对应图标和尺寸->SwiftUI展示,也很简单。

数据抓取

开发者文档载入是先框架后数据,简单看一下包,可以找到图标页的数据通过获取这一个网址取得:

1
https://developer.apple.com/tutorials/data/documentation/appkit/nsimage/name.json

既然已经想好SwiftUI展示,那就直接做进界面的方法里好了,这里就是一个简单的网页请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
extension ImageNameDemoView {
func getImages(callback: @escaping ([String]) -> Void) {
let url = URL(string: "https://developer.apple.com/tutorials/data/documentation/appkit/nsimage/name.json")!
let task = URLSession.shared.dataTask(with: url) { d, _, _ in
guard let d = d else { return }
if let r = try? JSONSerialization.jsonObject(with: d, options: .mutableContainers) {
if let refs = (r as! [String: Any])["references"] {
var rl = [String]()
for v in (refs as! [String: Any]).values {
let n = (v as! [String: Any])["title"] as! String
if n.hasSuffix("Name") {
rl.append(n)
}
}
callback(rl)
}
}
}
task.resume()
}
}

我们要的数据全都在references键里,通过判断后缀是否有Name去除非图标的内容。

数据对应图标和尺寸

这里获取了需要的数据的数组,但这个数据只是属性名不是实际的值,所以还需要一个转化。

理论上来说这是个很简单的过程,想象中通过value(forKey:)就可以解决。但NSImage现在还不是正常实现的类,所以这个方法和同理的反射都不能用。

这里是可以OC去取值的,但实际上取巧的办法就可行:

1
2
3
4
5
6
7
extension ImageNameDemoView {
func getNSImageName(_ s: String) -> String {
var r = "NS\(s.prefix(1).capitalized)\(s.suffix(s.count - 1).prefix(s.count - 5))"
if r == NSImage.applicationIconName { r = "" }
return r
}
}

图标和对应的尺寸就简单了:

1
2
NSImage(named: name)
NSImage(named: name)?.size.debugDescription ?? ""

SwiftUI展示

大致构思一下界面,这里一行放十个图标,放一个按钮加载数据(onAppear也成),右击图标显示属性名和大小。

所以大致的框架就出来了:

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
struct ImageNameDemoView: View {

var columnNumber = 10
@State var images: [String] = []

var body: some View {
VStack {
ForEach(0 ..< images.count / columnNumber + 1, id: \.self) { y in
HStack(spacing: 5) {
ForEach(0 ..< self.columnNumber, id: \.self) { x in
VStack(spacing: 5) {
if x + y * self.columnNumber < self.images.count {
// replace Text with Image
Text("\(self.images[x + y * self.columnNumber])")
}
}.frame(width: 30, height: 30)
}
}
}
Button("Press") {
self.getImages() { l in
self.images = l
}
}
}
}
}

下面把中间的文字换成有右键菜单的图片:

1
2
3
4
5
6
Image(nsImage: NSImage(named: self.getNSImageName(self.images[x + y * self.columnNumber])) ?? NSImage())
.contextMenu {
Text("\(self.images[x + y * self.columnNumber])")
.font(.system(size: 16))
Text("\(NSImage(named: self.getNSImageName(self.images[x + y * self.columnNumber]))?.size.debugDescription ?? "")")
}

界面就快速的完成了,大概是一个这样的界面:

demo-image

其他

SwiftUI已经有了越来越好的开发体验,单数据源绑定的逻辑,完备的组件和方法,让界面的构建变得十分简单。

加上Widgets等内容的大力支持,地位可以想见会快速上升。

就我的体验而言,目前有一些问题还是需要有思想准备。Swiftui错误提示仍旧非常糟糕,界面中小的语法错误会导致整个构建时间耗时非常久,并最后返回,最外层View无法推断返回值类型这样的无用信息。

可能搜出来的答案很多是不合适的用法,也许我是个例,也许OC程序员写Swift都一样,看自动补全有好像能用的方法用了就行。例如文本框获取焦点,搜到的都是view.becomeFirstResponder()。IOS上奇怪的可用,MacOS上奇怪的不能用。翻翻文档才会发现调用view.window?.makeFirstResponder(view)才是标准的用法。

以及一些小问题,即使有NSViewRepresentable的万能后备,有时候还是会因为一些神奇的原因重写整个组件,例如目前List在MacOS上如果需要去掉分割线的背景需要重写整个List,甚至没有IOS上取巧的直接改写UITableView的办法。

但总体,惊喜是会比坑多的,建议一试。