さて,こすりまくったEmojiGachaネタもこれで打ち止めです.今日はステータスバーにメニューを追加する話をする.
NSApplicationDelegateをSwiftUIで使う
ステータスバーにメニューを追加するのはSwiftUIの世界だけでは不可能っぽかったので,AppKitの世界とSwiftUIの世界を繋げる方法について話す.
エントリポイントになっている,Appに準拠するstructに@NSApplicationDelegateAdaptor
でラップしたプロパティを定義する.そのproperty wrapperの引数として,別に定義したNSApplicationDelegate
に準拠する型のメタタイプを渡す.
今回はAppKitだったが,UIKitの世界とつなげたい場合は@UIApplicationDelegateAdaptor
を使えばいい.もちろんNSApplicationDelegate
もUIApplicationDelegate
に変える.
// SomeApp.swift import SwiftUI @main struct SomeApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var body: some Scene { WindowGroup { ContentView() } } }
// AppDelegate.swift import AppKit class AppDelegate: NSObject, NSApplicationDelegate {}
ステータスバーにメニューを追加する
これでAppKitの世界のコードを実行できるようになったので,applicationDidFinishLaunching
でメニューを追加する.(それが定石っぽいので
NSStatusBar.system
でシステムワイドなステータスバーが取れるので,それに対してstatusItem(withLength:)
を呼ぶとメニューが作られる.一つ注意が必要で,statusItem(withLength:)
は新しく作られたNSStatusItem
のインスタンスを返すが,その参照はユーザで持たないといけない.
The receiver does not retain a reference to the status item, so you need to retain it. Otherwise, the object is removed from the status bar when it is deallocated.
最小コードはこんな感じ.
import AppKit class AppDelegate: NSObject, NSApplicationDelegate { var statusItem: NSStatusItem! func applicationDidFinishLaunching(_ notification: Notification) { statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) let image = NSImage(systemSymbolName: "face.smiling", accessibilityDescription: "Some App's status menu")! statusItem.button?.image = image let menu = NSMenu() let quitMenuItem = NSMenuItem.init(title: "Quit SomeAPp", action: #selector(quit), keyEquivalent: "q") menu.addItem(quitMenuItem) statusItem.menu = menu } @objc func quit() { print("quit") } }
EmojiGachaで書いたコードはこんな感じ.ビューが入れられるしインタラクションもちゃんと動くので割と何でもできる.
import AppKit import SwiftUI class AppDelegate: NSObject, NSApplicationDelegate { var statusItem: NSStatusItem! func applicationDidFinishLaunching(_ notification: Notification) { statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) let image = NSImage(systemSymbolName: "face.smiling", accessibilityDescription: "EmojiGacha's status menu")! image.isTemplate = true statusItem.button?.image = image statusItem.menu = { let menu = NSMenu() let customView = MenuView() let hostingView = NSHostingView(rootView: customView) hostingView.setFrameSize(.init(width: 320, height: 100)) let customViewMenuItem = NSMenuItem.init() customViewMenuItem.view = hostingView menu.addItem(customViewMenuItem) menu.addItem(.separator()) let quitMenuItem = NSMenuItem.init(title: "Quit EmojiGacha", action: #selector(quit), keyEquivalent: "q") menu.addItem(quitMenuItem) return menu }() } @objc private func quit() { NSRunningApplication.current.terminate() } } struct MenuView: View { @State var emoji: Emoji = Emoji.random() var body: some View { VStack { Text(emoji.image) .font(.system(size: 40)) Text(emoji.description) .font(.system(size: 15)) .padding([.leading, .trailing]) Button("gacha") { gacha() } } } func gacha() { emoji = Emoji.random() let pboard = NSPasteboard.general pboard.clearContents() pboard.setString(emoji.image, forType: .string) } }