index rss mastodon twitter github linkedin email
Álvaro Ramírez
sponsor

Álvaro Ramírez

02 August 2020 SwiftUI macOS desk clock

everclock.gif

For time display, I've gone back and forth between an always-displayed macOS's menu bar to an auto-hide menu bar, and letting Emacs display the time. Neither felt great nor settled.

With some tweaks, Paul Hudson's How to use a timer with SwiftUI, led me to build a simple desk clock. Ok, let's not get fancy. It's really just an always-on-top floating window, showing a swiftUI label, but hey I like the minimalist feel ;)

Let's see if it sticks around or it gets in the way… Either way, here's standalone snippet. Run with swift deskclock.swift.

import Cocoa
import SwiftUI

let application = NSApplication.shared
let appDelegate = AppDelegate()
NSApp.setActivationPolicy(.regular)
application.delegate = appDelegate
application.mainMenu = NSMenu.makeMenu()
application.run()

struct ClockView: View {
  @State var time = "--:--"

  let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

  var body: some View {
    GeometryReader { geometry in

      VStack {
        Text(time)
          .onReceive(timer) { input in
            let formatter = DateFormatter()
            formatter.dateFormat = "HH:mm"
            time = formatter.string(from: input)
          }
          .font(.system(size: 40))
          .padding()
      }.frame(width: geometry.size.width, height: geometry.size.height)
        .background(Color.black)
        .cornerRadius(10)
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
  }
}

extension NSWindow {
  static func makeWindow() -> NSWindow {
    let window = NSWindow(
      contentRect: NSRect.makeDefault(),
      styleMask: [.closable, .miniaturizable, .resizable, .fullSizeContentView],
      backing: .buffered, defer: false)
    window.level = .floating
    window.setFrameAutosaveName("everclock")
    window.collectionBehavior = [.canJoinAllSpaces, .stationary, .ignoresCycle, .fullScreenPrimary]
    window.makeKeyAndOrderFront(nil)
    window.isMovableByWindowBackground = true
    window.titleVisibility = .hidden
    window.backgroundColor = .clear
    return window
  }
}

class AppDelegate: NSObject, NSApplicationDelegate {
  var window = NSWindow.makeWindow()
  var hostingView: NSView?

  func applicationDidFinishLaunching(_ notification: Notification) {
    hostingView = NSHostingView(rootView: ClockView())
    window.contentView = hostingView
    NSApp.activate(ignoringOtherApps: true)
  }
}

extension NSRect {
  static func makeDefault() -> NSRect {
    let initialMargin = CGFloat(60)
    let fallback = NSRect(x: 0, y: 0, width: 100, height: 150)

    guard let screenFrame = NSScreen.main?.frame else {
      return fallback
    }

    return NSRect(
      x: screenFrame.maxX - fallback.width - initialMargin,
      y: screenFrame.maxY - fallback.height - initialMargin,
      width: fallback.width, height: fallback.height)
  }
}

extension NSMenu {
  static func makeMenu() -> NSMenu {
    let appMenu = NSMenuItem()
    appMenu.submenu = NSMenu()

    appMenu.submenu?.addItem(
      NSMenuItem(
        title: "Quit \(ProcessInfo.processInfo.processName)",
        action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"
      ))

    let mainMenu = NSMenu(title: "Main Menu")
    mainMenu.addItem(appMenu)
    return mainMenu
  }
}