📜 ⬆️ ⬇️

Application in the menu bar for macOS

Applications placed in the menu bar have long been known to macOS users. Some of these applications have a “regular” part, others are placed only in the menu bar.
In this guide, you will write an application that shows several quotes from famous people in a pop-up window. In the process of creating this application, you will learn:


Note: this guide assumes that you are familiar with Swift and macOS.

Getting started


Launch Xcode. Next in the File / New / Project ... menu, select the macOS / Application / Cocoa App template and click Next .

On the next screen, enter Quotes as the Product Name , select your Organization Name and Organization Identifier . Then make sure that Swift is selected as the application language and the Use Storyboards checkbox is checked. Uncheck the checkboxes Create Document-Based Application , Use Core Data , Include Unit tests and Include UI Tests .
')


Finally, click Next again, specify a place to save the project and click Create .
Once the new project is created, open AppDelegate.swift and add the following property to the class:

let statusItem = NSStatusBar.system.statusItem(withLength:NSStatusItem.squareLength) 

Here we create in the menu bar the Status Item (application icon) of a fixed length that will be visible to users.

Then we need to assign our image to this new item in the menu bar so that we can distinguish our new application.

In the project navigator, go to Assets.xcassets, upload a picture and drag it to the asset catalog.

Select a picture and open the attribute inspector. Change the Render As option to Template Image .



If you are using your own image, make sure that the image is black and white and configure it as a Template image so that the icon looks great on both the dark and light menu bar.

