Skip to content

feat(macos): migrate init template from Objective-C to Swift#2948

Draft
Saadnajmi wants to merge 5 commits intomicrosoft:mainfrom
Saadnajmi:swift-init-template
Draft

feat(macos): migrate init template from Objective-C to Swift#2948
Saadnajmi wants to merge 5 commits intomicrosoft:mainfrom
Saadnajmi:swift-init-template

Conversation

@Saadnajmi
Copy link
Copy Markdown
Collaborator

@Saadnajmi Saadnajmi commented Apr 23, 2026

Summary

Replaces the Objective-C/Storyboard-based macOS init template with a modern SwiftUI implementation. The app delegate hosts the React Native root view inside a SwiftUI Window scene via NSViewRepresentable, so the template gets standard macOS menus, window management, and lifecycle handling for free from SwiftUI.

Net diff vs main: ~219 insertions, ~770 deletions — most of the reduction comes from removing the 684-line Main.storyboard XML and the ~130-line programmatic NSMenu builder.

Why SwiftUI?

When replacing the Storyboard, both pure AppKit and SwiftUI were on the table:

  • Pure AppKit required hand-rolling the entire menu bar (~130 lines of NSMenuItem boilerplate) and manually creating an NSWindow.
  • SwiftUI provides standard App, Edit, View, Window, and Help menus by default through WindowGroup/Window, plus declarative window sizing via .defaultSize(...).

Because React Native's view is an NSView, embedding it in SwiftUI is a one-liner with NSViewRepresentable — no awkward bridging.

Changes

Files removed

  • AppDelegate.h / AppDelegate.mm — replaced by AppDelegate.swift
  • main.m — replaced by @main on the SwiftUI App struct (no main.swift needed)
  • Base.lproj/Main.storyboard (684 lines of XML) — replaced by SwiftUI

Files added

  • AppDelegate.swift (59 lines) — contains:
    • @main struct HelloWorldApp: App with a Window scene, .defaultSize(width: 1280, height: 720)
    • AppDelegate: NSObject, NSApplicationDelegate wired via @NSApplicationDelegateAdaptor, initializing RCTReactNativeFactory in init()
    • ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate for bundleURL() / sourceURL(for:)
    • ReactNativeView: NSViewRepresentable wrapping factory.rootViewFactory.view(withModuleName:)

Files modified

  • Info.plist — removed NSMainStoryboardFile key
  • project.pbxproj — updated file references from ObjC/storyboard to Swift; removed main.swift references in favor of @main

Architecture alignment with upstream

Aspect Upstream iOS This PR (macOS)
Language Swift Swift ✅
App lifecycle UIKit UIApplicationDelegate SwiftUI App + NSApplicationDelegate adaptor
AppDelegate base UIResponder, UIApplicationDelegate NSObject, NSApplicationDelegate
Factory pattern RCTReactNativeFactory + ReactNativeDelegate Same ✅
RCTAppDelegate (deprecated) Not used Not used ✅
Dependency provider RCTAppDependencyProvider() Same ✅
bundleURL() / sourceURL(for:) Debug→bundler, Release→jsbundle Same ✅
Storyboard None (uses @main) None (uses @main) ✅
Window creation UIWindow(frame:) SwiftUI Window scene
RN view hosting RCTRootView directly NSViewRepresentableRCTRootView
Menu bar N/A (iOS) Provided by SwiftUI defaults
Entry point Generated by @main Generated by @main

Menu bar

SwiftUI provides the standard macOS menu bar automatically with no code:

  • App: About, Settings, Services, Hide, Hide Others, Show All, Quit
  • Edit: Undo, Redo, Cut, Copy, Paste, Delete, Select All
  • View: Show/Hide Toolbar, Customize Toolbar, Enter Full Screen
  • Window: Minimize, Zoom, Close, Bring All to Front
  • Help: Standard system-managed

Using Window (instead of WindowGroup) intentionally omits the "New Window" command, which is appropriate for a single-window React Native app.

Testing

  • ✅ Template generator produces correct output with project name substitution
  • pod install succeeds
  • AppDelegate.swift compiles cleanly
  • ✅ Full build succeeds with the new architecture (RCT_NEW_ARCH_ENABLED=1)
  • ✅ App launches, Metro connects, and the JS bundle (NewAppScreen) renders inside the SwiftUI Window
  • ✅ Standard menus (Quit ⌘Q, Copy ⌘C, Paste ⌘V, Enter Full Screen ⌃⌘F, Minimize ⌘M, etc.) all work out of the box

Why this is better than pure AppKit

  • −122 net lines in AppDelegate.swift vs the AppKit version
  • No NSMenu boilerplate — SwiftUI provides standard menus automatically
  • No manual NSWindow setupWindow scene handles size, title bar, lifecycle
  • No main.swift needed@main generates the entry point
  • Declarative window sizing via .defaultSize(width:height:)
  • Future-proof — aligns with Apple's recommended app structure

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
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
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.
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.
Copy link
Copy Markdown

@mischreiber mischreiber left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved assuming you take the suggestion in that one comment on AppDelegate.swift

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants