天天看點

SwiftUI與macOS開發:menuBar/statusBar詳解

macOS的menuBar功能非常強大,我們在開發macOS應用的時候,經常需要利用menuBar實作功能。然而網上關于menuBar開發的文檔卻甚少,更别提用上SwiftUI開發的。這篇文章從以下幾部分講解用SwiftUI開發menuBar/statusBar的方法,希望對大家有幫助:

一、在statusBarItem彈出Popover

二、更改Popover背景色

三、在statusBar上控制主視窗的開關

四、在statusBar上控制statusBar的顯隐/開關

一、在statusBarItem彈出Popover

statusBar彈出menu是比較簡單的,但是如果我們要做一些定制,就需要用到Popover了

建立Popover

在AppDelegate檔案中,聲明一個NSPopover變量(下面的statusBarItem之後會用到)

var popover: NSPopover! var statusBarItem: NSStatusItem!

           

随後,在applicationDidFinishLaunching中, ContentView 初始化之後,加上

// Create the popover 
let popover = NSPopover() 
popover.contentSize = NSSize(width: 400, height: 400) 
popover.behavior = .transient //代表使用者點選其他區域時popover自動消失 popover.contentViewController = NSHostingController(rootView: contentView) self.popover = popover
           

此時我們的AppDelegate檔案應該長這樣:

import Cocoa
import SwiftUI

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    var popover: NSPopover!
    
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        // Create the SwiftUI view that provides the window contents.
        let contentView = ContentView()

        // Create the popover
        let popover = NSPopover()
        popover.contentSize = NSSize(width: 400, height: 500)
        popover.behavior = .transient
        popover.contentViewController = NSHostingController(rootView: contentView)
        self.popover = popover
    }
    
}
           

這樣我們即建立了一個Popover視圖以及它的控制器,這個Popover的視圖内容是我們的ContentView.swift裡的内容。這樣我們就可以在ContentView裡用SwiftUI自由地定制我們的Popover。接下來我們要做的是,建立一個menuBarItem,當使用者點選menuBarItem的按鈕時,彈出我們已經寫好的Popover。

self.statusBarItem = NSStatusBar.system.statusItem(withLength: CGFloat(NSStatusItem.variableLength))
if let button = self.statusBarItem.button {
     button.image = NSImage(named: "Icon")
     button.action = #selector(togglePopover(_:))
}
           

還記得我們一開始聲明了

var statusBarItem: NSStatusItem!
           

現在我們需要建立它,同時給它在menuBar設定個圖示,綁定點選事件為togglePopover

self.statusBarItem = NSStatusBar.system.statusItem(withLength: CGFloat(NSStatusItem.variableLength))
if let button = self.statusBarItem.button {
     statusBarItem.button?.title = "⏳"
     button.action = #selector(togglePopover(_:))
}
           

綁定togglePopover是為了在使用者點選時彈出Popover

// Create the status item
@objc func togglePopover(_ sender: AnyObject?) {
     if let button = self.statusBarItem.button {
          if self.popover.isShown {
               self.popover.performClose(sender)
          } else {
               self.popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
          }
     }
}
           

最終,我們的AppDelegate檔案長這樣

import Cocoa
import SwiftUI

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    var popover: NSPopover!
    var statusBarItem: NSStatusItem!
    
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        // Create the SwiftUI view that provides the window contents.
        let contentView = ContentView()

        // Create the popover
        let popover = NSPopover()
        popover.contentSize = NSSize(width: 400, height: 500)
        popover.behavior = .transient
        popover.contentViewController = NSHostingController(rootView: contentView)
        self.popover = popover
        
        // Create the status item
        self.statusBarItem = NSStatusBar.system.statusItem(withLength: CGFloat(NSStatusItem.variableLength))
        
        if let button = self.statusBarItem.button {
            statusBarItem.button?.title = "⏳"
            button.action = #selector(togglePopover(_:))
        }
    }
    
    @objc func togglePopover(_ sender: AnyObject?) {
        if let button = self.statusBarItem.button {
            if self.popover.isShown {
                self.popover.performClose(sender)
            } else {
                self.popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
            }
        }
    }
    
}
           

二、更改Popover背景色

我們建立出來的Popover預設的背景色是透明的,很容易與我們的UI不搭配。如果需要定制背景顔色,目前我沒有發現直接用SwiftUI實作的方法。我們需要去拓展NSPopover類,在建立執行個體的時候指定背景色。

首先,拓展NSPopover類:

import Cocoa
import SwiftUI
extension NSPopover {
    
    private struct Keys {
        static var backgroundViewKey = "backgroundKey"
    }
    
    private var backgroundView: NSView {
        let bgView = objc_getAssociatedObject(self, &Keys.backgroundViewKey) as? NSView
        if let view = bgView {
            return view
        }
        
        let view = NSView()
        objc_setAssociatedObject(self, &Keys.backgroundViewKey, view, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        NotificationCenter.default.addObserver(self, selector: #selector(popoverWillOpen(_:)), name: NSPopover.willShowNotification, object: nil)
        return view
    }
    
    @objc private func popoverWillOpen(_ notification: Notification) {
        if backgroundView.superview == nil {
            if let contentView = contentViewController?.view, let frameView = contentView.superview {
                frameView.wantsLayer = true
                backgroundView.frame = NSInsetRect(frameView.frame, 1, 1)
                backgroundView.autoresizingMask = [.width, .height]
                frameView.addSubview(backgroundView, positioned: .below, relativeTo: contentView)
            }
        }
    }
    
    var backgroundColor: NSColor? {
        get {
            if let bgColor = backgroundView.layer?.backgroundColor {
                return NSColor(cgColor: bgColor)
            }
            return nil
        }
        set {
            backgroundView.wantsLayer = true
            backgroundView.layer?.backgroundColor = newValue?.cgColor
        }
    }
    
}
           

在AppDelegate初始化時,增加background參數

let popover = NSPopover()
        popover.backgroundColor = NSColor.white   //設定popover顔色,對應extension裡對popover的改寫
        popover.contentSize = NSSize(width: 350, height: 400)
           

搞定!

SwiftUI與macOS開發:menuBar/statusBar詳解

三、在statusBar增加控制主視窗開關和插件開關

現在statusBarItem的視圖檔案中加入按鈕

HStack{
                Text("Time Capsule").font(Font.system(.headline)).bold()
                Spacer()
                MenuButton(label: Image("moreBlack").resizable().frame(width:20,height:20)) {
                    Button(action: {
                      showWindow()
                    }) {
                        Text("打開主視窗")
                    }
                    Button(action: {
                        hideStatusBar()
                    }) {
                        Text("退出")
                    }
                }.menuButtonStyle(BorderlessButtonMenuButtonStyle())
                
            }
            .padding([.top, .trailing], 18).padding(.leading,30)
           

接下來我們要補充兩個按鈕點選事件showWindow()和hideStatusBar()的方法

func showWindow() {
    NSApp.unhide(nil)

func hideStatusBar() {
    let appDelegate = NSApplication.shared.delegate as! AppDelegate
    appDelegate.statusBarItem.isVisible = false
}
           

NSApp.unhide方法可以直接顯示我們的主程式,hide則可以隐藏主程式

statusBarItem.isVisible設為false則可以直接讓我們的插件消失