Go back to AppDelegate.swift , and add the following code to applicationDidFinishLaunching (_ :)

 if let button = statusItem.button { button.image = NSImage(named:NSImage.Name("StatusBarButtonImage")) button.action = #selector(printQuote(_:)) } 

Here we assign the image we just added to the application icon and assign the action when we click on it.

Add the following method to the class:

 @objc func printQuote(_ sender: Any?) { let quoteText = "Never put off until tomorrow what you can do the day after tomorrow." let quoteAuthor = "Mark Twain" print("\(quoteText) — \(quoteAuthor)") } 

This method simply prints a quote to the console.

Pay attention to the objc method directive . This allows you to use this method as a response to a button click.

Build and run the application, and you will see the new application in the menu bar. Hooray!
Every time you click on the icon in the menu bar, the famous Mark Twain sentence is displayed in the Xcode console.

Hiding the main window and dock icon


There are a couple of little things that we need to do before tackling the functionality directly:


To remove the dock icon, open Info.plist . Add a new key Application is agent (UIElement) and set its value to YES .



Now it's time to figure out the main application window.





Build and run the application. Now the application does not have both the main window and the unnecessary icon in the dock. Fine!

Add to the Status Item menu


A single response to a click is clearly not enough for a serious application. The easiest way to add functionality is to add a menu. Add this function at the end of AppDelegate .

 func constructMenu() { let menu = NSMenu() menu.addItem(NSMenuItem(title: "Print Quote", action: #selector(AppDelegate.printQuote(_:)), keyEquivalent: "P")) menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem(title: "Quit Quotes", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")) statusItem.menu = menu } 

And then add this call at the end of the applicationDidFinishLaunching (_ :)

 constructMenu() 

We create NSMenu , add 3 instances of NSMenuItem to it and set this menu as an application icon menu.

A few important notes:


Launch the application, and you will see the menu by clicking on the application icon.



Try clicking on the menu — the Print Quote option will print the quote to the Xcode console, and Quit Quotes will complete the application.

Add a popup window


You've seen how easy it is to add a menu from code, but showing a quote in the Xcode console is clearly not what users expect from the application. Now we will add a simple view controller to show the quotes in the right way.

Go to the File / New / File ... menu, select the macOS / Source / Cocoa Class template and click Next .




Finally, click Next again, select a place to save the file and click Create .
Now open the Main.storyboard . Expand View Controller Scene and select View Controller instance .



First select the Identity Inspector and change the class to QuotesViewController , then set the Storyboard ID to QuotesViewController

Now add the following code to the end of the QuotesViewController.swift file:

 extension QuotesViewController { // MARK: Storyboard instantiation static func freshController() -> QuotesViewController { //1. let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil) //2. let identifier = NSStoryboard.SceneIdentifier("QuotesViewController") //3. guard let viewcontroller = storyboard.instantiateController(withIdentifier: identifier) as? QuotesViewController else { fatalError("Why cant i find QuotesViewController? - Check Main.storyboard") } return viewcontroller } } 

What's going on here:

  1. get the link to Main.storyboard .
  2. create a Scene identifier that matches the one we just installed a little higher.
  3. Create an instance of QuotesViewController and return it.

You create this method, so now everyone who uses the QuotesViewController does not need to know how it is created. It just works.

Notice the fatalError inside the guard statement. It can be a good idea to use it or assertionFailure , so that if something in the development goes wrong, to yourself, and to other members of the development team, to be aware of.

Now back to AppDelegate.swift . Add a new property.

 let popover = NSPopover() 

Then replace a pplicationDidFinishLaunching (_ :) with the following code:

 func applicationDidFinishLaunching(_ aNotification: Notification) { if let button = statusItem.button { button.image = NSImage(named:NSImage.Name("StatusBarButtonImage")) button.action = #selector(togglePopover(_:)) } popover.contentViewController = QuotesViewController.freshController() } 

You changed the click action to the togglePopover (_ :) method, which we will write later. Also, instead of setting up and adding a menu, we set up a popup window that will display something from the QuotesViewController .

Add the following three methods to AppDelegate :

 @objc func togglePopover(_ sender: Any?) { if popover.isShown { closePopover(sender: sender) } else { showPopover(sender: sender) } } func showPopover(sender: Any?) { if let button = statusItem.button { popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY) } } func closePopover(sender: Any?) { popover.performClose(sender) } 

showPopover () shows a popup window. You just indicate where it comes from, macOS positions it and draws an arrow as if it appears from the menu bar.

closePopover () simply closes the pop-up window, and togglePopover () is a method that either shows or hides the pop-up window, depending on its state.

Launch the application and click on its icon.



Everything is great, but where is the content?

Implement Quote View Controller


First you need a model for storing quotes and attributes. Go to the File / New / File ... menu and select the macOS / Source / Swift File template , then Next . Name the file Quote and click Create .

Open the Quote.swift file and add the following code to it:

 struct Quote { let text: String let author: String static let all: [Quote] = [ Quote(text: "Never put off until tomorrow what you can do the day after tomorrow.", author: "Mark Twain"), Quote(text: "Efficiency is doing better what is already being done.", author: "Peter Drucker"), Quote(text: "To infinity and beyond!", author: "Buzz Lightyear"), Quote(text: "May the Force be with you.", author: "Han Solo"), Quote(text: "Simplicity is the ultimate sophistication", author: "Leonardo da Vinci"), Quote(text: "It's not just what it looks like and feels like. Design is how it works.", author: "Steve Jobs") ] } extension Quote: CustomStringConvertible { var description: String { return "\"\(text)\" — \(author)" } } 

Here we define a simple quotation structure and a static property that returns all quotes. Since we made Quote conforming to the CustomStringConvertible protocol, we can easily get conveniently formatted text.

There is progress, but we still need controls to display all this.

Adding interface elements


Open the Main.storyboard and pull out 3 buttons ( Push Button ) and a label ( Multiline Label) on the view controller.

Arrange the buttons and label so that they look like this:



Attach the left button to the left edge with a gap of 20 and center vertically.
Attach the right button to the right edge with a gap of 20 and center vertically.
Attach the bottom button to the bottom edge with a gap of 20 and center horizontally.
Attach the left and right edges of the label to the buttons with a gap of 20 center vertically.



You will see several layout errors, as there is not enough information for auto layout to sort out.

Set the Horizontal Content Hugging Priority label to 249 to allow the label to be resized.



Now do the following:



Now open QuotesViewController.swift and add the following code to the implementation of the QuotesViewController class:

 @IBOutlet var textLabel: NSTextField! 


Add this extension to the class implementation. Now in QuotesViewController.swift two class extensions.

 // MARK: Actions extension QuotesViewController { @IBAction func previous(_ sender: NSButton) { } @IBAction func next(_ sender: NSButton) { } @IBAction func quit(_ sender: NSButton) { } } 

We have just added an outlet for the label that we will use to display quotes, and 3 stub methods that we connect to the buttons.

We connect code with Interface Builder


Pay attention: Xcode placed circles to the left of your code - near the keywords IBAction and IBOutlet .



We will use them to connect the code with the UI.

Hold down the alt key and click on the Main.storyboard in the project navigator . This will open the storyboard in the Assistant Editor on the right, and the code on the left.

Drag the circle to the left of the textLabel to the label in the interface builder . In the same way, connect the previous , next, and quit methods with the left, right, and bottom buttons, respectively.



Run your application.



We used the default popup size. If you want a larger or smaller pop-up window, simply change its size in the storyboard .

Write the code for the buttons


If you have not already hidden the Assistant Editor , click Cmd-Return or V iew> Standard Editor> Show Standard Editor

Open QuotesViewController.swift and add the following properties to the class implementation:

 let quotes = Quote.all var currentQuoteIndex: Int = 0 { didSet { updateQuote() } } 

The quotes property contains all quotes, and currentQuoteIndex is the index of the quotation that is currently being output. CurrentQuoteIndex also has a property observer to update the contents of the label with a new quote when the index changes.

Now add the following methods:

 override func viewDidLoad() { super.viewDidLoad() currentQuoteIndex = 0 } func updateQuote() { textLabel.stringValue = String(describing: quotes[currentQuoteIndex]) } 

When the view loads, we set the citation index to 0, which, in turn, causes the interface to be updated. updateQuote () simply updates the text label to display a quote. corresponding currentQuoteIndex .

Finally, update these methods with the following code:

 @IBAction func previous(_ sender: NSButton) { currentQuoteIndex = (currentQuoteIndex - 1 + quotes.count) % quotes.count } @IBAction func next(_ sender: NSButton) { currentQuoteIndex = (currentQuoteIndex + 1) % quotes.count } @IBAction func quit(_ sender: NSButton) { NSApplication.shared.terminate(sender) } 

The methods next () and previous () cycle through all the quotes. quit closes the application.

Run the application:



Event monitoring


There is one more thing that users expect from our application - hide the pop-up window when the user clicks somewhere outside of it. For this we need a mechanism called macOS global event monitor .

Create a new Swift file, call it EventMonitor , and replace its contents with the following code:

 import Cocoa public class EventMonitor { private var monitor: Any? private let mask: NSEvent.EventTypeMask private let handler: (NSEvent?) -> Void public init(mask: NSEvent.EventTypeMask, handler: @escaping (NSEvent?) -> Void) { self.mask = mask self.handler = handler } deinit { stop() } public func start() { monitor = NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler) } public func stop() { if monitor != nil { NSEvent.removeMonitor(monitor!) monitor = nil } } } 

When initializing an instance of this class, we give it an event mask that we will listen to (such as keystrokes, mouse wheel scrolling, etc.) and an event handler.
When we are ready to start listening, start () calls addGlobalMonitorForEventsMatchingMask (_: handler :) , which returns an object that we save. As soon as the event contained in the mask happens, the system calls your handler.

To stop monitoring events, removeMonitor () is called in stop () and we remove an object by assigning it a nil value.

All we have to do is call start () and stop () at the right time. The class also calls stop () in the de-initializer to tidy up after itself.

We connect Event Monitor


Open AppDelegate.swift for the last time and add a new property:

 var eventMonitor: EventMonitor? 

Then add this code to configure the event monitor at the end of the applicationDidFinishLaunching (_ :)

 eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown]) { [weak self] event in if let strongSelf = self, strongSelf.popover.isShown { strongSelf.closePopover(sender: event) } } 

This will inform your application when you click the left or right button. Note: the handler will not be called in response to mouse clicks inside your application. That is why the popup window will not close until you click inside it.

We use the weak link to self to avoid the danger of a cycle of strong links between AppDelegate and EventMonitor .

Add the following code to the end of the showPopover (_ :) method:

 eventMonitor?.start() 

Here we start monitoring events when a popup window appears.

Now add the code at the end of the closePopover (_ :) method:

 eventMonitor?.stop() 

Here we finish monitoring when the pop-up closes.

The app is ready!

Conclusion


Here you will find the complete code for this project.

You have learned how to set the menu and pop-up window in the application located in the menu bar. Why not experiment with a few tags or formatted text to get a better view of the quotes? Or connect backend to get quotes from the Internet? Or do you want to use the keyboard to navigate between quotes?

A good place for research is official documentation: NSMenu , NSPopover and NSStatusItem .

Source: https://habr.com/ru/post/447754/


All Articles