侧边栏壁纸
博主头像
神奇的程序员

今天的努力只为未来

  • 累计撰写 167 篇文章
  • 累计创建 25 个标签
  • 累计收到 215 条评论

目 录CONTENT

文章目录

Swift实现MacOS菜单栏应用的开发

神奇的程序员
2020-03-25 / 0 评论 / 2 点赞 / 419 阅读 / 7,540 字 正在检测是否收录...

通向荣誉的路上,并不铺满鲜花。

前言

用Java爬到我房间电表电量使用情况后,封装了一个接口,用于客户端的调用😝
在日常生活中,我用Mac的时间是最多的,如果将爬到的数据,展示在Mac的顶栏上,是一件很美好的事情😌
作为一个对Swift一无所知的我,花了两天时间,用Swift语言开发了一个Mac应用,接下来就跟大家分享下这个应用的开发过程,欢迎各为感兴趣的开发者阅读本文。

先跟大家看下最终实现的效果:

环境搭建

Swift: 4.2.1

Xcode: 10.1

MacOS: 10.14.2

库管理工具: Carthage

Alamofire: 4.0 (网络请求库)

SwiftyJSON: 3.0 (Json解析库)

创建项目

  • 打开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爬取电表电量使用情况

参考文献: https://www.smslit.top/2018/06/29/macOS-dev-basic-NSPopover/

本篇文章对应的代码地址: home-battery-tool

  • 文中如有错误,欢迎在评论区指正,如果这篇文章帮到了你,欢迎点赞和关注😊
  • 本文首发于掘金,未经许可禁止转载💌
2

评论区