Swift實現MacOS菜單欄應用的開發

通向榮譽的路上,並不鋪滿鮮花。前端

前言

用Java爬到我房間電錶電量使用狀況後,封裝了一個接口,用於客戶端的調用😝 在平常生活中,我用Mac的時間是最多的,若是將爬到的數據,展現在Mac的頂欄上,是一件很美好的事情😌 做爲一個對Swift一無所知的我,花了兩天時間,用Swift語言開發了一個Mac應用,接下來就跟你們分享下這個應用的開發過程,歡迎各爲感興趣的開發者閱讀本文。git

先跟你們看下最終實現的效果: github

環境搭建

Swift: 4.2.1npm

Xcode: 10.1json

MacOS: 10.14.2swift

庫管理工具: Carthageapi

Alamofire: 4.0 (網絡請求庫)xcode

SwiftyJSON: 3.0 (Json解析庫)bash

建立項目

  • 打開Xcode,咱們看到的界面如圖所示
    • 左邊爲建立項目部分,右邊爲最近打開的項目
    • 點擊圖中用紅款勾選的地方
  • 如圖所示,按圖中標明的序號,分別進行點擊。
  • 如圖所示,分別填寫項目相關信息
  • 選擇項目建立位置
  • 出現如圖所示的頁面後,項目建立成功

配置項目爲一個菜單欄應用

此時項目爲一個空白項目,點運行後會出現一個空的window窗口,同時dock上出現應用圖標,這並非咱們要的,因此要添加配置來移除他們。 網絡

  • 如圖所示,在info下添加添加一個配置項
  • Key選擇Application is agent (UI Element),Value選擇Yes
  • 此時再次運行程序,咱們發現dock欄已經不顯示程序圖標,可是window窗口依然在
  • 根據圖中所示,刪除Window Controller SceneView Controller Scene
  • 此時,咱們在雲心應用程序,發現窗口也沒了。

在菜單欄建立圖標

  • 如圖所示,點擊Assets.xcassets文件,新建一個Image Set
  • 如圖所示,將其命名爲StatusIcon,將本身中意的圖片製做成3種規格,託入對應的位置。
  • 拖入圖標後,執行圖片中的步驟,讓圖標適配系統的黑暗模式
  • 打開AppDelegate.swift文件,添加以下代碼
// 建立狀態欄按鈕
    let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
複製代碼
// applicationDidFinishLaunching生命週期
    if let button = statusItem.button {
         button.image = NSImage(named: "StatusIcon")
    }
複製代碼
  • 運行程序,咱們發現菜單欄有了咱們剛纔設置的圖標,此時點擊後什麼都不會發生。

添加Popover容器

  • 在項目目錄下,新建一個Cocoa Class,命名爲PopoverViewController,此文件爲點擊時打開的彈層頁面
  • 添加View Controller容器
  • 如圖所示,對上一步添加的容器進行修改
  • 打開PopoverViewController.swift文件,在末尾添加以下代碼
extension PopoverViewController {
    static func freshController() -> PopoverViewController {
        //獲取對Main.storyboard的引用
        let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil)
        // 爲PopoverViewController建立一個標識符
        let identifier = NSStoryboard.SceneIdentifier("PopoverViewController")
        // 實例化PopoverViewController並返回
        guard let viewcontroller = storyboard.instantiateController(withIdentifier: identifier) as? PopoverViewController else {
            fatalError("Something Wrong with Main.storyboard")
        }
        return viewcontroller
    }
}

複製代碼
  • 打開AppDelegate.swift文件在class內添加以下代碼
// 聲明一個Popover
let popover = NSPopover()
複製代碼

建立顯示/隱藏Popover的函數

  • 在AppDelegate.swift文件的class內添加以下代碼
// 控制Popover狀態
    @objc func togglePopover(_ sender: AnyObject) {
        if popover.isShown {
            closePopover(sender)
        } else {
            showPopover(sender)
        }
    }
    // 顯示Popover
    @objc func showPopover(_ sender: AnyObject) {
        if let button = statusItem.button {
            popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
        }
        
    }
    // 隱藏Popover
    @objc func closePopover(_ sender: AnyObject) {
        popover.performClose(sender)
    }
複製代碼
  • 在AppDelegate.swift文件的applicationDidFinishLaunching函數中添加以下代碼
