Converting a React Native for macOS Application into a Menu Bar Application

The status menus (also known as menu bar icons) of a macOS menu bar let users quickly access status information and perform actions without launching an application inside a new, separate window. For example, upon installation, a popular desktop application like Dropbox automatically adds a status menu to the macOS menu bar for monitoring and reporting the status of uploads. With the single click of an icon/title, the status menu toggles open a condensed version of the Dropbox desktop application. You can get the latest uploads and share them immediately without having to change your active window.

Status menus helps to maximize your workflow and keep you productive. They run in the background and periodically update with new notifications and content to avoid distracting you while you are focused on completing your work. With so many status bar applications available, you can customize this section of the macOS menu bar and arrange the items in any order to best complement your workflow.

However, if you cannot find a status bar application that addresses a specific aspect of your workflow, such as aggregating and displaying relevant information about the status of jobs triggered by a CI build pipeline, then you can build your own status bar application with React Native for macOS. Unlike a desktop application, building a status bar application involves quite a few steps beyond the installation steps listed in the React Native for macOS documentation.

Below, I'm going to show you how to convert a React Native for macOS application into a menu bar application.

Installation and Setup#

To get started, let's create a new React Native for macOS project from a standard React Native template.

Verify that the project was initialized correctly by running it:

Migrating to Swift#

The status bar application consists of two components: a status item and a popover. A status item is a clickable icon/title that sits in the system menu bar. A popover is a self-contained view that hovers over its surrounding context and displays content within a non-modal dialog. It is anchored to the element responsible for making it appear, which is indicated by the direction of its tooltip arrow. Both of these parts will be implemented in Swift.

Unlike a pull-down menu, which simply lists options, the layout of a popover's content is flexible. This content will come directly from the React application whose root component <App /> is registered with AppRegistry inside of the project's index.js file.

To transition to Swift, we must first remove the existing Objective-C code from the project. Delete the following files:

Even if you delete these files inside of Finder or within your terminal, Xcode still references these from the project navigator. Therefore, open the project inside of Xcode by either...

  • Dragging and dropping the macos directory from Finder to the Xcode application icon.

  • Running the command xed macos inside of the terminal.

Then, delete those files manually from the project navigator.

Keep Xcode opened for the remainder of this tutorial.

Adding the Status Bar Item and Popover#

To add a status bar item and popover into our application, let's first create two new files:

Note: The casing of projectname must match the casing of the parent project directory's name. For example, if the project's directory name is rnmacmenubar (despite the project being named rnMacMenubar during initialization via npx react-native init), then projectname will be rnmacmenubar.

The <projectname>-macOS-Bridging-Header.h bridging header file exposes Objective-C libraries to Swift. For this project, the specified libraries provide APIs for integrating React Native into native code and rendering the React Native application within a UIView.

(<projectname>-macOS-Bridging-Header.h)

The AppDelegate.swift file serves as the application's initial entry file and defines a AppDelegate class, which handles application lifecycle events like applicationDidFinishLaunching and how the application responds to these events.

(AppDelegate.swift)

Inside of AppDelegate, define the properties popover and statusBarItem, which will reference the application's popover and status bar item respectively. An additional window property will be defined to reference a "development" window.

(AppDelegate.swift)

Once the application has launched, initialize the React Native application with RCTRootView, which displays the React Native application whose root component <App /> is registered inside of the project's index.js file (development environment) or from the compiled bundle (production build). This base view is embedded within a NSViewController.

(AppDelegate.swift)

Note: #if DEBUG is a pre-processor directive. Code inside of this directive are pre-processed before compilation. In the above code, we initialize the React Native bridge within this directive.

Initialize the popover by setting its base dimensions to 700px by 800px and behavior to transient. Enable animation for its dismissal and emergence. Set its content to the NSViewController that has the React application embedded within it.

(AppDelegate.swift)

Initialize the status bar item by allocating 60px (in width) within the macOS status bar for it. Using the button property, we can customize the appearance and behavior of this item. Whenever the status item is clicked, the togglePopover method is called. The status item will be shown in the status bar via the project's name, not a typical icon, to make this example simpler.

(AppDelegate.swift)

The togglePopover dismisses the popover when its opened, and vice-versa.

(AppDelegate.swift)

Note: The @objc attribute marks the method as accessible and executable in Objective-C.

The "development" window allows us to iterate upon the React application and immediately see changes without having to open the popover to see changes anytime they occur. Both the window and popover share the same rootViewController to avoid having two separate instances of the application.

(AppDelegate.swift)

Note: Once the popover opens, the window will no longer possess the controller, and the React application will only be rendered in the popover. For development, this is fine.

applicationDidFinishLaunching tells the delegate when the application has launched. We will place all of the initialization code within the body of this instance method to execute this code once the application has launched.

