概要
- Macの右上のメニューバーにアイコンを表示するアプリを作成します。
- また最終的に、そのアイコン上に画像ファイルをドラッグ・アンド・ドロップできるよう実装を行います。
- 上のバーの名称は以下メニューバーと呼ぶこととします。

GitHub
基本的な実装
- 以下の記事の通り実装を行います。
class AppDelegate: NSObject, NSApplicationDelegate {
let statusItem = NSStatusBar.system.statusItem(withLength:NSStatusItem.squareLength)
func applicationDidFinishLaunching(_ aNotification: Notification) {
if let statusBarButton = statusItem.button {
statusBarButton = NSImage(named:NSImage.Name("StatusBarButtonImage"))
}
constructMenu()
}
@objc func printQuote(_ sender: Any?) {
let quoteText = "Never put off until tomorrow what you can do the day after tomorrow."
let quoteAuthor = "Mark Twain"
print("\(quoteText) — \(quoteAuthor)")
}
func constructMenu() {
let menu = NSMenu()
menu.addItem(NSMenuItem(title: "Print Quote", action: #selector(AppDelegate.printQuote(_:)), keyEquivalent: "P")) // 大文字なので、Commant + "Shitf + p"となる
menu.addItem(NSMenuItem.separator())
menu.addItem(NSMenuItem(title: "Quit Quotes", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"))
statusItem.menu = menu
}
}
- これより以下は、記事の内容外の実装を紹介します。
メニュー項目にサブメニューを持たせる
- 以下のようなメニューを入れ子にすることを考えます。
- クラスとしては、下記のように入れ子になっています
-
submenu
として追加するのがポイントです。
func constructMenu() {
let menu = NSMenu()
menu.addItem(NSMenuItem(title: "Parent_01", action: #selector(AppDelegate.printQuote(_:)), keyEquivalent: ""))
menu.addItem(NSMenuItem(title: "Parent_02", action: #selector(NSApplication.terminate(_:)), keyEquivalent: ""))
menu.addItem(NSMenuItem.separator())
// サブメニューを持つNSMenuItemを作成する
let paren_03 = NSMenuItem(title: "Parent_03", action: #selector(AppDelegate.printQuote(_:)), keyEquivalent: "")
let menuForParent = NSMenu()
menuForParent.addItem(NSMenuItem(title: "Children_01", action: #selector(AppDelegate.printQuote(_:)), keyEquivalent: ""))
menuForParent.addItem(NSMenuItem.separator())
menuForParent.addItem(NSMenuItem(title: "Children_02", action: #selector(NSApplication.terminate(_:)), keyEquivalent: ""))
paren_03.submenu = menuForParent
menu.addItem(paren_03)
statusItem.menu = menu
}
メニューアイコンにファイルをドラッグ・アンド・ドロップする
概要
- 以下のようにメニューアイコンに画像ファイルをドラッグし、アプリ側で認識するよう実装を行います。
- 以下の方針を参考に実装します。
- NSDraggingDestinationに適合しているのは下記のクラスです。
Conforming Types
NSCollectionView, NSView
- またNSButton、もっというと今回はNSStatusBarButtonは
NSView
を継承しています。 - よって結論としては、NSStatusBarButtonのNSDraggingDestinationのメソッドをoverrideしてやれば良いですね。
- 当初はカスタムクラスを作ろうとしましたが、
NSStatusItem.NSStatusBarButtonがget only
なので、extensionで動作をoverrideを行いました。
実装
- AppDelegate外でアイコンを変更するため、publicメソッド
changeMenubarIconImage
を用意しておく。- Notificationを使うほうが疎でいいかも。
class AppDelegate: NSObject, NSApplicationDelegate {
public let statusItem = NSStatusBar.system.draggableStatusItem(withLength: NSStatusItem.squareLength)
public func changeMenubarIconImage(isDragging isDragIcon: Bool) {
if let statusBarButton = statusItem.button {
if isDragIcon {
statusBarButton.image = NSImage(named:NSImage.Name("StatusBarButtonImageDragged"))
} else {
statusBarButton.image = NSImage(named:NSImage.Name("StatusBarButtonImage"))
}
}
}
public func receiveImageDraggedOnMenubarIcon(for urls: [URL]) {
print("🍎URL Detected!")
for url in urls {
print(url)
}
}
...
- 特定のファイル形式を受け取るための
NSStatusItem
を返すメソッドを用意しておきます。- この辺は「ファイルをNSViewにドラッグしてパスを取得する (Swift) - Qiita」と同じですね。
-
registerForDraggedTypes
でドラッグできるアイテムのタイプを指定します。
extension NSStatusBar {
func draggableStatusItem(withLength length: CGFloat) -> NSStatusItem {
let statusItem = self.statusItem(withLength: length)
if let button = statusItem.button {
button.image = NSImage(named:NSImage.Name("StatusBarButtonImage"))
button.registerForDraggedTypes([.fileURL, .tiff, .png, .URL, .string])
}
return statusItem
}
}
- こちらも「ファイルをNSViewにドラッグしてパスを取得する (Swift) - Qiita」と同様です。
- 要するに、ファイルのドラッグ状態によって、メニューバーのアイコンを切り替えることをしています。
extension NSStatusBarButton {
// MARK: - Helper Methods
private func filteringOptions() -> [NSPasteboard.ReadingOptionKey : Any] {
return [.urlReadingContentsConformToTypes : NSImage.imageTypes]
}
private func shouldAllowDrag(_ draggingInfo: NSDraggingInfo) -> Bool {
var canAccept = false
let pasteBoard = draggingInfo.draggingPasteboard
if pasteBoard.canReadObject(forClasses: [NSURL.self], options: filteringOptions()) ||
pasteBoard.canReadObject(forClasses: [NSImage.self], options: filteringOptions()) {
canAccept = true
}
return canAccept
}
private func setDraggingInfo(isDragging: Bool) {
if let appDelegate = NSApplication.shared.delegate as? AppDelegate {
appDelegate.changeMenubarIconImage(isDragging: isDragging)
}
}
// MARK:- NSDraggingDestination Methods
open override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
print("draggingEntered")
let allow = shouldAllowDrag(sender)
setDraggingInfo(isDragging: allow)
return allow ? .copy : NSDragOperation()
}
open override func draggingExited(_ sender: NSDraggingInfo?) {
setDraggingInfo(isDragging: false)
}
open override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
let pasteBoard = sender.draggingPasteboard
// 今回はfileURLがドラッグされた場合のみ実装
if let urls = pasteBoard.readObjects(forClasses: [NSURL.self], options: filteringOptions()) as? [URL],
urls.count > 0,
urls[0].isFileURL {
if let appDelegate = NSApplication.shared.delegate as? AppDelegate {
appDelegate.receiveImageDraggedOnMenubarIcon(for: urls)
}
return true
}
return false
}
open override func draggingEnded(_ sender: NSDraggingInfo) {
setDraggingInfo(isDragging: false)
print("draggingEnded")
}
}
余談
-
registerForDraggedTypes with NSStatusBarButton?
-
NSStatusBarButton
にドラッグ・アンド・ドロップ可能なNSView
をaddSubview
すればと回答 - 試しましたが、アイコンの外観は変更はできましたが、ドラッグのアクションを受け取るのが不可でした。
-
Responder Chain
あたりを考えるとうまくいくのだろうか…?
-
-
メニューバーのアイコンを非表示にする
- isVisibleを設定等から設定できるようにすれば良さそうです。
statusItem.isVisible = false // で非表示になる
ステータスメニューを実装する際にはそれを非表示にすることができるオプションを環境設定などに用意しておくべきです。
その他参考した記事
- カスタムなウィンドウを表示した状態でNSStatusItemをハイライトする - a.out
-
Mac - メニューバーのアイコンを並び替え・削除
- Commandを押しながらでアイコンの順番を入れ替えれます。
- ターゲットのアイコン4