Vue NativeVue Native
Guide
Components
Composables
Navigation
Architecture
  • iOS
  • Android
  • macOS
GitHub
Guide
Components
Composables
Navigation
Architecture
  • iOS
  • Android
  • macOS
GitHub
  • macOS
  • macOS Setup — Build & Run
  • VueNativeWindowController

macOS Setup — Build & Run

This guide walks through getting a Vue Native app building and running as a native macOS desktop application. vue-native create scaffolds a complete, working macOS host, so the happy path is three commands.

Prerequisites

ToolVersionInstall
Xcode16+Mac App Store
Xcode Command Line Tools(bundled)xcode-select --install
XcodeGen2.40+brew install xcodegen
Bun1.3+curl -fsSL https://bun.sh/install | bash

XcodeGen is required, not optional: macos/project.yml is the committed source of truth and the generated macos/*.xcodeproj is gitignored, so vue-native run macos and vue-native build macos call xcodegen generate whenever the project is missing. Without it the CLI stops with XcodeGen is required to generate macos/project.yml.

Verify your environment:

xcode-select -p          # /Applications/Xcode.app/Contents/Developer
swift --version           # Apple Swift version 6.0+
xcodegen --version        # Version: 2.4x.x
bun --version             # 1.3+

vue-native doctor checks all of these for you, including the macos/ host.

First-time Xcode install

After installing Xcode, open it once and accept the license.

Quick start

# 1. Scaffold the shared Vue app plus the iOS, Android and macOS hosts
npx @thelacanians/vue-native-cli create my-mac-app
cd my-mac-app
bun install

# 2. Build the JS bundle, generate the Xcode project, build and launch
vue-native run macos

That is the whole flow. There is no Xcode project to create by hand, no Package.swift to write, no Info.plist to fill in and no bundle resource to drag into a target — create generates all of it and run macos wires it together.

vue-native run macos does the following:

  1. Runs vite build with VUE_NATIVE_PLATFORM=macos to produce dist/vue-native-bundle.js
  2. Calls ensureXcodeProject(macos/), which runs xcodegen generate if no .xcodeproj exists yet
  3. Builds with xcodebuild -destination 'platform=macOS' build
  4. Locates the product in Derived Data and launches it with open

The generated host is ad-hoc signed (CODE_SIGN_IDENTITY: "-"), so it builds and runs with no Apple Developer team configured. Change CODE_SIGN_STYLE to Automatic and set DEVELOPMENT_TEAM in macos/project.yml before distributing the app off your machine.

What create generates

my-mac-app/
  app/                          # Vue 3 source (shared by all three platforms)
    main.ts
    App.vue
    pages/Home.vue
  macos/                        # macOS host
    project.yml                 # XcodeGen spec — the committed source of truth
    Sources/
      main.swift                # Programmatic NSApplication entry point
      AppDelegate.swift         # Subclasses VueNativeAppDelegate (installs the main menu)
      MainWindowController.swift# Subclasses VueNativeWindowController
      Info.plist
      App.entitlements
    MyMacApp.xcodeproj/         # Generated by XcodeGen — gitignored
  ios/                          # iOS host (project.yml + Sources/)
  android/                      # Android host (Gradle)
  native/                       # Vendored Vue Native runtime, copied from the CLI
    macos/VueNativeMacOS/       # The Swift package your host links against
    shared/VueNativeShared/     # Cross-platform Swift, a local path dependency of the above
    ios/VueNativeCore/
    android/VueNativeCore/
  dist/                         # Built JS bundle (generated)
  vue-native.config.ts          # Includes a macos { deploymentTarget } section
  vite.config.ts
  package.json

Only macos/project.yml and macos/Sources/ are committed. macos/*.xcodeproj/ and macos/*.xcworkspace/ are in the generated .gitignore, exactly like their ios/ counterparts — regenerate them with xcodegen generate (or just run vue-native run macos).

The generated host

Every file below is ordinary source you own. Edit it freely; vue-native run macos keeps using whatever is in macos/Sources/.

main.swift — programmatic entry point

import AppKit

// Retained for the process lifetime: NSApplication.delegate is weak, and
// top-level bindings in main.swift live as long as the executable does.
let delegate = AppDelegate()
let app = NSApplication.shared
app.delegate = delegate

// Nothing reads an activation policy out of a nib here, so state it: the app
// owns a Dock item and should come to the front when `vue-native run macos`
// launches it with `open`.
app.setActivationPolicy(.regular)
app.activate(ignoringOtherApps: true)
app.run()

There is no storyboard and no .xib, so @main on the delegate and NSApplicationMain would have nothing to load. The activation policy is set explicitly for the same reason.

AppDelegate.swift — the main menu

import AppKit
import VueNativeMacOS

class AppDelegate: VueNativeAppDelegate {
    override func createWindowController() -> VueNativeWindowController {
        MainWindowController()
    }
}

Subclass VueNativeAppDelegate rather than implementing NSApplicationDelegate yourself: it installs a main menu at launch. A programmatically launched NSApplication otherwise gets an empty menu bar, and AppKit routes key equivalents through the main menu — so without it Cmd+Q does not quit and Cmd+C/V/X/A never reach a focused VInput. Override makeMainMenu() to replace or extend the standard App / Edit / View / Window / Help menu; useMenu() merges into whatever is installed.

MainWindowController.swift — where the Vue app renders

import Foundation
import VueNativeMacOS

final class MainWindowController: VueNativeWindowController {
    /// Matches the resource copied in by macos/project.yml
    /// (../dist/vue-native-bundle.js -> Contents/Resources/vue-native-bundle.js).
    override var bundleName: String { "vue-native-bundle" }

    #if DEBUG
    /// `vue-native dev --platform macos` serves the rebuilt bundle here.
    /// A production build skips the connection entirely.
    override var devServerURL: URL? { URL(string: "ws://localhost:8174") }
    #endif
}

Info.plist

Ships live values, not commented-out placeholders: NSPrincipalClass = NSApplication, LSMinimumSystemVersion = 15.0, NSHighResolutionCapable, NSAllowsLocalNetworking (for the hot-reload socket), and the three usage descriptions the macOS framework can actually reach — camera, microphone and location. Contacts, Calendar, Bluetooth and Photos have no macOS code path in the framework, and LocalAuthentication and UNUserNotificationCenter need no Info.plist string on macOS.

App.entitlements

Sets com.apple.security.app-sandbox to false. That is deliberate: useFileSystem() and useDatabase() read and write per-app folders under ~/Library (see useFileSystem), the dev host opens a ws://localhost:8174 socket, and useCamera() / useGeolocation() reach device hardware. Under the sandbox each of those needs its own entitlement (network.client, files.user-selected.read-write, device.camera) or the call fails at runtime. Sandboxing is a distribution decision, so the scaffold states the non-sandboxed default in one editable place. If you enable the sandbox, add the entitlements your app actually uses.

project.yml

The XcodeGen spec. It points the target at the vendored package, copies the Vite output into Contents/Resources, sets INFOPLIST_FILE / CODE_SIGN_ENTITLEMENTS, declares an explicit (shared) scheme so xcodebuild -scheme works on a fresh clone, and pins ad-hoc signing:

packages:
  VueNativeMacOS:
    path: ../native/macos/VueNativeMacOS

targets:
  MyMacApp:
    type: application
    platform: macOS
    sources:
      - path: Sources
        excludes:
          - Info.plist
          - App.entitlements
      - path: ../dist/vue-native-bundle.js
        buildPhase: resources
        optional: true
    scheme: {}
    dependencies:
      - package: VueNativeMacOS
        product: VueNativeMacOS

Info.plist and App.entitlements are excluded from sources because they are consumed through build settings; leaving them in would make two build steps produce the same file.

Consuming VueNativeMacOS

The only supported path is the vendored native/ tree inside a scaffolded project, referenced from project.yml as a local path: package.

Do not add VueNativeMacOS as a remote git dependency:

# Does NOT work — do not do this
packages:
  VueNativeMacOS:
    url: https://github.com/abdul-hamid-achik/vue-native
    branch: main

native/macos/VueNativeMacOS/Package.swift declares VueNativeShared as a local dependency (path: ../../shared/VueNativeShared). SPM rejects local path: dependencies inside a package it resolves from a remote URL, so resolution fails before it ever compiles. A relative path only resolves inside a native/{macos,shared} tree on disk — which is exactly what vue-native create copies into your project.

To pick up a newer runtime, re-scaffold and copy native/ across, or update the vendored tree in place. native/.vue-native-version records which CLI version vendored it.

One window per app

The generated host is single-window by design. NativeBridge.shared and JSRuntime.shared are process-wide singletons, so MainWindowController is the app's one and only Vue surface: a second VueNativeWindowController would tear the first one's component registry down rather than render a second app. Multi-window Vue Native apps are not supported.

If you need more chrome, put it in the Vue tree (VToolbar, VSplitView, VOutlineView, VModal) or in the main menu via makeMainMenu() / useMenu().

Hot reload

Hot reload lets you edit .vue files and see changes instantly without rebuilding the native app.

Setup

Terminal 1 — Start the dev server:

vue-native dev --platform macos

This starts:

  • Vite in watch mode (rebuilds dist/vue-native-bundle.js on every save)
  • A WebSocket server on ws://localhost:8174

Terminal 2 — Run the app (first time only):

vue-native run macos

Once the app is running you do not need to rebuild — changes are pushed over the WebSocket. The scaffolded MainWindowController already returns ws://localhost:8174 from devServerURL inside #if DEBUG, and a production build skips the connection entirely. Because the app runs on the same machine as the dev server, localhost works directly; NSAllowsLocalNetworking in the generated Info.plist is what permits the socket.

How it works

  1. You save app/pages/Home.vue
  2. Vite detects the change and rebuilds vue-native-bundle.js
  3. The dev server sends the new bundle over WebSocket
  4. HotReloadManager in the app receives it
  5. JSRuntime tears down the old JSContext, creates a new one, evaluates the new bundle
  6. The window content updates on screen

Debugging

Xcode console

All console.log() / console.error() calls from your Vue code appear in the Xcode console prefixed with [VueNative JS]:

[VueNative JS] App started
[VueNative JS] Navigated to /settings

Bridge operations are logged as:

[VueNative] createComponent VView (nodeId: 3)
[VueNative] setProp text="Hello" (nodeId: 5)

Error overlay

In debug builds, unhandled JS errors display a red overlay in the window with the error message and stack trace.

Checking the bundle loaded

If you see an empty window, verify the bundle is executing:

// Add to the top of app/main.ts
console.log('Bundle executing')

If you see the log, the bundle loaded. If not, check:

  • dist/vue-native-bundle.js exists (bun run build or vue-native run macos completed without errors)
  • macos/project.yml still lists ../dist/vue-native-bundle.js with buildPhase: resources
  • The file landed in the product: ls <path-to>.app/Contents/Resources/vue-native-bundle.js

Common issues

"XcodeGen is required to generate macos/project.yml" — brew install xcodegen, then retry.

"No macos/ directory found" — the project was created before the macOS host existed. Re-scaffold with vue-native create into a scratch directory and copy macos/ and native/ across, then run vue-native run macos.

"No such module 'VueNativeMacOS'" — the package has not resolved yet. Regenerate the project (cd macos && xcodegen generate) and build once from Xcode so SPM resolves native/macos/VueNativeMacOS and its local native/shared/VueNativeShared dependency. If that fails, delete DerivedData:

rm -rf ~/Library/Developer/Xcode/DerivedData

SPM resolution fails after switching to a git URL dependency — expected. VueNativeMacOS depends on VueNativeShared by local path, which SPM rejects inside a remotely resolved package. Use the vendored path: dependency described above.

"Signing requires a development team" — the scaffold uses ad-hoc signing, so this only appears after you switch CODE_SIGN_STYLE to Automatic. Either set DEVELOPMENT_TEAM in macos/project.yml or revert to CODE_SIGN_STYLE: Manual with CODE_SIGN_IDENTITY: "-".

Empty window — the most likely cause is the JS bundle missing from Contents/Resources. Rebuild with bun run build, then rebuild the native app. Also check the Xcode console for [VueNative] errors.

Cmd+C / Cmd+V / Cmd+Q do nothing — AppDelegate must subclass VueNativeAppDelegate, which installs the main menu. Implementing NSApplicationDelegate directly leaves the menu bar empty and AppKit has nowhere to route key equivalents.

A second window shows the error overlay or a blank view — multi-window is unsupported; see One window per app.

Coordinate system issues — AppKit uses a bottom-left origin by default. VueNativeMacOS uses FlippedView (an NSView subclass with isFlipped = true) to provide a top-left origin consistent with iOS and CSS. If you add custom NSView subclasses, override isFlipped to return true.

See the Troubleshooting guide for more.

Edit this page
Last Updated: 10/14/26, 10:27 PM
Contributors: Abdul Hamid, Claude Opus 5.5
Prev
macOS
Next
VueNativeWindowController