正如很多开发者一样,进入一个新平台开发连带学习,通常都会开发一个极简的应用,很多人都会开发Todo类应用,这类应用一般来说比较简单容易上手。我也打算开发一个,一来是练手,二来是写一个自己用的极简Todo类应用。 目前市面上的Todo类应用,一般来说,都比较复杂,太多我不需要的功能了,而且还有一些增值付费,对于我来说,根本没有那么多的复杂需求,只需要一个随手打开和关闭的记录而已,放在托盘上就非常的方便。 所以,需求如下:
- macOS托盘应用,随手打开和关闭;
- 带有分页标签,以便标记不同类型的todo;
- 最好有利用iCloud的数据云同步,方便未来与其他平台同步数据;
基于以上需求,做一个技术选型:
- 由于SwiftUI在macOS上表现实在是不敢恭维,所以选用传统的AppKit进行开发;
- 基于个人写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文件内容如下:
import Cocoa
class MyApplication: NSApplication {}
let app = MyApplication.shared
let delegate = AppDelegate()
app.delegate = delegate
app.run()
2.3 创建托盘应用的基本框架
在AppDelegate.swift文件中,添加相关代码以实现点击托盘图标,主窗口的显示/隐藏。 创建一个Tray类,用于管理托盘图标的创建和窗口的弹出。
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()
,就添加了一个系统图标。
class AppDelegate: NSObject, NSApplicationDelegate {
private let tray = Tray(iconName: "text.badge.checkmark", viewController: ViewController())
func applicationDidFinishLaunching(_ aNotification: Notification) {
tray.install()
}
}
此处需要注意的是,必须要声明一个Tray的成员变量,如果不声明成员变量,则系统图标不会添加成功。
现在在ViewController中,显示一个Hello World。
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 为约束,从而避免这种冲突。
最终的效果如下:
另外,若要应用图标不显示在Docker栏上,需要在Info.plist上设置 Application is agent (UIEelement) 为YES。