Skip to content

Move from Mac Catalyst app to mac native app - #5935

Draft
bgoncal wants to merge 36 commits into
home-assistant:mainfrom
bgoncal:native-macos-app
Draft

bgoncal wants to merge 36 commits into
home-assistant:mainfrom
bgoncal:native-macos-app

Conversation

@bgoncal

@bgoncal bgoncal commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

AI Policy

Select exactly one option that describes AI usage in this contribution:

  • I have not used AI for this contribution.
  • AI assistance was used for this contribution.
  • AI fully generated the code for this contribution, but I've reviewed and understood it before submitting and will respond without AI during review.

Summary

Builds the Mac app natively with AppKit and SwiftUI instead of Mac Catalyst. The same targets keep building for iOS and Catalyst; native macOS is a third variant of them.

  • UIKit value types (UIColor, UIImage, ...) alias AppKit on macOS and iOS-only SwiftUI modifiers get Mac stand-ins, so screens are written once. Windows, menus, sheets, the frontend controller and the settings sidebar are written per platform.
  • Widgets, Intents, NotificationService, NotificationContent and Share build and embed natively.
  • The macOS deployment target is 13.3 for the app and its extensions: the Catalyst app already required 13.3 through iOS 16.4, and the native app's SwiftUI windows need macOS 13. The Widgets target asks for macOS 14.0.
  • Release is unchanged: fastlane/lanes/macos.rb now passes the Catalyst destination explicitly, so the Mac lane still archives the Catalyst app now that the targets build for both Mac variants. There is no signing or provisioning for the native app yet.
  • CI gets a build-mac job that builds the app for Catalyst and for native macOS, and runs HADesignSystem's package tests on macOS, which cover the AppKit stand-ins (renderer orientation and scale, colour components, dynamic colours in both appearances).

Changes that also apply to iOS

  • Settings screens are a GroupedList, which is a plain List on iOS.
  • The post-onboarding notification prompt is shown once: answering it is remembered, so it no longer comes back on a later launch while the system status is still undetermined.
  • Alerts described with AppAlert can mark a preferred action, which sets UIAlertController.preferredAction for the deep link confirmation ("Open").
  • The share buttons for a YAML preview, an NFC tag identifier and the push ID are ShareLinks, as the log export already was, instead of a button and a UIActivityViewController wrapper of their own.
  • SpeechTranscriber refuses an audio format with no sample rate or channels instead of trapping in AVAudioConverter.

Still to do before a native release

  • Signing and provisioning for the native app (aps-environment, app groups, keychain access groups). An unsigned build has no entitlements, which hides keychain and extension problems.
  • Keychain: the native app reads the data protection keychain, Catalyst the same one through the shared access group. The plan is a SecItem wrapper that sets kSecUseDataProtectionKeychain on macOS behind ServerManagerKeychain, ClientCertificate, AppConstants and KeychainWatchDeviceRegistrationStore, so a Catalyst install's servers are found by the native app.
  • Device ID: the native app and the Catalyst app derive it differently. The plan is to persist the identifier once and ship that in a Catalyst release first, so a user who moves to the native app keeps the same mobile_app registration.

Screenshots

Light Dark
Onboarding, light Onboarding, dark
Settings › General, light Settings › General, dark
Settings › Notifications, light Settings › Notifications, dark
About, light About, dark
Frontend, light Frontend, dark
Assist, light Assist, dark
Find in page Notification prompt
Find Notification prompt

Link to pull request in Documentation repository

Documentation: home-assistant/companion.home-assistant#

Any other notes

Native macOS signing and provisioning are still needed before push notifications and the extensions can be tried at runtime. The Debug builds keep the "Simulator" placeholder device name during onboarding on every platform, as before.

@bgoncal
bgoncal requested a balanced review from Copilot September 30, 2026 22:44
@bgoncal bgoncal changed the title Native macOS app Move from Mac Catalyst app to mac native app Sep 30, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

The macOS-enabled Assist widget intent still references an accessory-only widget family that is unavailable on native macOS.

Review effort: Balanced
Findings: 1 High severity

Open (1)
What changed in this PR

Adds native AppKit/SwiftUI macOS builds while preserving iOS and Mac Catalyst variants.

Changes:

  • Adds cross-platform aliases, lifecycle abstractions, availability declarations, and AppKit imports.
  • Enables native macOS widgets, intents, networking, sensors, settings, and web frontend support.
  • Excludes unsupported iOS, Watch, CarPlay, NFC, and accessory-widget functionality.
