This tutorial builds on the [general introduction to Native Code and Electron](./native-code-and-electron.md) and focuses on creating a native addon for macOS using Objective-C, Objective-C++, and Cocoa frameworks. To illustrate how you can embed native macOS code in your Electron app, we'll be building a basic native macOS GUI (using AppKit) that communicates with Electron's JavaScript.
Specifically, we'll be integrating with two macOS frameworks:
* AppKit - The primary UI framework for macOS applications that provides components like windows, buttons, text fields, and more.
* Foundation - A framework that provides data management, file system interaction, and other essential services.
This tutorial will be most useful to those who already have some familiarity with Objective-C and Cocoa development. You should understand basic concepts like delegates, NSObjects, and the target-action pattern commonly used in macOS development.
> [!NOTE]
> If you're not already familiar with these concepts, Apple's [documentation on Objective-C](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Introduction/Introduction.html) is an excellent starting point.
## Requirements
Just like our general introduction to Native Code and Electron, this tutorial assumes you have Node.js and npm installed, as well as the basic tools necessary for compiling native code on macOS. You'll need:
* Xcode installed (available from the Mac App Store)
* Xcode Command Line Tools (can be installed by running `xcode-select --install` in Terminal)
## 1) Creating a package
You can re-use the package we created in our [Native Code and Electron](./native-code-and-electron.md) tutorial. This tutorial will not be repeating the steps described there. Let's first setup our basic addon folder structure:
```txt
my-native-objc-addon/
├── binding.gyp
├── include/
│ └── objc_code.h
├── js/
│ └── index.js
├── package.json
└── src/
├── objc_addon.mm
└── objc_code.mm
```
Our `package.json` should look like this:
```json title='package.json'
{
"name": "objc-macos",
"version": "1.0.0",
"description": "A demo module that exposes Objective-C code to Electron",
"main": "js/index.js",
"author": "Your Name",
"scripts": {
"clean": "rm -rf build",
"build-electron": "electron-rebuild",
"build": "node-gyp configure && node-gyp build"
},
"license": "MIT",
"dependencies": {
"bindings": "^1.5.0",
"node-addon-api": "^8.3.0"
}
}
```
## 2) Setting Up the Build Configuration
For a macOS-specific addon using Objective-C, we need to modify our `binding.gyp` file to include the appropriate frameworks and compiler flags. We need to:
1. Ensure our addon is only compiled on macOS
2. Include the necessary macOS frameworks (Foundation and AppKit)
3. Configure the compiler for Objective-C/C++ support
*`.mm` extension for source files: This indicates Objective-C++ files which can mix Objective-C and C++.
*`libraries`: This section includes the Foundation and AppKit frameworks
*`xcode_settings` includes:
*`CLANG_ENABLE_OBJC_ARC`: "YES" enables Automatic Reference Counting for easier memory management
*`OTHER_CFLAGS`: `-ObjC++` to properly handle Objective-C++ compilation
*`MACOSX_DEPLOYMENT_TARGET`: This flag specifies the minimum macOS version supported. You'll likely want this to match the lowest version of macOS you support with your app.
## 3) Defining the Objective-C Interface
Let's define our interface in `include/objc_code.h`:
using TodoCallback = std::function<void(conststd::string&)>;
// Callback setters
void setTodoAddedCallback(TodoCallback callback);
} // namespace objc_code
```
This header:
* Includes a basic hello_world function from the general tutorial
* Adds a `hello_gui` function to create a native macOS GUI
* Defines callback types for Todo operations
* Provides setter functions for these callbacks
## 4) Implementing the Objective-C Code
Now, let's implement our Objective-C code in `src/objc_code.mm`. This is where we'll create our native macOS GUI using AppKit.
We'll always add code to the bottom of our file. To make this tutorial easier to follow, we'll start with the basic structure and add features incrementally - step by step.
### Setting Up the Basic Structure
```objc title='src/objc_code.mm'
#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#import <string>
#import <functional>
#import "../include/objc_code.h"
using TodoCallback = std::function<void(conststd::string&)>;
static TodoCallback g_todoAddedCallback;
// More code to follow later...
```
This imports the required frameworks and defines our callback type. The static `g_todoAddedCallback` variable will store our JavaScript callback function.
### Defining the Window Controller Interface
At the bottom of `objc_code.mm`, add the following code to define our window controller class interface:
This declares our TodoWindowController class which will manage the window and UI components:
* A text field (`NSTextField`) for entering todo text
* A date picker (`NSDatePicker`) for selecting the date
* An "Add" button (`NSButton`)
* A table view to display the todos (`NSTableView`)
* An array to store the todo items (`NSMutableArray`)
### Implementing the Window Controller
At the bottom of `objc_code.mm`, add the following code to start implementing the window controller with an initialization method:
```objc title='src/objc_code.mm'
// Previous code...
// Controller for the main window
@implementation TodoWindowController
- (instancetype)init {
self = [super initWithWindowNibName:@""];
if (self) {
// Create an array to store todos
_todos = [NSMutableArray array];
[self setupWindow];
}
return self;
}
// More code to follow later...
```
This initializes our controller. We're not using a nib file, so we pass an empty string to `initWithWindowNibName`. We create an empty array to store our todos and call the `setupWindow` method, which we'll implement next.
At this point, our full file looks like this:
```objc title='src/objc_code.mm'
#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#import <string>
#import <functional>
#import "../include/objc_code.h"
using TodoCallback = std::function<void(conststd::string&)>;
Now, we'll add a `setupWindow()` method. This method will look a little overwhelming on first sight, but it really just instantiates a number of UI controls and then adds them to our window.
1. Creates a window with a title and standard window controls
2. Centers the window on the screen
3. Creates a text field for entering todo text
4. Adds a date picker configured to show only date (no time)
5. Adds an "Add" button that will call the `addTodo:` method when clicked
We're still missing the table view to display our todos. Let's add that to the bottom of our `setupWindow()` method, right where it says `More UI elements to follow in the next step...` in the code above.
2. Create a table view with two columns: one for the todo text and one for the date
3. Set up the data source and delegate to this class
4. Add the table to the scroll view
This concludes the UI elements in `setupWindow()`, so we can now move on to business logic.
### Implementing the "Add Todo" Functionality
Next, let's implement the `addTodo:` method to handle adding new todos. We'll need to do two sets of operations here: First, we need to handle our native UI and perform operations like getting the data out of our UI elements or resetting them. Then, we also need notify our JavaScript world about the newly added todo.
In the interest of keeping this tutorial easy to follow, we'll do this in two steps.
```objc title='src/objc_code.mm'
// Previous code...
// Action method for the Add button
- (void)addTodo:(id)sender {
NSString *text = [_textField stringValue];
if ([text length] > 0) {
NSDate *date = [_datePicker dateValue];
// Create a unique ID
NSUUID *uuid = [NSUUID UUID];
// Create a dictionary to store the todo
NSDictionary *todo = @{
@"id": [uuid UUIDString],
@"text": text,
@"date": date
};
// Add to our array
[_todos addObject:todo];
// Reload the table
[_tableView reloadData];
// Reset the text field
[_textField setStringValue:@""];
// Next, we'll notify our JavaScript world here...
}
}
// More code to follow later...
```
This method:
1. Gets the text from the text field
2. If the text is not empty, creates a new todo with a unique ID, the entered text, and the selected date
3. Adds the todo to our array
4. Reloads the table to show the new todo
5. Clears the text field for the next entry
Now, let's extend the `addTodo:` method to notify JavaScript when a todo is added. We'll do that at the bottom of the method, where it currently reads "Next, we'll notify our JavaScript world here...".
This adds code to do a whole bunch of conversions (so that N-API can eventually turn this data into structures ready for V8 and the JavaScript world) - and then calls our JavaScript callback. Specifically, it does the following:
1. Check if a callback function has been registered
2. Convert the todo to JSON format
3. Convert the date to milliseconds since epoch (JavaScript date format)
4. Convert the JSON to a C++ string
5. Call the callback function with the JSON string
We're now done with our `addTodo:` method and can move on to the next step: The data source for the Table View.
### Implementing the Table View Data Source
Let's implement the table view data source methods to display our todos:
1. Implement the `hello_world` function that returns a greeting string
2. Provide a way to set the callback function for todo additions
3. Implement the `hello_gui` function that creates and shows our native UI
4. Lastly, we also keep a static reference to prevent the window controller from being deallocated
Note that we're using GCD (Grand Central Dispatch) to dispatch to the main thread, which is required for UI operations. We're not dedicating more time to thread safety in this tutorial, but here's a quick reminder: In macOS/iOS, all UI updates must happen on the main thread. The main thread is the primary execution path where the application runs its event loop and processes user interface events. In our code, when JavaScript calls the `hello_gui()` function, the call might be coming from a Node.js worker thread, not the main thread. Using GCD, we safely redirect the window creation code to the main thread, ensuring proper UI behavior.
This is a common pattern in macOS/iOS development - any code that touches the UI needs to be executed on the main thread, and GCD provides a clean way to ensure this happens.
The final version of `objc_code.mm` looks like this:
```objc title='src/objc_code.mm'
#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#import <string>
#import <functional>
#import "../include/objc_code.h"
using TodoCallback = std::function<void(conststd::string&)>;
We now have working Objective-C code. To make sure it can be safely and properly called from the JavaScript world, we need to build a bridge between Objective-C and C++, which we can do with Objective-C++. We'll do that in `src/objc_addon.mm`.
Bear with us: This bridge code always ends up being pretty verbose and might seem difficult to follow. As far as modern desktop development goes, it's fairly low-level, so be patient with yourself - it might take a little bit before the bridging really "clicks".
### Basic Class Definition
```objc title='src/objc_addon.mm'
#include <napi.h>
#include <string>
#include "../include/objc_code.h"
class ObjcAddon : public Napi::ObjectWrap<ObjcAddon> {
* Sets up the constructor with member initialization
* Creates a threadsafe function using N-API, which allows safe callbacks from any thread
* Defines a lambda to create callback functions for different event types
* Registers the "todoAdded" callback with our Objective-C code
* Implements a destructor to clean up resources when the addon is destroyed
The threadsafe function is important because UI events in Objective-C might happen on a different thread than the JavaScript event loop. This mechanism safely bridges those thread boundaries.
### Implementing JavaScript Methods
Finally, let's implement the methods that JavaScript will call:
Let's take a look at what we've added in this step:
*`HelloWorld()`: Takes a string input, calls our Objective-C function, and returns the result
*`HelloGui()`: A simple wrapper around the Objective-C `hello_gui` function
*`On`: Allows JavaScript to register event listeners that will be called when native events occur
The `On` method is particularly important as it creates the event system that our JavaScript code will use to receive notifications from the native UI.
Together, these three components form a complete bridge between our Objective-C code and the JavaScript world, allowing bidirectional communication. Here's what the finished file should look like:
```objc title='src/objc_addon.mm'
#include <napi.h>
#include <string>
#include "../include/objc_code.h"
class ObjcAddon : public Napi::ObjectWrap<ObjcAddon> {
You're so close! We now have working Objective-C and thread-safe ways to expose methods and events to JavaScript. In this final step, let's create a JavaScript wrapper in `js/index.js` to provide a more friendly API:
throw new Error('This module is only available on macOS')
}
const native = require('bindings')('objc_addon')
this.addon = new native.ObjcMacosAddon()
this.addon.on('todoAdded', (payload) => {
this.emit('todoAdded', this.parse(payload))
})
}
helloWorld (input = '') {
return this.addon.helloWorld(input)
}
helloGui () {
this.addon.helloGui()
}
parse (payload) {
const parsed = JSON.parse(payload)
return { ...parsed, date: new Date(parsed.date) }
}
}
if (process.platform === 'darwin') {
module.exports = new ObjcMacosAddon()
} else {
module.exports = {}
}
```
This wrapper:
1. Extends EventEmitter to provide event support
2. Checks if we're running on macOS
3. Loads the native addon
4. Sets up event listeners and forwards them
5. Provides a clean API for our functions
6. Parses JSON payloads and converts timestamps to JavaScript Date objects
## 7) Building and Testing the Addon
With all files in place, you can build the addon:
```sh
npm run build
```
Please note that you _cannot_ call this script from Node.js directly, since Node.js doesn't set up an "app" in the eyes of macOS. Electron does though, so you can test your code by requiring and calling it from Electron.
## Conclusion
You've now built a complete native Node.js addon for macOS using Objective-C and AppKit. This provides a foundation for building more complex macOS-specific features in your Electron apps, giving you the best of both worlds: the ease of web technologies with the power of native macOS code.
The approach demonstrated here allows you to:
* Create native macOS UIs using AppKit
* Implement bidirectional communication between JavaScript and Objective-C
* Leverage macOS-specific features and frameworks
* Integrate with existing Objective-C codebases
For more information on developing with Objective-C and Cocoa, refer to Apple's developer documentation: