🦋

SwiftUI: MenuBarExtraの中でSettingsLinkを使う際にウインドウが最前面に表示されるようにする

2024/02/05に公開

macOS Sonoma以前は

NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)

を使えばSettingsのウインドウを呼び出せていましたが、Sonoma以降ではSettingsLinkを使わないと設定ウインドウを開くことができなくなり大変不便になりましたね。

なにより、MenuBarExtraでメニューを表示している時はアプリはフォアグラウンドにならないため、そのままSettingsLinkを使ってもウインドウが最前面に表示されないというのがユーザビリティ的にも最悪です。

メニューバー常駐型アプリの開発において、macOS Sonoma対応の難所となっていたのですが、同様のことに悩んでいた有志のOSS(orchetect/SettingsAccess)にヒントがあり、SettingsLinkを押したときにアプリをフォアグラウンドにする方法を実装できました。

ButtonStyleを用いるとボタンの見た目をカスタマイズできますが、PrimitiveButtonStyleを用いればボタンのアクションをカスタマイズできます。しかもSettingsLinkは特殊なButtonであるため、ButtonStyleの適用が可能です。そのため、SettingsLink押下時にNSApp.activate(ignoringOtherApps: true)を発火できるようにすれば良いようです。

PremitiveButtonStyleの実装
@available(macOS 14.0, *)
struct PreActionButtonStyle: PrimitiveButtonStyle {
    let preAction: () -> Void

    init(preAction: @escaping () -> Void) {
        self.preAction = preAction
    }

    func makeBody(configuration: Configuration) -> some View {
        Button(role: configuration.role) {
            preAction()
            configuration.trigger()
        } label: {
            configuration.label
        }
    }
}

@available(macOS 14.0, *)
struct PreActionButtonStyleModifier: ViewModifier {
    let preAction: () -> Void

    init(preAction: @escaping () -> Void) {
        self.preAction = preAction
    }

    func body(content: Content) -> some View {
        content.buttonStyle(PreActionButtonStyle(preAction: preAction))
    }
}

@available(macOS 14.0, *)
extension View {
    func preActionButtonStyle(preAction: @escaping () -> Void) -> some View {
        modifier(PreActionButtonStyleModifier(preAction: preAction))
    }
}
作ったButtonStyleを適用する
if #available(macOS 14.0, *) {
    SettingsLink {
        Text("settings")
    }
    .preActionButtonStyle {
        NSApp.activate(ignoringOtherApps: true)
    }
}

なぜかXcodeがアクティブな時はフォーカスがうまく機能しませんが、それ以外のアプリがアクティブな時は、自作アプリの方にフォーカスが移って設定ウインドウが最前面に表示されます。

Discussion