File Description
Tests/​App/​WebView/​Mocks/​MockWebViewController.swift Adds appearance-state mock support.
Sources/​Shared/​Watch/​* Enables shared watch APIs to compile for non-watch platforms.
Sources/​Shared/​Toast/​* Adds macOS toast availability.
Sources/​Shared/​Shared.h Selects AppKit or UIKit by platform.
Sources/​Shared/​Panels/​PanelsUpdater.swift Uses cross-platform lifecycle notifications.
Sources/​Shared/​Notifications/​* Adds native macOS notification compatibility.
Sources/​Shared/​MaterialDesignIcons+CarPlay.swift Guards CarPlay-only code.
Sources/​Shared/​Location/​* Adapts location handling for macOS.
Sources/​Shared/​LiveActivity/​* Adds explicit platform availability.
Sources/​Shared/​Extensions/​* Removes or guards UIKit-only dependencies.
Sources/​Shared/​Environment/​* Adds AppKit, battery, connectivity, haptic, and theme support.
Sources/​Shared/​Domain/​Domain.swift Adds AppKit import support.
Sources/​Shared/​DesignSystem/​* Adds macOS availability to shared UI.
Sources/​Shared/​Common/​* Adds platform colors, images, lifecycle, and application state.
Sources/​Shared/​API/​* Extends models, sensors, updates, and authentication to macOS.
Sources/​MacBridge/​* Makes bridge implementations platform-conditional.
Sources/​HAWatchComplications/​* Excludes watch rendering components from macOS.
Sources/​HAWatchCommunicationMessages/​Package.swift Declares macOS package support.
Sources/​HAUtilities/​Package.swift Declares macOS package support.
Sources/​HANetworking/​Package.swift Declares macOS package support.
Sources/​HAModels/​Package.swift Declares macOS package support.
Sources/​HAIconic/​* Adds macOS package support and guards UIKit views.
Sources/​HADesignSystem/​* Adds macOS availability, exports, and platform UI behavior.
Sources/​Extensions/​Widgets/​* Enables native macOS widgets and guards unsupported families.
Sources/​Extensions/​AppIntents/​* Adds macOS availability to intents and entities.
Sources/​CarPlay/​* Guards CarPlay-only implementation files.
Sources/​App/​WhatsNew/​* Removes direct UIKit dependencies and recognizes native Mac.
Sources/​App/​VoiceToolsServer/​* Uses cross-platform grouped settings forms.
Sources/​App/​Utilities/​* Adds platform abstractions for state, settings, themes, and intents.
Sources/​App/​TestFlightCommunication/​* Removes unnecessary UIKit dependency.
Sources/​App/​Templating/​* Uses platform-compatible colors and imports.
Sources/​App/​Settings/​* Adapts settings UI and platform-specific features for native macOS.
Sources/​App/​Servers/​ServerSelectionListView.swift Adds macOS list-spacing availability.
Sources/​App/​Scenes/​* Guards UIKit scenes and adds platform window support.
Sources/​App/​PermissionScreen/​* Removes unnecessary UIKit dependency.
Sources/​App/​Onboarding/​* Adds macOS availability, naming, and toolbar behavior.
Sources/​App/​Notifications/​KioskPushCommand.swift Adds macOS toast availability.
Sources/​App/​MainWindowGroupCommands.swift Enables menu commands on native macOS.
Sources/​App/​Frontend/​WebView/​* Adds AppKit hosting, platform windows, and native Mac frontend behavior.
Sources/​App/​Frontend/​TagApprovalBottomSheet.swift Removes unnecessary UIKit dependency.
Sources/​App/​Frontend/​OnscreenPage/​* Adds macOS entity-identifier availability.
Sources/​App/​Frontend/​OnscreenEntity/​* Adds macOS entity-identifier availability.
Sources/​App/​Frontend/​ExternalMessageBus/​* Adds macOS symbol availability.
Sources/​App/​Frontend/​Extensions/​* Adds platform presentation and notification registration.
Sources/​App/​Frontend/​EntityDeeplink/​* Removes unnecessary UIKit dependency.
Sources/​App/​FlightGreetings/​* Uses cross-platform grouped settings forms.
Sources/​App/​Container/​LaunchSplash/​* Disables the fake splash on native Mac.
Sources/​App/​ClientEvents/​* Adds platform hosting and native detail navigation.
Sources/​App/​Cameras/​* Adds AppKit image handling and skips iOS audio sessions.
Sources/​App/​Assist/​* Adds macOS API availability.
Configuration/​HomeAssistant.xcconfig Raises the macOS deployment target to 13.3.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread Sources/Extensions/Widgets/Assist/WidgetAssistAppIntent.swift
bgoncal added 24 commits October 1, 2026 01:06
The notification prompt no longer comes back once answered, the log files open selected in Finder, buttons in a grouped form are drawn as rows, and the widgets screen uses the same grouped form as the other screens.
On a loaded CI machine the network refresh an intent does first can outlast the second the tests polled for, after which they awaited a perform nobody would answer and the whole run hung until its timeout.
Adding macosx to the supported platforms made gym's generic macOS destination resolve the app to the native variant, which the committed Catalyst profiles cannot sign. The lane now names the Catalyst destination. CI builds the app for Catalyst and for native macOS and runs HADesignSystem's package tests on macOS, which cover the AppKit stand-ins.
…own string

