Write a Digital Clock on macOS

昨天打着打着电话,突然玲就问我,macOS 能不能同时显示两个数字时钟,于是我就找了一圈,DashBoard 上虽然可以显示两个,但是都是模拟时钟,而且玲也没买触摸板,在桌面和 DashBoard 之间切换不那么方便。

就计划帮她写一个,于是“产品经理玲”就发给我了一个 prototype (゚o゚;;

写,都可以写(x

于是结合产品经理玲和我的想法的话,就要自己来画 NSWindow 和它下面NSViewController 对应的 NSView

NSWindow 就自然是全部透明,无边框,这个很好实现~

import Cocoa

class KiminoWindow: NSWindow {
override init(contentRect: NSRect, styleMask style: NSWindow.StyleMask, backing backingStoreType: NSWindow.BackingStoreType, defer flag: Bool) {
super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag)
setupWindow()
}

func setupWindow() {
isOpaque = false
backgroundColor = NSColor.clear
isMovableByWindowBackground = true
styleMask = .borderless
}
}

接下来就是 NSView 了,这里就先用 PaintCode 画一个接近玲想的 prototype

接下来就是把这个 NSView 显示的内容从模版里静态的文字、颜色,变成可以通过代码调整、自动更新的

import Cocoa

class KiminoView: NSView {
/// timezone
var timeZone: String = "Asia/Chongqing" {
didSet {
self.update()
}
}

/// font colour
var clockColor: NSColor = NSColor(red: 0.946, green: 0.829, blue: 0.243, alpha: 1) {
didSet {
self.update()
}
}

/// redraw
func update() {
self.setNeedsDisplay(self.frame)
}

/// draw clock interface
///
/// - Parameter dirtyRect: rect
override func draw(_ dirtyRect: NSRect) {
// rectangle drawing
NSGraphicsContext.saveGraphicsState()
let rectanglePath = NSBezierPath(roundedRect: NSRect(x: 2, y: 0, width: 150, height: 55), xRadius: 10, yRadius: 10)
// clock background
NSColor(red: 0, green: 0, blue: 0, alpha: 0.916).setFill()
rectanglePath.fill()
NSGraphicsContext.restoreGraphicsState()

// digits on the clock
let textRect = NSRect(x: 2, y: 2, width: 150, height: 55)
let formatter = DateFormatter()
// 24 hrs
formatter.dateFormat = "HH:mm"
// specific timezone
formatter.timeZone = TimeZone.init(identifier: timeZone)
let textTextContent = formatter.string(from: Date())
let textStyle = NSMutableParagraphStyle()
textStyle.alignment = .center
let textFontAttributes = [
.font: NSFont(name: "digital-7", size: 63)!,
.foregroundColor: clockColor,
.paragraphStyle: textStyle,
] as [NSAttributedString.Key: Any]
let textTextHeight: CGFloat = textTextContent.boundingRect(with: NSSize(width: textRect.width, height: CGFloat.infinity), options: .usesLineFragmentOrigin, attributes: textFontAttributes).height
let textTextRect: NSRect = NSRect(x: textRect.minX, y: textRect.minY + (textRect.height - textTextHeight) / 2, width: textRect.width, height: textTextHeight)
NSGraphicsContext.saveGraphicsState()
textRect.clip()
textTextContent.draw(in: textTextRect.offsetBy(dx: 0, dy: 0), withAttributes: textFontAttributes)
NSGraphicsContext.restoreGraphicsState()
}
}

接下来就在应用启动时添加第二个 NSWindow 就可以啦~

顺便想到也许会有需要将两个时钟放在最前的需要,于是就再做了个Status Bar的坑~

//
// AppDelegate.swift
// kimino
//
// Created by Cocoa on 8/11/2019.
// Copyright © 2019 Cocoa. All rights reserved.
//

import Cocoa

@NSApplicationMain
class AppDelegate: NSWindowController, NSApplicationDelegate {
var keepFrontMenuItem: NSMenuItem!
var statusBarItem: NSStatusItem!
var clockWindow: Array<NSWindow>!
var isFloating: Bool!

/// Button Action
@objc func keepFrontAction() {
// toggle floating
isFloating = !isFloating
// save user preference
UserDefaults.standard.set(isFloating, forKey: "keepfront")
// do the actual functionality
keepFront()
}

/// Actual keep front functionality
func keepFront() {
// update diplayed status
keepFrontMenuItem.title = "Keep Front: \(isFloating ? "On" : "Off")"

// change window level accordingly
if isFloating {
self.clockWindow.forEach { window in
window.level = .floating
}
} else {
self.clockWindow.forEach { window in
window.level = .normal
}
}
}

/// Quit this application
@objc func quit() {
NSApplication.shared.terminate(self)
}

/// Setup after application finishing launching
///
/// - Parameter aNotification: unused
func applicationDidFinishLaunching(_ aNotification: Notification) {
// put the first clock window into `clockWindow` array
clockWindow = [NSApplication.shared.windows[0]]

// prepare and display the second clock window, London
let storyboard = NSStoryboard(name: "Main", bundle: nil)
let kiminoSecondaryWindowController = storyboard.instantiateInitialController() as! NSWindowController
if let kiminoSecondaryWindow = kiminoSecondaryWindowController.window {
let kiminoSecondaryView = kiminoSecondaryWindow.contentViewController?.view as! KiminoView
// timezone, Europe/London
kiminoSecondaryView.timeZone = "Europe/London"
kiminoSecondaryView.clockColor = NSColor(red: 0, green: 0.628, blue: 1, alpha: 1)
// bring front
kiminoSecondaryWindow.orderFront(self)
// append the second window to `clockWindow` array
clockWindow.append(kiminoSecondaryWindow)
}

// place clock windows to top right
placeWindow(clockWindow[0], 100, 100)
placeWindow(clockWindow[1], 120, 40)

// update every second
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
self.clockWindow.forEach({ window in
let view = window.contentViewController?.view as! KiminoView
view.update()
})
}

// Status Bar
let statusMenu = NSMenu(title: "kimino")
keepFrontMenuItem = NSMenuItem(title: "Keep Front", action: #selector(AppDelegate.keepFrontAction), keyEquivalent: "")
keepFrontMenuItem.keyEquivalentModifierMask = .command
keepFrontMenuItem.keyEquivalent = "L"
let quiteMenuItem = NSMenuItem(title: "Quit", action: #selector(AppDelegate.quit), keyEquivalent: "")
quiteMenuItem.keyEquivalentModifierMask = .command
quiteMenuItem.keyEquivalent = "Q"

statusMenu.addItem(keepFrontMenuItem)
statusMenu.addItem(quiteMenuItem)
statusBarItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
statusBarItem.menu = statusMenu
statusBarItem.button?.image = NSImage(named: "kimino-status-bar")

// restore user defaults
isFloating = UserDefaults.standard.bool(forKey: "keepfront")
keepFront()

print("every day, every night, every dream")
}


/// Place NSWindow to specific postion with respect to the screen
///
/// - Parameters:
/// - window: window to be replaced
/// - right: pixels to the right edge
/// - top: pixels to the top edge
func placeWindow(_ window: NSWindow, _ right: CGFloat, _ top: CGFloat) {
if let screen = window.screen {
let screenRect = screen.visibleFrame
let newOriginX = screenRect.maxX - window.frame.width - right
let newOriginY = screenRect.maxY - window.frame.height - top
window.setFrameOrigin(NSPoint(x: newOriginX, y: newOriginY))
}
}
}

其实最后效果还是不错的(*^3^)

Leave a Reply

Your email address will not be published. Required fields are marked *