Skip to content

feat(ios): run iOS apps on an Apple silicon Mac with --device mac - #6205

Draft
NathanWalker wants to merge 1 commit into
mainfrom
feat/ios-designed-for-ipad
Draft

NathanWalker wants to merge 1 commit into
mainfrom
feat/ios-designed-for-ipad

Conversation

@NathanWalker

Copy link
Copy Markdown
Contributor

Draft, for discussion. Building, installing, launching, logs and device listing work locally. Relaunching after a file sync is still blocked; see Known issue below. A few design questions are open too.

PR Checklist

What is the current behavior?

Since NativeScript/ios#551, an iOS app can run on an Apple silicon Mac as "Designed for iPad": it loads UIKit and the other iOS-only frameworks from /System/iOSSupport. The CLI has no way to target the Mac, so today you build with xcodebuild, build the wrapper bundle and launch it by hand.

What is the new behavior?

ns devices ios                  # lists "My Mac (Designed for iPad)"
ns run ios --device mac         # build, install, launch, sync, logs
ns debug ios --device mac

The Mac is an iOS device, not a new platform, so run, debug, deploy and devices work with no new commands. It is listed by ns devices, but ns run ios only uses it when --device is mac or the Mac's provisioning UDID. People with a phone plugged in won't get a Mac launch as well.

This is a different product from #6125 (ns run catalyst). Catalyst rebuilds the app against the macOS SDK. This runs the unmodified iOS (iphoneos) build, the same binary that ships to devices. The two don't overlap and can land independently.

How it works

  • Discovery (IOSMacDeviceDiscovery):

    • Only on a Darwin host that is arm64, checked with sysctl hw.optional.arm64 because Rosetta hides it from process.arch.
    • Only for the iOS platform, and not with --emulator.
    • The device identifier is the Mac's provisioning UDID, read from system_profiler SPHardwareDataType.
  • Build:

    • A signed iphoneos build with -destination 'platform=macOS,arch=arm64,variant=Designed for iPad,id=<UDID>' and -allowProvisioningDeviceRegistration, so the profile includes this Mac.
    • Output goes to platforms/ios/build/mac, so it doesn't replace device or simulator builds.
    • New buildForMac flag on IBuildData / IBuildForDevice, set next to each buildForDevice.
  • Install and launch (IOSMacApplicationManager):

    • macOS only launches an iOS app from the layout it gives installed iPad apps: <Name>.app/Wrapper/<Name>.app plus a WrappedBundle symlink.
    • Launches with open -n <wrapper>.
    • Stops by finding the process with pgrep. macOS runs a copy of the wrapper from /private/var/folders, not from the install path.
  • Sync: local file copies, like a simulator. IOSMacDevice.isEmulator is true so it uses those code paths.

  • Logs: log stream --predicate 'process == "<exe>" AND <IOS_LOG_PREDICATE>'.

  • Debugger:

    1. Post the attach request with notifyutil -p <appId>:NativeScript.Debug.AttachRequest.
    2. Read the port from the runtime's "inspector socket on port N" log line.
    3. Connect to 127.0.0.1.
  • Runtime check: warns when @nativescript/ios predates fix(runtime): load iOS system frameworks from /System/iOSSupport on a Mac ios#551:

    • releases older than 9.1.1
    • next builds before 9.1.0-next.2026-11-11-43080423883

    Older runtimes stop at launch on a Mac.

Tested locally

M3 Max, macOS 26.6.2, Xcode 27, with an Angular + Vite app on @nativescript/ios@next:

ns devices ios lists the Mac ✅
--device mac selects it; plain ns run ios doesn't ✅
Build into build/mac, provisioning includes the Mac (about 20 s) ✅
Wrapper install + open -n launch ✅
JS console output via log stream ✅
New unit tests + devices-service + ios-project-service (160) ✅
Relaunch after syncing files ❌ see below
Vite HMR, --no-hmr sync + restart, ns debug not yet verified

Known issue: syncing breaks the code signature

The CLI syncs app files (and .nsbuildinfo) into the signed bundle. That invalidates its seal: codesign --verify --strict reports "a sealed resource is missing or invalid". macOS then refuses to launch the app and shows the "damaged, move to Trash" dialog.

Re-signing the inner bundle with the identity it was built with fixes it. It takes about 0.1 s and keeps the entitlements:

codesign --force --sign <leaf-cert-sha1> \
  --preserve-metadata=identifier,entitlements,flags,runtime <bundle>

Proposed fix:

  1. At install, record the identity from the untouched bundle.
  2. Before each open, run codesign --verify --strict, which takes about 0.04 s.
  3. If the verify fails, re-sign.

Vite HMR modules come from the dev server and are never written into the bundle, so HMR edits shouldn't trigger this.

Open questions

  1. How to expose the Mac: --device mac under ios, as here, or a --mac flag, or a separate platform? A device seemed the least new surface.
  2. isEmulator = true for a Mac: the simulator's local-file code paths are exactly right, but the name is misleading. Add an isLocal / runsOnHost capability instead? That means auditing every isEmulator check.
  3. Old runtime: warn, as here, or fail the build? Local file: runtimes can't be version-checked, so this warns.
  4. Intel Macs: they are skipped silently. Should --device mac on one give an explicit error?

Apple silicon Macs run iPhone and iPad apps as "Designed for iPad".
@nativescript/ios supports this from NativeScript/ios#551, which loads
UIKit and the other iOS-only frameworks from /System/iOSSupport.

The Mac is offered as an iOS device ("My Mac (Designed for iPad)"). It
is listed by `ns devices` and used only when `--device` is `mac` or the
Mac's provisioning UDID, so a plain `ns run ios` never launches on it.

- Build: the signed iphoneos build with destination
  `platform=macOS,arch=arm64,variant=Designed for iPad,id=<UDID>`,
  into platforms/ios/build/mac so it doesn't replace device or
  simulator output.
- Install: the bundle goes into the wrapper layout macOS requires
  (<Name>.app/Wrapper/<Name>.app plus a WrappedBundle symlink), then
  `open -n` launches it.
- Sync, logs (`log stream`) and the debugger (Darwin notification,
  then a local socket) work on the local file system, the same way
  they do for a simulator.
- Warns when @nativescript/ios is older than NativeScript/ios#551.
@coderabbitai

coderabbitai Bot commented Nov 14, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

This branch has not been deployed

No deployments
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.

1 participant