正如很多开发者一样,进入一个新平台开发连带学习,通常都会开发一个极简的应用,很多人都会开发Todo类应用,这类应用一般来说比较简单容易上手。我也打算开发一个,一来是练手,二来是写一个自己用的极简Todo类应用。
目前市面上的Todo类应用,一般来说,都比较复杂,太多我不需要的功能了,而且还有一些增值付费,对于我来说,根本没有那么多的复杂需求,只需要一个随手打开和关闭的记录而已,放在托盘上就非常的方便。
所以,需求如下:

  1. macOS托盘应用,随手打开和关闭;
  2. 带有分页标签,以便标记不同类型的todo;
  3. 最好有利用iCloud的数据云同步,方便未来与其他平台同步数据;

基于以上需求,做一个技术选型:

  1. 由于SwiftUI在macOS上表现实在是不敢恭维,所以选用传统的AppKit进行开发;
  2. 基于个人写Android应用的习惯,更倾向于使用代码或者XML的方式构建界面,所以在使用AppKit时,不使用Storyboard或者XIB的方式构建界面。

一、创建项目

打开Xcode,创建一个新的macOS项目,语言选择swift,Interface暂时选storyboard,先创建项目,在稍后删除相关的storyboard文件和配置。

二、项目配置

2.1 删除Main.storyboard

首先,删除自动创建的storyboard文件Main.storyboard,此时再构建项目会出现错误,先不用着急,接下来进行修改。

2.2 替换应用程序入口

其次,在target目录下,创建一个main.swift文件,然后把AppDelegate.swift中的@main注解删除掉,main.swift文件内容如下:

1
2
3
4
5
6
7
8
9
import Cocoa

class MyApplication: NSApplication {}

let app = MyApplication.shared
let delegate = AppDelegate()

app.delegate = delegate
app.run()

2.3 创建托盘应用的基本框架

在AppDelegate.swift文件中,添加相关代码以实现点击托盘图标,主窗口的显示/隐藏。
创建一个Tray类,用于管理托盘图标的创建和窗口的弹出。

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class Tray {

public static let POPOVER_SIZE = NSSize(width: 320, height: 400)

private let iconName: String
private var statusItem: NSStatusItem!
private var popover: NSPopover!
private var viewController: NSViewController

init(iconName: String, viewController: NSViewController) {
self.iconName = iconName
self.viewController = viewController
}

func install() {
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)

if let trayBtn: NSStatusBarButton = statusItem.button {
trayBtn.image = NSImage(systemSymbolName: iconName, accessibilityDescription: nil)

trayBtn.target = self;
trayBtn.action = #selector(togglePopover(_:))
}

viewController.loadViewIfNeeded()
viewController.view.frame = NSRect(x: 0, y: 0, width: Tray.POPOVER_SIZE.width, height: Tray.POPOVER_SIZE.height)

popover = NSPopover()
popover.contentViewController = viewController

}

@objc private func togglePopover(_ sender: Any?) {
if popover.isShown {
closePopover(sender: sender)
} else {
showPopover(sender: sender)
}
}

private func showPopover(sender: Any?) {
if let button = statusItem.button {
popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
}
}

private func closePopover(sender: Any?) {
popover.performClose(sender)
}

}

此类中,接收两个构造参数,一个是图标名称,对应着资源名称或者是SF Symbols图标名称,另外一个参数就是ViewController的具体实现。
在AppDelegate中的applicationDidFinishLaunching方法中,执行Tray.install(),就添加了一个系统图标。

1
2
3
4
5
6
7
8
9
class AppDelegate: NSObject, NSApplicationDelegate {

private let tray = Tray(iconName: "text.badge.checkmark", viewController: ViewController())

func applicationDidFinishLaunching(_ aNotification: Notification) {
tray.install()
}

}

此处需要注意的是,必须要声明一个Tray的成员变量,如果不声明成员变量,则系统图标不会添加成功。

现在在ViewController中,显示一个Hello World。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class ViewController: NSViewController {

override func viewDidLoad() {
super.viewDidLoad()

let text = NSTextView()
text.translatesAutoresizingMaskIntoConstraints = false
text.alignment = .center
text.string = "Hello World"

self.view.addSubview(text)

NSLayoutConstraint.activate([
text.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
text.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
text.topAnchor.constraint(equalTo: self.view.topAnchor),
text.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
])
}
}

注意此处的text.translatesAutoresizingMaskIntoConstraints = false,这里如果不设置的话,弹出窗口是不会填满整个父布局的。
translatesAutoresizingMaskIntoConstraints 是一个布尔属性,用于指示是否启用自动布局中的自动转换。

在 iOS 和 macOS 开发中,通常使用自动布局来管理界面的布局。自动布局系统使用约束(constraints)来描述视图之间的关系和布局规则。当你通过 Interface Builder 或代码创建视图时,默认情况下,视图的 translatesAutoresizingMaskIntoConstraints 属性是设置为 true 的。这意味着视图会根据其 frame 和 autoresizingMask 属性自动转换为相应的约束。

但是,在使用自动布局时,通常推荐将 translatesAutoresizingMaskIntoConstraints 设置为 false。这样做的原因是,如果你手动创建约束来布局视图,那么这些约束将会和自动转换的约束发生冲突,导致布局问题。通过将 translatesAutoresizingMaskIntoConstraints 设置为 false,你可以明确地告诉系统不要自动转换视图的 autoresizingMask 为约束,从而避免这种冲突。

最终的效果如下:
tray_app_hello_world

另外,若要应用图标不显示在Docker栏上,需要在Info.plist上设置 Application is agent (UIEelement) 为YES。

本文采用CC-BY-SA-3.0协议,转载请注明出处
Author: boybeak