肇鑫的技术博客

业精于勤,荒于嬉

macOS菜单栏状态项小结

所谓菜单栏状态项,指的是macOS顶部菜单栏右侧的那一排图标。Windows里叫系统托盘,macOS就叫菜单栏状态项。后面简称状态项。

获取状态项并制定图标

let statusItem = NSStatusBar.system.statusItem(withLength:NSStatusItem.squareLength)
guard let button = statusItem.button else {
    fatalError()
}
        
button.image = NSImage(imageLiteralResourceName: "TrayIcon")

图标的图片选择40x40像素的,放在2x下。

添加响应或菜单项

这里是一个坑。虽然状态项支持单击和显示菜单,但是苹果规定,当存在菜单时,单击无效,而只会显示菜单。

var menu: NSMenu?
When non-nil, the status item’s single click action behavior is not used. The menu can be removed by setting the value of this property to nil.

所以你要么添加button的点击功能,要么选择构建一个菜单。这个坑可以通过技术手段绕过,我们后面再提。

鼠标左键和鼠标右键对应不同的响应

状态项默认只支持鼠标左键的点击,而不支持其它的鼠标事件。如果想要支持鼠标右键的点击,我们需要自行实现。

https://stackoverflow.com/questions/32188581/call-action-when-nsstatusbarbutton-is-right-clicked

我选择是上面链接中的方案,新建一个自定义的视图,该视图支持鼠标右键点击,然后再将该视图添加到button上。

自定义的视图:

class MouseRightClickView: NSView {
    var closure:(() -> ())!

    override func rightMouseUp(with event: NSEvent) {
        super.rightMouseUp(with: event)
        
        closure()
    }
}

同时支持鼠标单击和菜单项

现在状态项支持鼠标左键和鼠标右键了,下面我们来实现鼠标左键点击“显示/隐藏应用”,右键点击“显示菜单项”。

guard let button = statusItem.button else {
    fatalError()
}
    
button.image = NSImage(imageLiteralResourceName: "TrayIcon")
button.action = #selector(mouseLeftButtonClicked)
    
// Add mouse right click
let subView = MouseRightClickView(frame: button.frame)
subView.closure = {
    self.constructMenu()
    button.performClick(nil) // menu won't show without this
}
button.addSubview(subView)

大家注意看,创建完成菜单后,必须手工添加一个按钮的点击。如果没有这个,那么只会生成菜单,而不会立即弹出菜单。最后,由于苹果规定如果存在菜单,则不会响应鼠标点击,我们必须在每次菜单关闭时,自动去掉菜单。这就需要设置菜单的代理。

private func constructMenu() {
    let menu = NSMenu()
    menu.delegate = self
    ...
}

菜单代理

extension AppDelegate:NSMenuDelegate {
    // remove the menu or later mouse left click will call it.
    func menuDidClose(_ menu: NSMenu) {
        statusItem.menu = nil
    }
}

Locale苹果官方文档补遗

我们知道,在Xcode中可以通过指定运行时的语言,来测试相应的翻译。如图,我指定了中文作为在测试时的语言。

测试时指定语言

这个方式,在测试翻译的情况下是没有问题的。

最近我需要在应用中通过WKWebView,根据用户所使用的语言来动态加载网页。为了达到这个目的,我使用了

Locale.current.languageCode

根据苹果的文档,languageCode的含义是

The language code of the locale, or nil if has none.

但是实际使用中,我发现它并不能返回zh

iOS_12.1_Locale_iPhoneX

如图,实际上返回的是en_CN

我就此问题向苹果提交了错误报告。苹果回复说,从macOS 10.13, iOS 11开始,Locale.current.languageCode返回的实际上是Bundle.main.preferredLocalizations.first。这是因为苹果认为这样的用户体验最好。

苹果举例说,如果一个用户对于语言的偏好顺序是[ "zh-CN", "de-CN", "en-CN" ],而某个应用仅支持德语和英语,那么Locale.current.languageCode返回的就是de_CN

按照苹果的建议,如果想要测试中文,那么第一步就是添加中文的翻译。于是我添加了简体中文的翻译。这一下,简体中文的内容正确了。

简体中文

另外,对比一下没有中文翻译时,iOS 10和iOS 11的不同。

Locale 对比

事情还没有完,因为我希望的是用户懂中文时,使用中文;否则使用英文。而现在的方式仅能识别简体中文,当用户使用繁体中文时,还是会显示英文。于是我改了代码,新的代码

private func isChinese() -> Bool {
    let languages = Locale.preferredLanguages
    
    for lang in languages {
        if lang.hasPrefix("zh") {
            return true
        }
    }
    
    return false
}

iOS、watchOS应用用户设置相关问题的答案

最近在写咕唧2,对于iOS应用与配套watchOS应用之间的设置的相关问题有了一些心得。记下来,备查。

问题与答案

1. 创建的Settings.bundle文件放在哪里?

无论iOS还是watchOS的,都放在iOS应用下。

2. iOS应用的设置,为什么watchOS应用不能直接读了?

这是一个历史遗留问题。1代的手表系统,当时还不叫watchOS,上的应用只有一种,叫瞥一眼(glance)。生成机制是所有的一切都在手机上生成,然后传到手表上显示。因此,当然手机应用和手表应用实际上都在手机上运行。因此,直接通过分享组(share group),手表应用就能读取手机应用的设置。

watchOS 2开始,手表应用改在手表上运行了。而手表上的共享组和手机上的共享组之间不自动同步,所以手表和手机之间的设置不能直接读了。

3. iOS应用如何获得watchOS应用的设置?

不能直接读。因为苹果规定,手表应用可以唤醒手机应用,反之则不行。

4. watchOS应用如何获得iOS应用的设置?

通过session的sendMessage方法唤醒手机,然后手机发送设置给手表。

5. 如何设置watchOS应用的设置?

通过手机端的手表应用。这个应用虽然在手机上,但是可以直接读手表应用的设置。并且会写回去。

一张图

iOS与watchOS的设置同步

参考资料