This document provides an overview of the native macOS code in the Bergen. It explains the architecture, dependencies, and design decisions to help developers understand and extend the native functionality.
- Overview
- Directory Structure
- Key Components
- Adding New Native Functionality
- Building and Debugging
- Common Issues
- Logging with os_log
The Bergen app uses React Native for macOS (RN macOS) to provide a cross-platform experience while leveraging native capabilities when needed. The native code is written in Objective-C/Objective-C++ and follows Apple's Cocoa framework patterns.
The architecture is designed to:
- Separate concerns between the UI (React Native) and platform-specific features
- Provide clean interfaces between JavaScript and native code
- Enable easy extension with new native functionality
- Follow macOS app development best practices
The main native code is located in the /macos directory:
/macos
├── Podfile # CocoaPods dependency management
├── bergen-macos/ # Main application code
│ ├── AppDelegate.h # App initialization and lifecycle
│ ├── AppDelegate.mm # Implementation of app delegate
│ ├── main.m # App entry point
│ ├── NativeImplementation.mm # Combined native module implementations
│ ├── Info.plist # App configuration
│ ├── bergen.entitlements # App permissions
│ └── Base.lproj/ # Storyboards and UI resources
│ └── Main.storyboard # Main UI layout including menus
├── bergen.xcodeproj/ # Xcode project configuration
└── bergen.xcworkspace/ # Xcode workspace
Note on Implementation Approach: To avoid Xcode project file modifications and simplify the build process, the MenuManager and NativeMenuModule implementations are combined in the single
NativeImplementation.mmfile, which is directly imported by the AppDelegate.
The AppDelegate class is the entry point for the macOS application and handles:
- Application lifecycle events (launch, terminate, etc.)
- React Native bridge setup
- Menu system initialization
- Window management
The AppDelegate extends RCTAppDelegate from React Native, which provides most of the React Native integration automatically.
Key methods:
applicationDidFinishLaunching:- Sets up the app when it launchessourceURLForBridge:- Provides the URL for the JavaScript bundlebundleURL- Returns the URL based on debug/release modeconcurrentRootEnabled- Controls React 18 concurrent featuresbuildMenu- Sets up the application menu and wires up menu item actions
The native menu system consists of:
-
Main.storyboard - Contains the default menu structure including:
- Application menu (bergen)
- File menu
- Edit menu
- View menu
- Window menu
- Help menu
-
MenuManager - Singleton class that manages menus programmatically
-
NativeMenuModule - Handles menu actions and bridges to React Native
The menu includes important functionality:
- File -> Open (⌘O): Opens a native file picker dialog to select markdown files
- File -> Quit (⌘Q): Quits the application
- Application menu -> Quit bergen (⌘Q): Alternative quit option
The application implements several native modules to bridge between React Native and macOS:
The MenuManager class provides a programmatic interface to the menu system:
// Get the shared instance
MenuManager *menuManager = [MenuManager sharedInstance];
// Add a menu item to a specific menu
[menuManager createMenuItemWithTitle:@"Custom Action"
action:@selector(customAction:)
keyEquivalent:@"c"
menuTitle:@"File"];This design separates menu management from the AppDelegate, making it easier to maintain and extend.
The project includes the following native modules:
Allows JavaScript code to interact with the native menu system:
- Extends RCTEventEmitter to send events to JavaScript
- Exposes methods to add/modify menu items
- Sends events to JavaScript when menu items are selected
- Handles File -> Open menu action via native file picker
- Maintains a shared instance for access from AppDelegate
- Runs on the main thread for UI operations
- Includes safety checks to ensure bridge is ready before sending events
Example JavaScript usage:
import { NativeModules, NativeEventEmitter } from 'react-native';
const { NativeMenuModule } = NativeModules;
const menuEmitter = new NativeEventEmitter(NativeMenuModule);
// Add a menu item
NativeMenuModule.addMenuItem('Custom Action', 'File', 'c', 'custom_action');
// Listen for menu selections
menuEmitter.addListener('menuItemSelected', (event) => {
if (event.identifier === 'custom_action') {
// Handle the menu action
}
});Provides native file picker functionality:
- Exposes a
showOpenDialogmethod that returns a Promise with the selected file path - Configures the dialog to filter for markdown files only
- Runs on the main thread for UI operations
Example JavaScript usage:
import { NativeModules } from 'react-native';
const { FileManagerModule } = NativeModules;
async function openMarkdownFile() {
try {
const filePath = await FileManagerModule.showOpenDialog();
if (filePath) {
// Handle the selected file
console.log('Selected file:', filePath);
}
} catch (error) {
console.error('Failed to open file dialog:', error);
}
}To add new native functionality to the app:
-
Determine the appropriate pattern:
- Native Module: For features that need to be called from JavaScript
- Native UI Component: For custom UI elements
- App Extension: For system-level integration
-
Create the necessary files:
- Header (.h) and implementation (.m/.mm) files
- Add to the Xcode project
-
Bridge to React Native (if needed):
- Create a native module class extending
RCTEventEmitterand implementingRCTBridgeModule - Export methods using
RCT_EXPORT_METHOD - Send events using
sendEventWithName:body:
- Create a native module class extending
-
Update documentation in this guide
// NotificationManager.h
@interface NotificationManager : NSObject
+ (instancetype)sharedInstance;
- (void)showNotification:(NSString *)title body:(NSString *)body;
@end
// NotificationModule.h
@interface NotificationModule : RCTEventEmitter <RCTBridgeModule>
@end
// NotificationModule.m
RCT_EXPORT_METHOD(showNotification:(NSString *)title body:(NSString *)body)
{
[[NotificationManager sharedInstance] showNotification:title body:body];
}Build the macOS app using:
# Install dependencies
yarn install
# Run for development
yarn macos
# Build for production
./build-macos.sh-
Xcode Debugging:
- Open
macos/bergen.xcworkspacein Xcode - Set breakpoints in native code
- Run the app from Xcode
- Open
-
Logging:
- Use structured logging with
os_logfor better organization and performance:os_log_info(bergenAppLog, "Application starting up"); os_log_error(bergenFileLog, "File not found: %{public}@", filePath); os_log_debug(bergenMenuLog, "Menu item selected: %{public}@", itemTitle);
- See the Logging with os_log section for detailed information
- For React Native bridge logging, use
RCTLogInfo(@"message")
- Use structured logging with
-
Common Debug Tools:
- Xcode's Debug Navigator
- Console.app for viewing structured logs with filtering options
- Instruments for performance analysis
If JavaScript cannot find your native module:
- Check the module is properly registered with
RCT_EXPORT_MODULE() - Restart the Metro bundler and app
- Verify the module name matches between native and JS code
If you see errors like "RCTCallableJSModules is not set":
- Make sure your RCTEventEmitter subclasses properly implement
setBridge: - Don't redeclare properties already provided by parent classes (e.g.,
bridgeproperty) - Add safety checks with a
isBridgeReadymethod before sending events - Ensure module's inheritance hierarchy is correct for its purpose (NSObject vs RCTEventEmitter)
If custom menu items don't appear:
- Verify the menu title exactly matches the existing menu (case-sensitive)
- Check that code is running on the main thread
- Inspect the menu structure using the Menu Editor in Xcode
Common build issues:
- Missing dependencies: Run
pod installin themacosdirectory - Linking errors: Ensure all native code is included in the Xcode project
- React Native version mismatch: Check compatibility with the RN macOS version
As the project evolves, consider:
- Mac Catalyst support for shared iOS/macOS code
- SwiftUI integration for modern UI components
- Apple Silicon optimizations
- Sandboxing for Mac App Store distribution
Bergen uses Apple's unified logging system (os_log) for structured logging in the native code. This provides better performance, organization, and filtering capabilities than traditional NSLog.
-
Subsystem and Categories
The app defines a logging subsystem in
Info.plist:<key>OSLogSubsystemIdentifier</key> <string>com.bergen.app</string>
And creates specialized logging categories:
// Define log categories os_log_t bergenAppLog = os_log_create("com.bergen.app", "app"); // General app events os_log_t bergenFileLog = os_log_create("com.bergen.app", "files"); // File operations os_log_t bergenMenuLog = os_log_create("com.bergen.app", "menu"); // Menu interactions
-
Log Levels
The logger supports different severity levels:
os_log_info(bergenAppLog, "Application starting up"); // Normal events os_log_debug(bergenFileLog, "Processing file data"); // Debug information os_log_error(bergenFileLog, "File not found: %{public}@", filePath); // Error conditions
-
Privacy Formatting
Use the following formatting for sensitive data:
// Public data (visible in logs) os_log_info(bergenFileLog, "File type: %{public}@", fileExtension); // Private data (redacted in logs unless special access) os_log_debug(bergenFileLog, "File content hash: %{private}@", contentHash);
To view Bergen's logs in Console.app:
- Open Console.app (located in
/Applications/Utilities/) - Select "bergen" from the sidebar under "App" section
- Filter logs using:
subsystem:com.bergen.app- All Bergen logscategory:files- Only file operation logscategory:menu- Only menu interaction logscategory:app- Only general application logs- Combine with level filters:
level:errororlevel:debug
- Performance: Minimal overhead compared to NSLog
- Organization: Categorized logs for easier debugging
- Filtering: Powerful filtering in Console.app
- Security: Privacy controls for sensitive information
- Integration: Works with macOS system logging
For example, to debug file opening issues, you can filter for category:files level:error in Console.app to quickly find problems.
This documentation will be updated as the native codebase evolves. Contributions to this guide are welcome.