SwiftUI版通知栏应用开发(2) ——Combine 小试牛刀创建定时器

2,879 阅读2分钟

搭好结构后,我们开始写第一个功能了:

通过定时器实时显示当前时间

要实现这个功能,主要的思想在于:通过逻辑,影响 UI 的显示效果,这个符合我们今天的主角:Combine

如果要满足使用 Combine,需要具备两要素:

  1. ViewModel
  2. 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,所以我利用 NSHostingViewContentViewSwiftUI 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)

好了,我们运行看看效果:

times

整个代码:

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

未完待续