World clock status bar app for OS X
Due to the nature of my work, I often am working with people in many different time zones across the world. Figuring out what time it is everywhere I work with people is a bit tricky especially with time zone changes, so I thought it would be cool to modify the Mac time display to show the time in places that I work with.
What I was surprised to find is that the Mac menubar doesn't actually offer this capability, so I decided to give a shot writing an app in Swift using XCode. It didn't actually end up being very hard and I learned a lot doing it. Here's a short tour through the code in case you'd like to try something similar. I'll walk you through the source below, but if you'd like to just see it in GitLab click here.
The first thing we need to do initialize the AppDelegate class, which is used in Cocoa apps to implement the overall behavior of the program. We create a reference object for the main application window (which is hidden, but seemingly required for any Cocoa application), then initialize our status bar object, date formatter which we'll use to get the time in different time zones, our application menu, our timer that we'll use to trigger an update to the display every 60 seconds, and a utility function which we can call to exit the program when the user chooses to.
import Cocoa
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
@IBOutlet weak var window: NSWindow!
var statusBar = NSStatusBar.system
var statusBarItem : NSStatusItem = NSStatusItem()
var dateFormatter = DateFormatter()
let menu = NSMenu()
@objc func terminate() {
exit(0)
}
var timer = Timer()
Within the AppDelegate class, we define a few important functions. This timerAction function in particular is run every 60 seconds to update the display with the new time. Each time this is run we need to get the current date, then set a variable for each time zone we want to display that contains the result of running the dateFormatter for each time zone. Finally, we set the menubar text to the new output.
@objc func timerAction() {
let date = Date()
dateFormatter.timeZone = TimeZone(identifier: "America/Los_Angeles")
let pacific_time = dateFormatter.string(from:date)
dateFormatter.timeZone = TimeZone(identifier: "America/New_York")
let eastern_time = dateFormatter.string(from:date)
dateFormatter.timeZone = TimeZone(identifier: "Europe/Amsterdam")
let amsterdam_time = dateFormatter.string(from:date)
dateFormatter.timeZone = TimeZone(identifier: "Asia/Kolkata")
let newdelhi_time = dateFormatter.string(from:date)
statusBarItem.title = "🇺🇸" + pacific_time + " 🇺🇸" + eastern_time + " 🇳🇱" + amsterdam_time + " 🇮🇳" + newdelhi_time
}
Another important function within the AppDelegate class is this one, which is run when the application is launched and is used to initialize everything.
First, we create the status bar item itself, then add a "Quit" menu option when it's selected that references our terminate function we implemented above. We also set the dateFormatter to always output in 24 hour format.
func applicationDidFinishLaunching(_ aNotification: Notification) {
statusBarItem = statusBar.statusItem(withLength: -1)
menu.addItem(NSMenuItem(title: "Quit WorldClock", action: #selector(AppDelegate.terminate), keyEquivalent: "q"))
statusBarItem.menu = menu
dateFormatter.dateFormat = "HH:mm"
Next, we set the initial output to the current time so that the timer isn't blank while we wait for the minute changeover.
// Populate initial timer
self.timerAction()
In order to ensure the app runs efficiently and isn't constantly checking/updating the time, we wait for the minute to change over to start our 60 second update timer. In order to start that, we calculate the delay needed until the next minute changeover happens.
// Sync to minute changeover
let now = Date.timeIntervalSinceReferenceDate
let delayFraction = trunc(now) - now
//Calculate a delay until the next even minute
let delay = 60.0 - Double(Int(now) % 60) + delayFraction
Now that we've calculated the delay, we create an object that will wait that delay before triggering. Once the delay happens and the minute changes over, it creates our trigger that runs timerAction every 60 seconds. Because our first update won't trigger for another 60 seconds and we just had a minute changeover, we also manually run the timerAction to update the display.
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: {
// update for this new minute and then start repeating
self.timerAction()
self.timer = Timer.scheduledTimer(timeInterval: 60, target: self, selector: #selector(AppDelegate.timerAction), userInfo: nil, repeats: true)
})
At this point we're done! Really simple. If you've implemented the project yourself, you'll need to update the window in the nib to be hidden by default and also set the app to be an agent in the plist.
If you have a similar need, customizing this program should be pretty straightforward. You can find the latest version as well as the wrapper XCodeProj file and updates to the plist and nib in my GitLab repo here.
I had a lot of fun writing this little app, hopefully you learned something fun as well and feel encouraged also to try a small project like this!