The notification prompt is remembered once the system has answered it, not before, so an app killed under the system alert asks again. The welcome screen's background is a Mac-only addition. The Preferences string keeps its value for Lokalise and the Mac menu item reads a new Settings key.
…he audio output

Quitting now waits for the in-flight background activities, with a cap, so the inactive state reaches the server. The audio output sensor reads CoreAudio's default output device on the Mac instead of vanishing. The app asks for the Bluetooth entitlement it tells the frontend it can use. The empty Format menu no longer appears, so the code that stripped it is gone, along with two dead lint directives and a redundant environment assignment.
…n none is left

The View menu's frontend commands resolve against the key window synchronously and are disabled when there is no frontend there. A closed window unregisters its coordinator and web view, and the next request without a window opens one. The Settings and About scenes no longer add their own Window menu entries.
…right window

Return presses the action marked preferred, else cancel, never a destructive one. Deep link alerts go through the coordinator so they sit on an open sheet rather than behind it. A sheet's record is dropped as it closes, so what its completion presents gets a window of its own, and the tag approval and camera sheets are sized to their content. Deleting a server from the Settings window resets the sidebar instead of dismissing the pane, and the Servers entry is left out of the Mac's settings search.
… view

Find Next, Find Previous and Use Selection for Find reach NSTextFinder and are validated by it, and the bar follows the window's layout. The user activity becomes current again when the window comes back. Reconnects treat a visible window as active. The web view's background is made transparent through a guarded key-value call.
…plication source

The system background colours follow the iOS values so cards stand out from the page, components are read in extended sRGB, and bitmaps are rendered at the densest screen's scale. The AppKit and SwiftUI stand-ins each live in their own file. The foreground state is read on the main thread, on-device transcription refuses an input with no sample rate, the gauge widget no longer offers a complication source on the Mac, and the labs info sheet is a NavigationStack.
…they found it

The render smoke tests fail when a body lays out to nothing and say they are iOS smoke tests. The badge test drives an injected badge instead of the host app's. The alert tests put the coordinator back, dismiss what they presented and point the web view at a loopback address. The deep link alert tests await the main queue instead of spinning the run loop under the main actor, which is why they never saw their alert on CI. The tautological tests are gone.
One type per file, previews for every view, bodies kept inline, the banner's numbers named, and the share buttons as ShareLinks on every platform instead of a label shared across platform branches. The symbol lint rule names the AppKit initializer that exists.
…ighlighting

The stand-by view takes the notification center it listens to, so its test posts to one of its own and checks the network type is read again. A remote notification carrying an unknown command is handed to the command manager and reported as not handled. The reconnect manager's default activity check, the server invitation share sheet and the template highlighter's entity pills get tests of their own.
Shown in a window, the controller loaded the frontend and refreshed its server, and that work was still running when the next suite swapped in its own network gate, which then counted callers it did not expect and waited forever. The controller is logged out before it appears and the updaters are idle for the suite, so nothing outlives the test.
# Conflicts:
#	Sources/Shared/API/WatchHelpers.swift
@codecov

