SwiftUI版通知栏应用开发(2) ——Combine 小试牛刀创建定时器
搭好结构后,我们开始写第一个功能了:
通过定时器实时显示当前时间
要实现这个功能,主要的思想在于:通过逻辑,影响 UI 的显示效果,这个符合我们今天的主角:Combine
。
如果要满足使用 Combine
,需要具备两要素:
ViewModel
SwiftUI View
ViewModel
在创建 ViewModel
之前,我们先使用一个 Package,那就是 SwiftDate
:
SwiftDate is the definitive toolchain to manipulate and display dates and time zones on all Apple platform and even on Linux and Swift Server Side frameworks like Vapor or Kitura.
使用它,主要是因为未来可以做一个多语言版本。
今天主要是展示中文格式:
self.context = Date().toFormat("yyyy年MM月dd日 HH:mm:ss")复制代码
下一步是使用一个定时器,每隔一秒更新一次self.context
:
cancellable = Timer.publish(every: 1.0, tolerance: nil, on: .main, in: .common, options: nil) .autoconnect() .sink { _ in self.context = Date().toFormat("yyyy年MM月dd日 HH:mm:ss") print(self.context) }复制代码
剩下的就是把 context
利用 Combine
同步到 SwiftUI
View
上。
@Published var context = "fanlymenu"复制代码
我们创建 TimerViewModel
集成 ObservableObject
,整个代码如下:
import Foundation import Combine import SwiftDate final class TimerViewModel: ObservableObject { // 通知栏显示内容,随着业务的发展,不断丰富 // 这一阶段实现动态时间显示 @Published var context = "fanlymenu" @Published var isTimerRunning = false private var cancellable: AnyCancellable? func startTimer() { isTimerRunning = true cancellable = Timer.publish(every: 1.0, tolerance: nil, on: .main, in: .common, options: nil) .autoconnect() .sink { _ in self.context = Date().toFormat("yyyy年MM月dd日 HH:mm:ss") print(self.context) } } func stopTimer() { isTimerRunning = false cancellable?.cancel() } func resetTimer() { context = "" } }复制代码
SwiftUI View
有了 ViewModel,我们就可以在昨天的代码里调用了,先定义 timerViewModel
变量:
@ObservedObject private var timerViewModel = TimerViewModel()复制代码
然后注入到 View
中:
let menuView = ContentView(timerViewModel: timerViewModel)复制代码
具体 ContentView
:
import SwiftUI struct ContentView: View { @ObservedObject private var timerViewModel: TimerViewModel init(timerViewModel: TimerViewModel) { self.timerViewModel = timerViewModel } var body: some View { Text("\(self.timerViewModel.context)") .padding() } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView(timerViewModel: TimerViewModel()) } }复制代码
这样就把 ViewModel
定义的 context
注入到 Text
SwiftUI View
里了。
尝试使用
回到 applicationDidFinishLaunching
,我们把 statusItem
长度设为:200
statusItem = NSStatusBar.system.statusItem(withLength: CGFloat(200))复制代码
因为在 statusItem
包裹的 Button
不是 SwiftUI View
,所以我利用 NSHostingView
把 ContentView
从 SwiftUI View
转为 NSView
:
let view = NSHostingView(rootView: ContentView(timerViewModel: timerViewModel))复制代码
最后在 Button
加上这个子 View
:
let view = NSHostingView(rootView: ContentView(timerViewModel: timerViewModel)) view.frame = CGRect(x: 0, y: 0, width: 200, height: 20) MenuButton.addSubview(view)复制代码
好了,我们运行看看效果:
整个代码:
class AppDelegate: NSObject, NSApplicationDelegate { // Status Bar Item... var statusItem: NSStatusItem? // PopOver... var popOver = NSPopover() @ObservedObject private var timerViewModel = TimerViewModel() func applicationDidFinishLaunching(_ notification: Notification) { // let timerViewModel = TimerViewModel() // Menu View... let menuView = ContentView(timerViewModel: timerViewModel) // Creating PopOver... popOver.behavior = .transient popOver.animates = true // Setting Empty View Controller... // And Setting View as SwiftUI View... // with the help of Hosting Controller... popOver.contentViewController = NSViewController() popOver.contentViewController?.view = NSHostingView(rootView: menuView) // also Making View as Main View... popOver.contentViewController?.view.window?.makeKey() // Creating Status Bar Button... statusItem = NSStatusBar.system.statusItem(withLength: CGFloat(200)) // Safe Check if status Button is Available or not... if let MenuButton = statusItem?.button { // MenuButton.image = NSImage(systemSymbolName: "icloud.and.arrow.up.fill", accessibilityDescription: nil) // MenuButton.imagePosition = NSControl.ImagePosition.imageLeft // MenuButton.title = "" let view = NSHostingView(rootView: ContentView(timerViewModel: timerViewModel)) view.frame = CGRect(x: 0, y: 0, width: 200, height: 20) MenuButton.addSubview(view) MenuButton.action = #selector(MenuButtonToggle) } timerViewModel.startTimer() } // Button Action @objc func MenuButtonToggle(sender: AnyObject) { // For Safer Sice... if popOver.isShown { popOver.performClose(sender) } else { // Showing PopOver if let menuButton = statusItem?.button { // Top Get Button Location For Popover Arrow... self.popOver.show(relativeTo: menuButton.bounds, of: menuButton, preferredEdge: NSRectEdge.maxY) } } } }复制代码
总结
今天使用 Combine
小试牛刀,把日期实时显示在菜单栏上。
下一步就是封装成单独的类了,以及 Popover
。
未完待续
作者:叶梅树
链接:https://juejin.cn/post/7023746494553391111