通向荣誉的路上,并不铺满鲜花。
前言
用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 Scene和View 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
- 文中如有错误,欢迎在评论区指正,如果这篇文章帮到了你,欢迎点赞和关注😊
- 本文首发于掘金,未经许可禁止转载💌
评论区