codecov Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.03887% with 96 lines in your changes missing coverage. Please review.
✅ Project coverage is 57.56%. Comparing base (8e49fcc) to head (41a8d0c).
⚠️ Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
...rnalMessageBus/WebViewExternalMessageHandler.swift 65.21% 8 Missing ⚠️
...es/App/Cameras/CameraPlayer/CameraPlayerView.swift 12.50% 7 Missing ⚠️
Sources/App/AppDelegate.swift 79.31% 6 Missing ⚠️
...ources/App/Notifications/NotificationManager.swift 53.84% 6 Missing ⚠️
Sources/App/Assist/Audio/AudioRecorder.swift 0.00% 4 Missing ⚠️
...ources/App/Container/AppContainerCoordinator.swift 73.33% 4 Missing ⚠️
Sources/App/Settings/DebugView.swift 55.55% 4 Missing ⚠️
Sources/App/Settings/Settings/SettingsView.swift 0.00% 4 Missing ⚠️
Sources/App/Assist/Local/SpeechTranscriber.swift 0.00% 3 Missing ⚠️
Sources/App/Frontend/IncomingURLHandler.swift 72.72% 3 Missing ⚠️
... and 33 more

❌ Your patch check has failed because the patch coverage (83.03%) is below the target coverage (90.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5935      +/-   ##
==========================================
+ Coverage   54.14%   57.56%   +3.42%     
==========================================
  Files        1244     1257      +13     
  Lines       82591    82725     +134     
==========================================
+ Hits        44720    47624    +2904     
+ Misses      37871    35101    -2770     
Files with missing lines Coverage Δ
Sources/App/Assist/AssistSettingsView.swift 64.55% <100.00%> (ø)
Sources/App/Assist/AssistSettingsViewModel.swift 76.92% <100.00%> (ø)
Sources/App/Assist/AssistTypingIndicator.swift 66.66% <100.00%> (ø)
Sources/App/Assist/Audio/AudioPlayer.swift 2.88% <ø> (ø)
Sources/App/Assist/Local/SpeechSynthesizer.swift 0.00% <ø> (ø)
...App/BarcodeScanner/Camera/BarcodeScannerView.swift 10.52% <ø> (ø)
...p/Cameras/CameraPlayer/CameraMJPEGPlayerView.swift 0.00% <ø> (ø)
.../CameraPlayer/CameraOverlayHostingController.swift 100.00% <ø> (ø)
.../Cameras/CameraPlayer/CameraOverlayPresenter.swift 100.00% <100.00%> (ø)
...meras/CameraPlayer/CameraPickerSnapshotCache.swift 100.00% <ø> (ø)
... and 258 more

... and 185 files with indirect coverage changes

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

# Conflicts:
#	Sources/App/Cameras/CameraPlayer/CameraOverlayPresenter.swift
# Conflicts:
#	Sources/App/Cameras/CameraPlayer/CameraPlayerView.swift
# Conflicts:
#	Sources/Watch/WatchCommunicatorService.swift
# Conflicts:
#	Sources/App/BarcodeScanner/Camera/BarcodeScannerCamera.swift
#	Sources/App/BarcodeScanner/Camera/BarcodeScannerView.swift
#	Sources/App/Frontend/Extensions/WebViewController+PostOnboarding.swift
#	Sources/App/Frontend/WebView/MacSidebar/MacNativeSidebarState.swift
#	Sources/App/Frontend/WebView/MacWebViewTitleBar.swift
#	Sources/App/Frontend/WebView/WebViewController/WebViewController+Alerts.swift
#	Sources/App/Frontend/WebView/WebViewController/WebViewController.swift
#	Sources/App/Onboarding/Steps/Servers/OnboardingServersListView.swift
#	Sources/App/Onboarding/Steps/Welcome/OnboardingWelcomeView.swift
#	Sources/App/Utilities/Extensions/SwiftUI+SafeArea.swift
#	Sources/App/Utilities/SoftwareKeyboardObserver.swift
#	Sources/HADesignSystem/Sources/Components/AppleLikeBottomSheet.swift
#	Sources/Shared/API/Webhook/Sensors/InputOutputDeviceSensor.swift
#	Sources/Shared/Extensions/View+ScreenCaptureProtection.swift
# Conflicts:
#	Sources/App/Settings/Sensors/List/SensorForegroundOnlyBadge.swift
# Conflicts:
#	Sources/App/Frontend/WebView/Views/HomeAssistantStandByView.swift
# Conflicts:
#	Sources/App/Settings/AppIconShortcuts/AppIconShortcutItemsUpdater.swift
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants