From c00ca3f37163ff2e3889807cc77083106cf4bb4a Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Thu, 23 Apr 2026 16:02:03 -0700 Subject: [PATCH 1/5] feat(macos): migrate init template from Objective-C to Swift Replace the Objective-C/Storyboard-based macOS init template with a modern Swift implementation that aligns with the upstream React Native community template (https://github.com/react-native-community/template). Changes: - Replace AppDelegate.h/AppDelegate.mm with AppDelegate.swift using the new RCTReactNativeFactory pattern (matching upstream iOS template) - Replace main.m with main.swift for the app entry point - Remove Main.storyboard (684 lines of XML) in favor of a programmatic menu bar built in Swift - Update project.pbxproj to reference Swift source files - Remove NSMainStoryboardFile from Info.plist The new template uses: - RCTReactNativeFactory + ReactNativeDelegate (non-deprecated API) - NSObject + NSApplicationDelegate (instead of deprecated RCTAppDelegate) - Programmatic NSWindow creation (1280x720 default) - Programmatic menu bar: App, Edit, View, Window, Help menus - Same bundleURL/sourceURL pattern as upstream community template --- .../macos/HelloWorld-macOS/AppDelegate.h | 6 - .../macos/HelloWorld-macOS/AppDelegate.mm | 47 -- .../macos/HelloWorld-macOS/AppDelegate.swift | 138 ++++ .../Base.lproj/Main.storyboard | 684 ------------------ .../macos/HelloWorld-macOS/Info.plist | 2 - .../templates/macos/HelloWorld-macOS/main.m | 5 - .../macos/HelloWorld-macOS/main.swift | 5 + .../HelloWorld.xcodeproj/project.pbxproj | 34 +- 8 files changed, 151 insertions(+), 770 deletions(-) delete mode 100644 packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/AppDelegate.h delete mode 100644 packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/AppDelegate.mm create mode 100644 packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/AppDelegate.swift delete mode 100644 packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/Base.lproj/Main.storyboard delete mode 100644 packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/main.m create mode 100644 packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/main.swift diff --git a/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/AppDelegate.h b/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/AppDelegate.h deleted file mode 100644 index 63db973168d0..000000000000 --- a/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/AppDelegate.h +++ /dev/null @@ -1,6 +0,0 @@ -#import -#import - -@interface AppDelegate : RCTAppDelegate - -@end diff --git a/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/AppDelegate.mm b/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/AppDelegate.mm deleted file mode 100644 index ddea64bfd235..000000000000 --- a/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/AppDelegate.mm +++ /dev/null @@ -1,47 +0,0 @@ -#import "AppDelegate.h" - -#import -#import - -@implementation AppDelegate - -- (void)applicationDidFinishLaunching:(NSNotification *)notification -{ - self.moduleName = @"HelloWorld"; - // You can add your custom initial props in the dictionary below. - // They will be passed down to the ViewController used by React Native. - self.initialProps = @{}; - self.dependencyProvider = [RCTAppDependencyProvider new]; - - return [super applicationDidFinishLaunching:notification]; -} - -- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge -{ - return [self bundleURL]; -} - -- (NSURL *)bundleURL -{ -#if DEBUG - return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; -#else - return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; -#endif -} - -/// This method controls whether the `concurrentRoot`feature of React18 is turned on or off. -/// -/// @see: https://reactjs.org/blog/2022/03/29/react-v18.html -/// @note: This requires to be rendering on Fabric (i.e. on the New Architecture). -/// @return: `true` if the `concurrentRoot` feature is enabled. Otherwise, it returns `false`. -- (BOOL)concurrentRootEnabled -{ -#ifdef RN_FABRIC_ENABLED - return true; -#else - return false; -#endif -} - -@end diff --git a/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/AppDelegate.swift b/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/AppDelegate.swift new file mode 100644 index 000000000000..1fdbead1ba7c --- /dev/null +++ b/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/AppDelegate.swift @@ -0,0 +1,138 @@ +import Cocoa +import React +import React_RCTAppDelegate +import ReactAppDependencyProvider + +class AppDelegate: NSObject, NSApplicationDelegate { + var window: NSWindow? + var reactNativeDelegate: ReactNativeDelegate? + var reactNativeFactory: RCTReactNativeFactory? + + func applicationDidFinishLaunching(_ notification: Notification) { + let delegate = ReactNativeDelegate() + let factory = RCTReactNativeFactory(delegate: delegate) + delegate.dependencyProvider = RCTAppDependencyProvider() + + reactNativeDelegate = delegate + reactNativeFactory = factory + + window = NSWindow( + contentRect: NSMakeRect(0, 0, 1280, 720), + styleMask: [.titled, .closable, .resizable, .miniaturizable], + backing: .buffered, + defer: false + ) + + setupMainMenu() + + factory.startReactNative(withModuleName: "HelloWorld", in: window) + } + + // MARK: - Menu Bar + + private func setupMainMenu() { + let mainMenu = NSMenu() + + mainMenu.addItem(createAppMenu()) + mainMenu.addItem(createEditMenu()) + mainMenu.addItem(createViewMenu()) + mainMenu.addItem(createWindowMenu()) + mainMenu.addItem(createHelpMenu()) + + NSApp.mainMenu = mainMenu + } + + private func createAppMenu() -> NSMenuItem { + let menuItem = NSMenuItem() + let menu = NSMenu() + menuItem.submenu = menu + + menu.addItem(withTitle: "About HelloWorld", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: "") + menu.addItem(.separator()) + + let servicesItem = NSMenuItem(title: "Services", action: nil, keyEquivalent: "") + let servicesMenu = NSMenu(title: "Services") + servicesItem.submenu = servicesMenu + NSApp.servicesMenu = servicesMenu + menu.addItem(servicesItem) + menu.addItem(.separator()) + + menu.addItem(withTitle: "Hide HelloWorld", action: #selector(NSApplication.hide(_:)), keyEquivalent: "h") + let hideOthersItem = menu.addItem(withTitle: "Hide Others", action: #selector(NSApplication.hideOtherApplications(_:)), keyEquivalent: "h") + hideOthersItem.keyEquivalentModifierMask = [.command, .option] + menu.addItem(withTitle: "Show All", action: #selector(NSApplication.unhideAllApplications(_:)), keyEquivalent: "") + menu.addItem(.separator()) + menu.addItem(withTitle: "Quit HelloWorld", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q") + + return menuItem + } + + private func createEditMenu() -> NSMenuItem { + let menuItem = NSMenuItem() + let menu = NSMenu(title: "Edit") + menuItem.submenu = menu + + menu.addItem(withTitle: "Undo", action: Selector(("undo:")), keyEquivalent: "z") + menu.addItem(withTitle: "Redo", action: Selector(("redo:")), keyEquivalent: "Z") + menu.addItem(.separator()) + menu.addItem(withTitle: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x") + menu.addItem(withTitle: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c") + menu.addItem(withTitle: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v") + menu.addItem(withTitle: "Paste and Match Style", action: #selector(NSTextView.pasteAsPlainText(_:)), keyEquivalent: "V") + menu.addItem(withTitle: "Delete", action: #selector(NSText.delete(_:)), keyEquivalent: "") + menu.addItem(withTitle: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a") + + return menuItem + } + + private func createViewMenu() -> NSMenuItem { + let menuItem = NSMenuItem() + let menu = NSMenu(title: "View") + menuItem.submenu = menu + + let fullScreenItem = menu.addItem(withTitle: "Enter Full Screen", action: #selector(NSWindow.toggleFullScreen(_:)), keyEquivalent: "f") + fullScreenItem.keyEquivalentModifierMask = [.command, .control] + + return menuItem + } + + private func createWindowMenu() -> NSMenuItem { + let menuItem = NSMenuItem() + let menu = NSMenu(title: "Window") + menuItem.submenu = menu + + menu.addItem(withTitle: "Close", action: #selector(NSWindow.performClose(_:)), keyEquivalent: "w") + menu.addItem(withTitle: "Minimize", action: #selector(NSWindow.performMiniaturize(_:)), keyEquivalent: "m") + menu.addItem(withTitle: "Zoom", action: #selector(NSWindow.performZoom(_:)), keyEquivalent: "") + menu.addItem(.separator()) + menu.addItem(withTitle: "Bring All to Front", action: #selector(NSApplication.arrangeInFront(_:)), keyEquivalent: "") + + NSApp.windowsMenu = menu + + return menuItem + } + + private func createHelpMenu() -> NSMenuItem { + let menuItem = NSMenuItem() + let menu = NSMenu(title: "Help") + menuItem.submenu = menu + + NSApp.helpMenu = menu + + return menuItem + } +} + +class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate { + override func sourceURL(for bridge: RCTBridge) -> URL? { + bundleURL() + } + + override func bundleURL() -> URL? { +#if DEBUG + RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") +#else + Bundle.main.url(forResource: "main", withExtension: "jsbundle") +#endif + } +} diff --git a/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/Base.lproj/Main.storyboard b/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/Base.lproj/Main.storyboard deleted file mode 100644 index a3afa65f0c0e..000000000000 --- a/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/Base.lproj/Main.storyboard +++ /dev/null @@ -1,684 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Default - - - - - - - Left to Right - - - - - - - Right to Left - - - - - - - - - - - Default - - - - - - - Left to Right - - - - - - - Right to Left - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/Info.plist b/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/Info.plist index 5c7ebb780201..5a46616ce88d 100644 --- a/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/Info.plist +++ b/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/Info.plist @@ -35,8 +35,6 @@ - NSMainStoryboardFile - Main NSPrincipalClass NSApplication NSSupportsAutomaticTermination diff --git a/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/main.m b/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/main.m deleted file mode 100644 index 1f154fcf69b0..000000000000 --- a/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/main.m +++ /dev/null @@ -1,5 +0,0 @@ -#import - -int main(int argc, const char *argv[]) { - return NSApplicationMain(argc, argv); -} diff --git a/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/main.swift b/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/main.swift new file mode 100644 index 000000000000..f2e879b62567 --- /dev/null +++ b/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/main.swift @@ -0,0 +1,5 @@ +import Cocoa + +let delegate = AppDelegate() +NSApplication.shared.delegate = delegate +_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv) diff --git a/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld.xcodeproj/project.pbxproj b/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld.xcodeproj/project.pbxproj index 63b8b0b4f7c5..9d4a9b1b11b3 100644 --- a/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld.xcodeproj/project.pbxproj +++ b/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld.xcodeproj/project.pbxproj @@ -7,21 +7,18 @@ objects = { /* Begin PBXBuildFile section */ - 5142014D2437B4B30078DB4F /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5142014C2437B4B30078DB4F /* AppDelegate.mm */; }; + 5142014D2437B4B30078DB4F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5142014C2437B4B30078DB4F /* AppDelegate.swift */; }; 514201522437B4B40078DB4F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 514201512437B4B40078DB4F /* Assets.xcassets */; }; - 514201552437B4B40078DB4F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 514201532437B4B40078DB4F /* Main.storyboard */; }; - 514201582437B4B40078DB4F /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 514201572437B4B40078DB4F /* main.m */; }; + 514201582437B4B40078DB4F /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514201572437B4B40078DB4F /* main.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 13B07F961A680F5B00A75B9A /* HelloWorld.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HelloWorld.app; sourceTree = BUILT_PRODUCTS_DIR; }; 514201492437B4B30078DB4F /* HelloWorld.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HelloWorld.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 5142014B2437B4B30078DB4F /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 5142014C2437B4B30078DB4F /* AppDelegate.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AppDelegate.mm; sourceTree = ""; }; + 5142014C2437B4B30078DB4F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 514201512437B4B40078DB4F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 514201542437B4B40078DB4F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 514201572437B4B40078DB4F /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 514201562437B4B40078DB4F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 514201572437B4B40078DB4F /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 514201592437B4B40078DB4F /* HelloWorld.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = HelloWorld.entitlements; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ @@ -55,12 +52,10 @@ 5142014A2437B4B30078DB4F /* HelloWorld-macOS */ = { isa = PBXGroup; children = ( - 5142014B2437B4B30078DB4F /* AppDelegate.h */, - 5142014C2437B4B30078DB4F /* AppDelegate.mm */, + 5142014C2437B4B30078DB4F /* AppDelegate.swift */, + 514201572437B4B40078DB4F /* main.swift */, 514201512437B4B40078DB4F /* Assets.xcassets */, - 514201532437B4B40078DB4F /* Main.storyboard */, 514201562437B4B40078DB4F /* Info.plist */, - 514201572437B4B40078DB4F /* main.m */, 514201592437B4B40078DB4F /* HelloWorld.entitlements */, ); path = "HelloWorld-macOS"; @@ -159,7 +154,6 @@ hasScannedForEncodings = 0; knownRegions = ( en, - Base, ); mainGroup = 83CBB9F61A601CBA00E9B192; productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; @@ -185,7 +179,6 @@ buildActionMask = 2147483647; files = ( 514201522437B4B40078DB4F /* Assets.xcassets in Resources */, - 514201552437B4B40078DB4F /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -282,24 +275,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 514201582437B4B40078DB4F /* main.m in Sources */, - 5142014D2437B4B30078DB4F /* AppDelegate.mm in Sources */, + 514201582437B4B40078DB4F /* main.swift in Sources */, + 5142014D2437B4B30078DB4F /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ -/* Begin PBXVariantGroup section */ - 514201532437B4B40078DB4F /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 514201542437B4B40078DB4F /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - /* Begin XCBuildConfiguration section */ 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; From 9fbf879eac016e237378ec05e6aefad0f8126e56 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Thu, 23 Apr 2026 16:41:26 -0700 Subject: [PATCH 2/5] refactor(macos): use @resultBuilder for declarative menu creation Replace imperative NSMenu/NSMenuItem creation with a small @resultBuilder DSL that reads almost like SwiftUI: NSApp.mainMenu = NSMenu { NSMenuItem { NSMenu("Edit") { NSMenuItem("Undo", action: Selector(("undo:")), key: "z") NSMenuItem.separator() NSMenuItem("Cut", action: #selector(NSText.cut(_:)), key: "x") } } } The builder infrastructure is ~30 lines: - MenuBuilder: collects NSMenuItem instances into an array - SubMenuBuilder: wraps a single NSMenu as a submenu - NSMenu convenience init with builder closure - NSMenuItem convenience inits and .keyModifiers() chainable helper --- .../macos/HelloWorld-macOS/AppDelegate.swift | 185 ++++++++++-------- 1 file changed, 101 insertions(+), 84 deletions(-) diff --git a/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/AppDelegate.swift b/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/AppDelegate.swift index 1fdbead1ba7c..ae3c8af73506 100644 --- a/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/AppDelegate.swift +++ b/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/AppDelegate.swift @@ -31,108 +31,125 @@ class AppDelegate: NSObject, NSApplicationDelegate { // MARK: - Menu Bar private func setupMainMenu() { - let mainMenu = NSMenu() - - mainMenu.addItem(createAppMenu()) - mainMenu.addItem(createEditMenu()) - mainMenu.addItem(createViewMenu()) - mainMenu.addItem(createWindowMenu()) - mainMenu.addItem(createHelpMenu()) - - NSApp.mainMenu = mainMenu - } - - private func createAppMenu() -> NSMenuItem { - let menuItem = NSMenuItem() - let menu = NSMenu() - menuItem.submenu = menu + let servicesMenu = NSMenu(title: "Services") - menu.addItem(withTitle: "About HelloWorld", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: "") - menu.addItem(.separator()) + NSApp.mainMenu = NSMenu { + NSMenuItem { + NSMenu { + NSMenuItem("About HelloWorld", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:))) + NSMenuItem.separator() + NSMenuItem("Services", submenu: servicesMenu) + NSMenuItem.separator() + NSMenuItem("Hide HelloWorld", action: #selector(NSApplication.hide(_:)), key: "h") + NSMenuItem("Hide Others", action: #selector(NSApplication.hideOtherApplications(_:)), key: "h") + .keyModifiers([.command, .option]) + NSMenuItem("Show All", action: #selector(NSApplication.unhideAllApplications(_:))) + NSMenuItem.separator() + NSMenuItem("Quit HelloWorld", action: #selector(NSApplication.terminate(_:)), key: "q") + } + } + NSMenuItem { + NSMenu("Edit") { + NSMenuItem("Undo", action: Selector(("undo:")), key: "z") + NSMenuItem("Redo", action: Selector(("redo:")), key: "Z") + NSMenuItem.separator() + NSMenuItem("Cut", action: #selector(NSText.cut(_:)), key: "x") + NSMenuItem("Copy", action: #selector(NSText.copy(_:)), key: "c") + NSMenuItem("Paste", action: #selector(NSText.paste(_:)), key: "v") + NSMenuItem("Paste and Match Style", action: #selector(NSTextView.pasteAsPlainText(_:)), key: "V") + .keyModifiers([.command, .option]) + NSMenuItem("Delete", action: #selector(NSText.delete(_:))) + NSMenuItem("Select All", action: #selector(NSText.selectAll(_:)), key: "a") + } + } + NSMenuItem { + NSMenu("View") { + NSMenuItem("Enter Full Screen", action: #selector(NSWindow.toggleFullScreen(_:)), key: "f") + .keyModifiers([.command, .control]) + } + } + NSMenuItem { + NSMenu("Window") { + NSMenuItem("Close", action: #selector(NSWindow.performClose(_:)), key: "w") + NSMenuItem("Minimize", action: #selector(NSWindow.performMiniaturize(_:)), key: "m") + NSMenuItem("Zoom", action: #selector(NSWindow.performZoom(_:))) + NSMenuItem.separator() + NSMenuItem("Bring All to Front", action: #selector(NSApplication.arrangeInFront(_:))) + } + } + NSMenuItem { + NSMenu("Help") {} + } + } - let servicesItem = NSMenuItem(title: "Services", action: nil, keyEquivalent: "") - let servicesMenu = NSMenu(title: "Services") - servicesItem.submenu = servicesMenu NSApp.servicesMenu = servicesMenu - menu.addItem(servicesItem) - menu.addItem(.separator()) + NSApp.windowsMenu = NSApp.mainMenu?.item(withTitle: "Window")?.submenu + NSApp.helpMenu = NSApp.mainMenu?.item(withTitle: "Help")?.submenu + } +} - menu.addItem(withTitle: "Hide HelloWorld", action: #selector(NSApplication.hide(_:)), keyEquivalent: "h") - let hideOthersItem = menu.addItem(withTitle: "Hide Others", action: #selector(NSApplication.hideOtherApplications(_:)), keyEquivalent: "h") - hideOthersItem.keyEquivalentModifierMask = [.command, .option] - menu.addItem(withTitle: "Show All", action: #selector(NSApplication.unhideAllApplications(_:)), keyEquivalent: "") - menu.addItem(.separator()) - menu.addItem(withTitle: "Quit HelloWorld", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q") +// MARK: - React Native Delegate - return menuItem +class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate { + override func sourceURL(for bridge: RCTBridge) -> URL? { + bundleURL() } - private func createEditMenu() -> NSMenuItem { - let menuItem = NSMenuItem() - let menu = NSMenu(title: "Edit") - menuItem.submenu = menu - - menu.addItem(withTitle: "Undo", action: Selector(("undo:")), keyEquivalent: "z") - menu.addItem(withTitle: "Redo", action: Selector(("redo:")), keyEquivalent: "Z") - menu.addItem(.separator()) - menu.addItem(withTitle: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x") - menu.addItem(withTitle: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c") - menu.addItem(withTitle: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v") - menu.addItem(withTitle: "Paste and Match Style", action: #selector(NSTextView.pasteAsPlainText(_:)), keyEquivalent: "V") - menu.addItem(withTitle: "Delete", action: #selector(NSText.delete(_:)), keyEquivalent: "") - menu.addItem(withTitle: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a") - - return menuItem + override func bundleURL() -> URL? { +#if DEBUG + RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") +#else + Bundle.main.url(forResource: "main", withExtension: "jsbundle") +#endif } +} - private func createViewMenu() -> NSMenuItem { - let menuItem = NSMenuItem() - let menu = NSMenu(title: "View") - menuItem.submenu = menu - - let fullScreenItem = menu.addItem(withTitle: "Enter Full Screen", action: #selector(NSWindow.toggleFullScreen(_:)), keyEquivalent: "f") - fullScreenItem.keyEquivalentModifierMask = [.command, .control] +// MARK: - Declarative Menu Builder - return menuItem +/// A result builder that collects `NSMenuItem` instances into an array. +@resultBuilder +enum MenuBuilder { + static func buildBlock(_ items: NSMenuItem...) -> [NSMenuItem] { + Array(items) } +} - private func createWindowMenu() -> NSMenuItem { - let menuItem = NSMenuItem() - let menu = NSMenu(title: "Window") - menuItem.submenu = menu - - menu.addItem(withTitle: "Close", action: #selector(NSWindow.performClose(_:)), keyEquivalent: "w") - menu.addItem(withTitle: "Minimize", action: #selector(NSWindow.performMiniaturize(_:)), keyEquivalent: "m") - menu.addItem(withTitle: "Zoom", action: #selector(NSWindow.performZoom(_:)), keyEquivalent: "") - menu.addItem(.separator()) - menu.addItem(withTitle: "Bring All to Front", action: #selector(NSApplication.arrangeInFront(_:)), keyEquivalent: "") - - NSApp.windowsMenu = menu +/// A result builder that expects a single `NSMenu` expression. +@resultBuilder +enum SubMenuBuilder { + static func buildBlock(_ menu: NSMenu) -> NSMenu { menu } +} - return menuItem +extension NSMenu { + /// Creates an `NSMenu` with items provided by a ``MenuBuilder``. + convenience init(_ title: String = "", @MenuBuilder builder: () -> [NSMenuItem]) { + self.init(title: title) + self.items = builder() } +} - private func createHelpMenu() -> NSMenuItem { - let menuItem = NSMenuItem() - let menu = NSMenu(title: "Help") - menuItem.submenu = menu - - NSApp.helpMenu = menu - - return menuItem +extension NSMenuItem { + /// Creates an `NSMenuItem` with a shorter parameter name for `keyEquivalent` + /// and an optional submenu. + convenience init( + _ title: String, + action: Selector? = nil, + key: String = "", + submenu: NSMenu? = nil + ) { + self.init(title: title, action: action, keyEquivalent: key) + self.submenu = submenu } -} -class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate { - override func sourceURL(for bridge: RCTBridge) -> URL? { - bundleURL() + /// Creates an `NSMenuItem` whose submenu is built by a ``SubMenuBuilder``. + convenience init(@SubMenuBuilder builder: () -> NSMenu) { + self.init(title: "", action: nil, keyEquivalent: "") + self.submenu = builder() } - override func bundleURL() -> URL? { -#if DEBUG - RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") -#else - Bundle.main.url(forResource: "main", withExtension: "jsbundle") -#endif + /// Sets custom modifier keys on this menu item and returns it for chaining. + func keyModifiers(_ modifiers: NSEvent.ModifierFlags) -> NSMenuItem { + keyEquivalentModifierMask = modifiers + return self } } From a2e782591f6b5cecb881fa127a90e675de5746a3 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Thu, 23 Apr 2026 17:10:34 -0700 Subject: [PATCH 3/5] refactor(macos): extract menu into NSMenu.standardMenu(appName:) factory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AppKit doesn't provide default menus programmatically (that was the storyboard's job), but keyboard shortcuts like ⌘C, ⌘V, ⌘Q, ⌘W only work when a corresponding NSMenuItem exists. So we need the menus, but they don't belong inline in AppDelegate. Move all standard menu construction into a single factory method: NSApp.mainMenu = .standardMenu(appName: "HelloWorld") This keeps AppDelegate focused on React Native setup while the menu boilerplate lives in a self-contained NSMenu extension that's easy to customize or replace. --- .../macos/HelloWorld-macOS/AppDelegate.swift | 220 ++++++++++-------- 1 file changed, 123 insertions(+), 97 deletions(-) diff --git a/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/AppDelegate.swift b/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/AppDelegate.swift index ae3c8af73506..67b39b2c507b 100644 --- a/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/AppDelegate.swift +++ b/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/AppDelegate.swift @@ -23,69 +23,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { defer: false ) - setupMainMenu() + NSApp.mainMenu = .standardMenu(appName: "HelloWorld") factory.startReactNative(withModuleName: "HelloWorld", in: window) } - - // MARK: - Menu Bar - - private func setupMainMenu() { - let servicesMenu = NSMenu(title: "Services") - - NSApp.mainMenu = NSMenu { - NSMenuItem { - NSMenu { - NSMenuItem("About HelloWorld", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:))) - NSMenuItem.separator() - NSMenuItem("Services", submenu: servicesMenu) - NSMenuItem.separator() - NSMenuItem("Hide HelloWorld", action: #selector(NSApplication.hide(_:)), key: "h") - NSMenuItem("Hide Others", action: #selector(NSApplication.hideOtherApplications(_:)), key: "h") - .keyModifiers([.command, .option]) - NSMenuItem("Show All", action: #selector(NSApplication.unhideAllApplications(_:))) - NSMenuItem.separator() - NSMenuItem("Quit HelloWorld", action: #selector(NSApplication.terminate(_:)), key: "q") - } - } - NSMenuItem { - NSMenu("Edit") { - NSMenuItem("Undo", action: Selector(("undo:")), key: "z") - NSMenuItem("Redo", action: Selector(("redo:")), key: "Z") - NSMenuItem.separator() - NSMenuItem("Cut", action: #selector(NSText.cut(_:)), key: "x") - NSMenuItem("Copy", action: #selector(NSText.copy(_:)), key: "c") - NSMenuItem("Paste", action: #selector(NSText.paste(_:)), key: "v") - NSMenuItem("Paste and Match Style", action: #selector(NSTextView.pasteAsPlainText(_:)), key: "V") - .keyModifiers([.command, .option]) - NSMenuItem("Delete", action: #selector(NSText.delete(_:))) - NSMenuItem("Select All", action: #selector(NSText.selectAll(_:)), key: "a") - } - } - NSMenuItem { - NSMenu("View") { - NSMenuItem("Enter Full Screen", action: #selector(NSWindow.toggleFullScreen(_:)), key: "f") - .keyModifiers([.command, .control]) - } - } - NSMenuItem { - NSMenu("Window") { - NSMenuItem("Close", action: #selector(NSWindow.performClose(_:)), key: "w") - NSMenuItem("Minimize", action: #selector(NSWindow.performMiniaturize(_:)), key: "m") - NSMenuItem("Zoom", action: #selector(NSWindow.performZoom(_:))) - NSMenuItem.separator() - NSMenuItem("Bring All to Front", action: #selector(NSApplication.arrangeInFront(_:))) - } - } - NSMenuItem { - NSMenu("Help") {} - } - } - - NSApp.servicesMenu = servicesMenu - NSApp.windowsMenu = NSApp.mainMenu?.item(withTitle: "Window")?.submenu - NSApp.helpMenu = NSApp.mainMenu?.item(withTitle: "Help")?.submenu - } } // MARK: - React Native Delegate @@ -104,52 +45,137 @@ class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate { } } -// MARK: - Declarative Menu Builder +// MARK: - Standard macOS Menu Bar -/// A result builder that collects `NSMenuItem` instances into an array. -@resultBuilder -enum MenuBuilder { - static func buildBlock(_ items: NSMenuItem...) -> [NSMenuItem] { - Array(items) +extension NSMenu { + /// Creates a standard macOS menu bar with App, Edit, View, Window, and Help menus. + /// + /// This provides the default set of menu items that most macOS apps need, including + /// keyboard shortcuts for Quit (⌘Q), Close (⌘W), Copy (⌘C), Paste (⌘V), and others. + /// You can customize the returned menu by adding, removing, or modifying items. + static func standardMenu(appName: String) -> NSMenu { + let mainMenu = NSMenu() + + mainMenu.addItem(appMenuItem(appName: appName)) + mainMenu.addItem(editMenuItem()) + mainMenu.addItem(viewMenuItem()) + mainMenu.addItem(windowMenuItem()) + mainMenu.addItem(helpMenuItem()) + + return mainMenu } -} -/// A result builder that expects a single `NSMenu` expression. -@resultBuilder -enum SubMenuBuilder { - static func buildBlock(_ menu: NSMenu) -> NSMenu { menu } -} + // MARK: App Menu -extension NSMenu { - /// Creates an `NSMenu` with items provided by a ``MenuBuilder``. - convenience init(_ title: String = "", @MenuBuilder builder: () -> [NSMenuItem]) { - self.init(title: title) - self.items = builder() + private static func appMenuItem(appName: String) -> NSMenuItem { + let item = NSMenuItem() + let menu = NSMenu() + + menu.addItem(title: "About \(appName)", + action: #selector(NSApplication.orderFrontStandardAboutPanel(_:))) + menu.addItem(.separator()) + + let servicesMenu = NSMenu(title: "Services") + let servicesItem = NSMenuItem(title: "Services", action: nil, keyEquivalent: "") + servicesItem.submenu = servicesMenu + menu.addItem(servicesItem) + NSApp.servicesMenu = servicesMenu + menu.addItem(.separator()) + + menu.addItem(title: "Hide \(appName)", + action: #selector(NSApplication.hide(_:)), key: "h") + menu.addItem(title: "Hide Others", + action: #selector(NSApplication.hideOtherApplications(_:)), + key: "h", modifiers: [.command, .option]) + menu.addItem(title: "Show All", + action: #selector(NSApplication.unhideAllApplications(_:))) + menu.addItem(.separator()) + menu.addItem(title: "Quit \(appName)", + action: #selector(NSApplication.terminate(_:)), key: "q") + + item.submenu = menu + return item } -} -extension NSMenuItem { - /// Creates an `NSMenuItem` with a shorter parameter name for `keyEquivalent` - /// and an optional submenu. - convenience init( - _ title: String, - action: Selector? = nil, - key: String = "", - submenu: NSMenu? = nil - ) { - self.init(title: title, action: action, keyEquivalent: key) - self.submenu = submenu + // MARK: Edit Menu + + private static func editMenuItem() -> NSMenuItem { + let item = NSMenuItem() + let menu = NSMenu(title: "Edit") + + menu.addItem(title: "Undo", action: Selector(("undo:")), key: "z") + menu.addItem(title: "Redo", action: Selector(("redo:")), key: "Z") + menu.addItem(.separator()) + menu.addItem(title: "Cut", action: #selector(NSText.cut(_:)), key: "x") + menu.addItem(title: "Copy", action: #selector(NSText.copy(_:)), key: "c") + menu.addItem(title: "Paste", action: #selector(NSText.paste(_:)), key: "v") + menu.addItem(title: "Paste and Match Style", + action: #selector(NSTextView.pasteAsPlainText(_:)), + key: "v", modifiers: [.command, .option]) + menu.addItem(title: "Delete", action: #selector(NSText.delete(_:))) + menu.addItem(title: "Select All", action: #selector(NSText.selectAll(_:)), key: "a") + + item.submenu = menu + return item } - /// Creates an `NSMenuItem` whose submenu is built by a ``SubMenuBuilder``. - convenience init(@SubMenuBuilder builder: () -> NSMenu) { - self.init(title: "", action: nil, keyEquivalent: "") - self.submenu = builder() + // MARK: View Menu + + private static func viewMenuItem() -> NSMenuItem { + let item = NSMenuItem() + let menu = NSMenu(title: "View") + + menu.addItem(title: "Enter Full Screen", + action: #selector(NSWindow.toggleFullScreen(_:)), + key: "f", modifiers: [.command, .control]) + + item.submenu = menu + return item + } + + // MARK: Window Menu + + private static func windowMenuItem() -> NSMenuItem { + let item = NSMenuItem() + let menu = NSMenu(title: "Window") + + menu.addItem(title: "Close", action: #selector(NSWindow.performClose(_:)), key: "w") + menu.addItem(title: "Minimize", action: #selector(NSWindow.performMiniaturize(_:)), key: "m") + menu.addItem(title: "Zoom", action: #selector(NSWindow.performZoom(_:))) + menu.addItem(.separator()) + menu.addItem(title: "Bring All to Front", + action: #selector(NSApplication.arrangeInFront(_:))) + + NSApp.windowsMenu = menu + + item.submenu = menu + return item + } + + // MARK: Help Menu + + private static func helpMenuItem() -> NSMenuItem { + let item = NSMenuItem() + let menu = NSMenu(title: "Help") + + NSApp.helpMenu = menu + + item.submenu = menu + return item } - /// Sets custom modifier keys on this menu item and returns it for chaining. - func keyModifiers(_ modifiers: NSEvent.ModifierFlags) -> NSMenuItem { - keyEquivalentModifierMask = modifiers - return self + // MARK: Convenience + + /// Adds a menu item with an optional keyboard shortcut and modifier keys. + @discardableResult + fileprivate func addItem( + title: String, + action: Selector?, + key: String = "", + modifiers: NSEvent.ModifierFlags = .command + ) -> NSMenuItem { + let item = addItem(withTitle: title, action: action, keyEquivalent: key) + item.keyEquivalentModifierMask = modifiers + return item } } From 049bf8508a74125192ee4813510e68db29318092 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Fri, 24 Apr 2026 14:28:07 -0700 Subject: [PATCH 4/5] refactor(macos): adopt SwiftUI App lifecycle with NSViewRepresentable Replace the AppKit-based AppDelegate template with a SwiftUI App that hosts the React Native root view via NSViewRepresentable. - Use @main on a SwiftUI App struct; remove main.swift - Use Window scene with .defaultSize(1280x720) instead of manual NSWindow - Wrap factory.rootViewFactory.view(withModuleName:) in NSViewRepresentable - Drop the ~130 line NSMenu.standardMenu extension; SwiftUI provides the standard App, Edit, View, Window, and Help menus by default - Initialize RCTReactNativeFactory in AppDelegate.init() instead of applicationDidFinishLaunching, so it's ready when the scene body evaluates Reduces the template AppDelegate from 181 to 59 lines. --- .../macos/HelloWorld-macOS/AppDelegate.swift | 174 +++--------------- .../macos/HelloWorld-macOS/main.swift | 5 - .../HelloWorld.xcodeproj/project.pbxproj | 8 +- 3 files changed, 30 insertions(+), 157 deletions(-) delete mode 100644 packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/main.swift diff --git a/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/AppDelegate.swift b/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/AppDelegate.swift index 67b39b2c507b..2cee372d97a1 100644 --- a/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/AppDelegate.swift +++ b/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/AppDelegate.swift @@ -1,31 +1,32 @@ -import Cocoa +import SwiftUI import React import React_RCTAppDelegate import ReactAppDependencyProvider +@main +struct HelloWorldApp: App { + @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + + var body: some Scene { + Window("HelloWorld", id: "main") { + ReactNativeView(factory: appDelegate.reactNativeFactory) + } + .defaultSize(width: 1280, height: 720) + } +} + +// MARK: - App Delegate + class AppDelegate: NSObject, NSApplicationDelegate { - var window: NSWindow? - var reactNativeDelegate: ReactNativeDelegate? - var reactNativeFactory: RCTReactNativeFactory? + private let reactNativeDelegate: ReactNativeDelegate + let reactNativeFactory: RCTReactNativeFactory - func applicationDidFinishLaunching(_ notification: Notification) { + override init() { let delegate = ReactNativeDelegate() - let factory = RCTReactNativeFactory(delegate: delegate) delegate.dependencyProvider = RCTAppDependencyProvider() - reactNativeDelegate = delegate - reactNativeFactory = factory - - window = NSWindow( - contentRect: NSMakeRect(0, 0, 1280, 720), - styleMask: [.titled, .closable, .resizable, .miniaturizable], - backing: .buffered, - defer: false - ) - - NSApp.mainMenu = .standardMenu(appName: "HelloWorld") - - factory.startReactNative(withModuleName: "HelloWorld", in: window) + reactNativeFactory = RCTReactNativeFactory(delegate: delegate) + super.init() } } @@ -45,137 +46,14 @@ class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate { } } -// MARK: - Standard macOS Menu Bar - -extension NSMenu { - /// Creates a standard macOS menu bar with App, Edit, View, Window, and Help menus. - /// - /// This provides the default set of menu items that most macOS apps need, including - /// keyboard shortcuts for Quit (⌘Q), Close (⌘W), Copy (⌘C), Paste (⌘V), and others. - /// You can customize the returned menu by adding, removing, or modifying items. - static func standardMenu(appName: String) -> NSMenu { - let mainMenu = NSMenu() - - mainMenu.addItem(appMenuItem(appName: appName)) - mainMenu.addItem(editMenuItem()) - mainMenu.addItem(viewMenuItem()) - mainMenu.addItem(windowMenuItem()) - mainMenu.addItem(helpMenuItem()) - - return mainMenu - } - - // MARK: App Menu - - private static func appMenuItem(appName: String) -> NSMenuItem { - let item = NSMenuItem() - let menu = NSMenu() - - menu.addItem(title: "About \(appName)", - action: #selector(NSApplication.orderFrontStandardAboutPanel(_:))) - menu.addItem(.separator()) - - let servicesMenu = NSMenu(title: "Services") - let servicesItem = NSMenuItem(title: "Services", action: nil, keyEquivalent: "") - servicesItem.submenu = servicesMenu - menu.addItem(servicesItem) - NSApp.servicesMenu = servicesMenu - menu.addItem(.separator()) - - menu.addItem(title: "Hide \(appName)", - action: #selector(NSApplication.hide(_:)), key: "h") - menu.addItem(title: "Hide Others", - action: #selector(NSApplication.hideOtherApplications(_:)), - key: "h", modifiers: [.command, .option]) - menu.addItem(title: "Show All", - action: #selector(NSApplication.unhideAllApplications(_:))) - menu.addItem(.separator()) - menu.addItem(title: "Quit \(appName)", - action: #selector(NSApplication.terminate(_:)), key: "q") - - item.submenu = menu - return item - } - - // MARK: Edit Menu - - private static func editMenuItem() -> NSMenuItem { - let item = NSMenuItem() - let menu = NSMenu(title: "Edit") - - menu.addItem(title: "Undo", action: Selector(("undo:")), key: "z") - menu.addItem(title: "Redo", action: Selector(("redo:")), key: "Z") - menu.addItem(.separator()) - menu.addItem(title: "Cut", action: #selector(NSText.cut(_:)), key: "x") - menu.addItem(title: "Copy", action: #selector(NSText.copy(_:)), key: "c") - menu.addItem(title: "Paste", action: #selector(NSText.paste(_:)), key: "v") - menu.addItem(title: "Paste and Match Style", - action: #selector(NSTextView.pasteAsPlainText(_:)), - key: "v", modifiers: [.command, .option]) - menu.addItem(title: "Delete", action: #selector(NSText.delete(_:))) - menu.addItem(title: "Select All", action: #selector(NSText.selectAll(_:)), key: "a") - - item.submenu = menu - return item - } - - // MARK: View Menu - - private static func viewMenuItem() -> NSMenuItem { - let item = NSMenuItem() - let menu = NSMenu(title: "View") - - menu.addItem(title: "Enter Full Screen", - action: #selector(NSWindow.toggleFullScreen(_:)), - key: "f", modifiers: [.command, .control]) - - item.submenu = menu - return item - } - - // MARK: Window Menu - - private static func windowMenuItem() -> NSMenuItem { - let item = NSMenuItem() - let menu = NSMenu(title: "Window") - - menu.addItem(title: "Close", action: #selector(NSWindow.performClose(_:)), key: "w") - menu.addItem(title: "Minimize", action: #selector(NSWindow.performMiniaturize(_:)), key: "m") - menu.addItem(title: "Zoom", action: #selector(NSWindow.performZoom(_:))) - menu.addItem(.separator()) - menu.addItem(title: "Bring All to Front", - action: #selector(NSApplication.arrangeInFront(_:))) - - NSApp.windowsMenu = menu - - item.submenu = menu - return item - } - - // MARK: Help Menu - - private static func helpMenuItem() -> NSMenuItem { - let item = NSMenuItem() - let menu = NSMenu(title: "Help") +// MARK: - React Native SwiftUI View - NSApp.helpMenu = menu +struct ReactNativeView: NSViewRepresentable { + let factory: RCTReactNativeFactory - item.submenu = menu - return item + func makeNSView(context: Context) -> NSView { + factory.rootViewFactory.view(withModuleName: "HelloWorld") } - // MARK: Convenience - - /// Adds a menu item with an optional keyboard shortcut and modifier keys. - @discardableResult - fileprivate func addItem( - title: String, - action: Selector?, - key: String = "", - modifiers: NSEvent.ModifierFlags = .command - ) -> NSMenuItem { - let item = addItem(withTitle: title, action: action, keyEquivalent: key) - item.keyEquivalentModifierMask = modifiers - return item - } + func updateNSView(_ nsView: NSView, context: Context) {} } diff --git a/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/main.swift b/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/main.swift deleted file mode 100644 index f2e879b62567..000000000000 --- a/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/main.swift +++ /dev/null @@ -1,5 +0,0 @@ -import Cocoa - -let delegate = AppDelegate() -NSApplication.shared.delegate = delegate -_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv) diff --git a/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld.xcodeproj/project.pbxproj b/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld.xcodeproj/project.pbxproj index 9d4a9b1b11b3..452236213426 100644 --- a/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld.xcodeproj/project.pbxproj +++ b/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld.xcodeproj/project.pbxproj @@ -9,7 +9,7 @@ /* Begin PBXBuildFile section */ 5142014D2437B4B30078DB4F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5142014C2437B4B30078DB4F /* AppDelegate.swift */; }; 514201522437B4B40078DB4F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 514201512437B4B40078DB4F /* Assets.xcassets */; }; - 514201582437B4B40078DB4F /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514201572437B4B40078DB4F /* main.swift */; }; + /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -17,7 +17,7 @@ 514201492437B4B30078DB4F /* HelloWorld.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HelloWorld.app; sourceTree = BUILT_PRODUCTS_DIR; }; 5142014C2437B4B30078DB4F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 514201512437B4B40078DB4F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 514201572437B4B40078DB4F /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; + 514201562437B4B40078DB4F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 514201592437B4B40078DB4F /* HelloWorld.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = HelloWorld.entitlements; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; @@ -53,7 +53,7 @@ isa = PBXGroup; children = ( 5142014C2437B4B30078DB4F /* AppDelegate.swift */, - 514201572437B4B40078DB4F /* main.swift */, + 514201512437B4B40078DB4F /* Assets.xcassets */, 514201562437B4B40078DB4F /* Info.plist */, 514201592437B4B40078DB4F /* HelloWorld.entitlements */, @@ -275,7 +275,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 514201582437B4B40078DB4F /* main.swift in Sources */, + 5142014D2437B4B30078DB4F /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; From a7d14f328bdee30caef6a042e1c9ceeafefd97c0 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Mon, 27 Apr 2026 10:33:26 -0700 Subject: [PATCH 5/5] Apply suggestion from @Saadnajmi --- .../templates/macos/HelloWorld-macOS/AppDelegate.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/AppDelegate.swift b/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/AppDelegate.swift index 2cee372d97a1..1a9040193722 100644 --- a/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/AppDelegate.swift +++ b/packages/react-native/local-cli/generator-macos/templates/macos/HelloWorld-macOS/AppDelegate.swift @@ -22,11 +22,14 @@ class AppDelegate: NSObject, NSApplicationDelegate { let reactNativeFactory: RCTReactNativeFactory override init() { + super.init() + let delegate = ReactNativeDelegate() + let factory = RCTReactNativeFactory(delegate: delegate) delegate.dependencyProvider = RCTAppDependencyProvider() + reactNativeDelegate = delegate - reactNativeFactory = RCTReactNativeFactory(delegate: delegate) - super.init() + reactNativeFactory = factory } }