if let button = statusItem.button {
            button.image = NSImage(named: "StatusIcon")
            button.action = #selector(togglePopover(_:))
    }
    popover.contentViewController =            PopoverViewController.freshController()
複製代碼
  • 此時,運行項目,點擊菜單欄的應用圖標,會顯示彈層,再次點擊彈層會消失

優化Popover

執行完上個步驟後,咱們會發現彈層只會在點擊時關閉或者消失,接下來咱們來優化下,失去焦點時,也讓它隱藏

  • 新建一個EventMonitor.swift文件,用於事件監聽,此文件代碼以下
import Cocoa

public class EventMonitor {
    private var monitor: Any?
    private let mask: NSEvent.EventTypeMask
    private let handler: (NSEvent?) -> Void
    
    public init(mask: NSEvent.EventTypeMask, handler: @escaping (NSEvent?) -> Void) {
        self.mask = mask
        self.handler = handler
    }
    
    deinit {
        stop()
    }
    
    public func start() { //開啓監視器
        monitor = NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler)
    }
    
    public func stop() { //關閉監視器
        if monitor != nil {
            NSEvent.removeMonitor(monitor!)
            monitor = nil
        }
    }
}

複製代碼
  • 打開AppDelegate.swift在class中聲明這個監視器
// 聲明監視器
var eventMonitor: EventMonitor?
複製代碼
  • 在applicationDidFinishLaunching函數中添加
eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown]) { [weak self] event in
  if let strongSelf = self, strongSelf.popover.isShown {
    strongSelf.closePopover(event!)
  }
}
複製代碼
  • 修改showPopover和closePopover函數
// 顯示Popover
    @objc func showPopover(_ sender: AnyObject) {
        if let button = statusItem.button {
            popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
        }
        eventMonitor?.start()
        
    }
    // 隱藏Popover
    @objc func closePopover(_ sender: AnyObject) {
        popover.performClose(sender)
        eventMonitor?.stop()
    }
複製代碼
  • 運行項目,咱們發現失去焦點後,彈層隱藏掉了。

添加右鍵菜單

  • 如圖所示,添加menu組件進來
  • 將 Menu 與 AppDelegate.swift 創建聯繫
  • 刪除多餘項,添加退出圖標和條目
  • 修改AppDelegate.swift文件,添加Handler來接管togglePopover
// 接管togglePopover
    @objc func mouseClickHandler() {
        if let event = NSApp.currentEvent {
            switch event.type {
            case .leftMouseUp:
                togglePopover(popover)
            default:
                statusItem.menu = Menu
                statusItem.button?.performClick(nil)
            }
        }
    }
複製代碼
  • statusItem.button添加以下代碼
// 點擊事件
 button.action = #selector(mouseClickHandler)
 button.sendAction(on: [.leftMouseUp, .rightMouseUp])
複製代碼
  • 在AppDelegate.swift的末尾添加
extension AppDelegate: NSMenuDelegate {
    // 爲了保證按鈕的單擊事件設置有效,menu要去除
    func menuDidClose(_ menu: NSMenu) {
        self.statusItem.menu = nil
    }
}

複製代碼
  • 在applicationDidFinishLaunching內添加
// 修復按鈕單擊事件無效問題
Menu.delegate = self
複製代碼
  • 此時右鍵,咱們發現已成功添加

實現退出功能

  • 如圖所示,將推出函數關聯至AppDelegate文件下
  • 完善退出app函數
// 關閉App
    @IBAction func Quit(_ sender: Any) {
        NSApplication.shared.terminate(self)
    }
複製代碼
  • 再次運行後,右鍵,點擊推出,便可關閉應用

編寫Popover頁面

執行完上述步驟後,咱們建立了一個空的Popover,接下來咱們往Popover添加內容,調用接口,顯示咱們房間電錶電量使用狀況。

佈局頁面

  • 如圖所示,添加Text Field和label組件,拖拽至PopoverViewController中,並與PopoverViewController.swift進行關聯
  • 調整拖出來的控件大小,搞成如圖所示的樣子

安裝Cartfile庫管理工具

Cartfile是一個優秀的庫管理工具,至關於咱們前端的npm。

  • 點擊Cartfile下載地址,進入Cartfile的github倉庫的下載頁面,選擇pkg文件下載,而後安裝。

安裝網絡請求庫和Json解析庫