Altogether...

(AppDelegate.swift)

Note: Replace all instances of <projectName> with your project's name. This is the project name specified during initialization via npx react-native init.

Drag and drop the <projectname>-macOS-Bridging-Header.h and AppDelegate.swift files from Finder into Xcode's project navigator (under <projectname>/<projectname>-macOS directory). When presented with the "Choose options for adding these files" modal, accept the default selected options.

Setting Correct Build Settings#

Inside of Xcode, check that the run destination is "My Mac" in the workspace toolbar. If not, then change the scheme to <projectname>-macOS.

  1. From the upper menu, select "Product".

  2. Hover over the "Scheme" option and select <projectname>-macOS.

This is what the workspace toolbar should display:

To avoid Swift linking errors, let's enable dead code stripping by visiting the project's build settings and switching the "Dead Code Stripping" setting to "YES."

  1. Click on the project name in the project navigator.

  2. Select the project from the list of projects and targets in the left sidebar of the project editor.

  3. Select the "Build Settings" tab in the project's settings.

  4. Filter the settings by selecting the conditionals "All" and "Combined."

  5. Under the "Linking" section, switch "Dead Code Stripping" to "Yes."

Now, let's tell Xcode to include Swift standard libraries in the final application bundle.

  1. Select the target <projectname>-macOS from the list of projects and targets in the left sidebar of the project editor.

  2. Select the "Build Settings" tab in the project's settings.

  3. Under the "Build Options" section, switch "Always Embed Swift Standard Libraries" to "Yes."

To enable the DEBUG flag only when building and running the application from Xcode, search for the OTHER_SWIFT_FLAGS setting under the "User-Defined" section of the target <projectname>-macOS's settings. Set the -DDEBUG flag only for the "Debug" mode, not for "Release." Otherwise, when you release the application to users, it will try to connect to the metro packager.

If you encounter the following issue:

Then resolve it by switching the "Don't Dead-strip Inits and Terms" project setting under the "Linking" section to "Yes" for both "Project" and "Target."

To import Objective-C libraries from the Objective-C bridging header file <projectname>-macOS-Bridging-Header.h into Swift code, visit the project's build settings and set the "Objective-C Bridging Header" setting under the "Swift Compiler - General" section to the header file's path.

Upon setting this path, SRCROOT and PROJECT_NAME are automatically substituted with their values.

To inform the compiler of the AppDelegate.swift source file that must be compiled during the build process, add this source file as a "Compile Source" within the target's "Build Phase" settings.

Click on project in project navigator. Clean the project by selecting "Product" from the upper menu and clicking the "Clean Build Folder" option.

With the old AppDelegate class deleted (from the deleted AppDelegate.m file), we need to reselect AppDelegate class to ensure the new AppDelegate class (from the AppDelegate.swift file) is selected.

Select the Main.storyboard file from the project navigator. Under "Application Scene" in the left-sidebar of the project editor. In the right-sidebar, select the "Attributes Inspector" tab and enter "AppDelegate" as the App Delegate class.

Running the Application Locally#

Run the application via the shortcut CMD + r.

This launches...

  • The metro packager in a new terminal window.

  • A small empty window.

  • A large development window that displays the React application.

  • The menu bar application.

If you click on the status item labeled with the project's name in the menu bar, a popover pops open and the development window no longer displays the React application. Inside of the popover is the React application. Click on the status item again and the popover is dismissed.

The smaller window is an initial window that's automatically launched for the application (in the top-left corner of the above screenshot). The larger window is the development window (in the lower-right corner of the above screenshot). Since the development window shares the same rootViewController, if you resize this window, the content within the popover is also resized.

Notice that the dock shows an icon for the application. To omit this icon from the dock when running the application, edit the project's Info.plist file.

  1. Select the Info.plist file in the project navigator.

  2. Click the "+" icon in the "Information Property List" row.

  3. Type until the key "Application is agent (UIElement)" is selected. Then, press enter to select this key.

  4. Set the value of this key to "YES."

If you rebuild and rerun the application, then you will notice that the application's icon no longer appears in the dock.

To hide the extra, initial (smaller) window, uncheck the "Is initial controller" checkbox in the storyboard's window controller.

If you rebuild and rerun the application, then you will notice that this window no longer appears.

Testing the Release Build#

To generate a release build, run the following command:

If you encounter the following error...

Then add the EXCLUDED_ARCHS build setting to the command and set it to arm64. This excludes support for ARM-based simulators.

Double-click the release build to verify that the application works properly. Now you can distribute this menu bar application to other macOS users!

Next Steps#

If you are stuck at any step of this tutorial, feel free to visit the final version of this project here.

Try building your next menu bar application with React Native for macOS!

Sources#