Alamofire,爲一個優秀的網絡請求庫,他封裝了各類http請求。

SwiftyJSON,爲一個優秀的json解析庫

咱們能夠經過Cartfile來獲取他們

  • 在咱們的項目根目錄建立Cartfile文件,並添加以下內容
github "Alamofire/Alamofire" ~> 4.0
github "SwiftyJSON/SwiftyJSON" ~> 3.0
複製代碼
  • 打開終端,進入到咱們項目的根目錄,執行以下命令
carthage update --platform macOS
複製代碼
  • 執行完畢後,咱們發現,項目的根目錄下多了Carthage文件夾
  • 此時咱們打開,xcode,打開如圖所示的頁面
  • 如圖所示,選擇咱們剛纔Carthage文件夾下的,build->Mac->framework文件
  • 添加成功後,如圖所示

開啓網絡訪問

Xcode默認不容許http請求,按照如圖所示的操做進行便可。

調用接口渲染頁面

在PopoverViewController.swift文件中添加以下代碼

import Cocoa
import Alamofire
import SwiftyJSON

class PopoverViewController: NSViewController {
    // 今日用電
    @IBOutlet weak var electricityToday: NSTextField!
    // 本月已用
    @IBOutlet weak var currentMonthBatteryTotal: NSTextField!
    // 剩餘電量
    @IBOutlet weak var remainingBattery: NSTextField!
    // 統計時間
    @IBOutlet weak var time: NSTextField!

    private var timer: Timer?
    // 定時器記數: 每20分鐘執行一次,3輪爲1小時
    private var timeCount = 3
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // 獲取並設置頁面數據
        setPageData()
        // 啓動定時器
        loop()
    }
    
    // 獲取並設置數據
    func setPageData(){
        // 發起post請求
        Alamofire.request("https://www.xxx.com",method: .post,parameters: ["userName":"xxx","password":"xxx"],encoding: JSONEncoding.default).responseJSON { (response) in
            switch response.result {
            // 請求成功
            case .success(let resData):
                // 將返回的數據轉爲JSON對象
                let jsonData = JSON.init(resData as Any)
                // 變量賦值
                self.electricityToday.stringValue = jsonData["data"]["electricityToday"].string!
                self.currentMonthBatteryTotal.stringValue = jsonData["data"]["currentMonthBatteryTotal"].string!
                self.remainingBattery.stringValue = jsonData["data"]["remainingBattery"].string!
                self.time.stringValue = jsonData["data"]["time"].string!
                break
            case .failure(let error):
                print("接口調用失敗")
                print(error);
                break
            }
        }
    }
    
    // GCD 方式的定時器,循環
    func loop() {
        print("\(Date()): 定時器初始化")
        // timeInterval: 隔多少秒執行一次
        timer = Timer(timeInterval: 1200, repeats: true, block: { timer in
            self.loopFireHandler(timer)
        })
        // 添加定時器
        RunLoop.main.add(timer!, forMode: .common)
    }
    // 定時器須要執行的內容
    @objc private func loopFireHandler(_ timer: Timer?) -> Void {
        // 定時器執行結束結束
        if self.timeCount <= 0 {
            print("\(Date()): 執行完1輪,開始下一輪")
            self.timeCount = 3
            return
        }
        // 獲取並設置頁面數據
        setPageData()
        // 執行完分鐘
        self.timeCount -= 1
      
    }

}


extension PopoverViewController {
    static func freshController() -> PopoverViewController {
        //獲取對Main.storyboard的引用
        let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil)
        // 爲PopoverViewController建立一個標識符
        let identifier = NSStoryboard.SceneIdentifier("PopoverViewController")
        // 實例化PopoverViewController並返回
        guard let viewcontroller = storyboard.instantiateController(withIdentifier: identifier) as? PopoverViewController else {
            fatalError("Something Wrong with Main.storyboard")
        }
        return viewcontroller
    }
}


複製代碼

寫在最後

如何爬取你房間內電錶使用狀況,請移步這篇文章:Java爬取電錶電量使用狀況

本篇文章對應的代碼地址: home-battery-tool

  • 文中若有錯誤,歡迎在評論區指正,若是這篇文章幫到了你,歡迎點贊和關注😊
  • 本文首發於掘金,未經許可禁止轉載💌
相關文章
相關標籤/搜索