Goal: ship the Mac app as a native macOS SwiftUI (AppKit-backed) app instead of Mac Catalyst,
so it gets real NSApplication lifecycle, dock/menu-bar/window behavior, native sheets, a
MenuBarExtra relay, and no Catalyst hacks (the macSheetClose overlay, the runtime
setActivationPolicy bridge in MacAgent.swift, etc.).
Strategy: build the native target in parallel with the working Catalyst app. Catalyst keeps shipping until the native target reaches parity; only then do we drop Catalyst. This avoids weeks with no usable Mac build.
The app is UIKit-deep (camera, WebRTC video views, ReplayKit, UIImage, PHPicker,
UIApplication), so the bulk of the work is replacing the platform layer, not flipping a setting.
- Add
aarch64-apple-darwinslice toHavenFFI.xcframework(apple/build-rust-xcframework.sh). (Apple Silicon only — Intel is dropped per OS 27.) - Confirm WebRTC SPM (stasel/WebRTC) resolves its macOS slice for
platform: macOS. (Resolvesmacos-x86_64_arm64; embeds WebRTC.framework. NOTE: that slice ships theRTCMTLNSVideoViewheader but NOT its implementation — see Phase 2 video note.) - Add a native macOS target
HavenMacinapple/project.yml(platform: macOS,deploymentTarget macOS 14), sharing the sameHavenApp/Shared/Generatedsources + WebRTC + SystemConfiguration. No NSE/Broadcast appex (iOS-only). CatalystHaventarget untouched. OwnHavenApp/Info.macOS.plist+HavenMacscheme; sandbox entitlements reused (Haven.macOS.entitlements). - Add
apple/HavenApp/Platform.swift: cross-platform typealiases + shims (PlatformImage= UIImage/NSImage withjpegData/pngData/cgImage/downscaled/resized;PlatformColor;PlatformViewRepresentable/PlatformViewControllerRepresentable;PlatformPasteboard;PlatformIdle(IOPMAssertion);PlatformApp;PlatformScreen;PlatformHaptics;Image(platformImage:); NSColor semantic-color shims; and SwiftUI cross-platform shims:havenInlineNavTitle,havenFullScreenCover,havenStatusBarHidden,havenPagedTabViewStyle,havenURLKeyboard,havenAutocap,ToolbarItemPlacement.haven{Leading,Trailing}).
The native HavenMac target builds clean and launches to a window. iOS + Mac Catalyst builds
stay green (parallel-port intact).
- Conditionalize every
import UIKit(→#if canImport(UIKit)/import AppKit). -
UIImage→PlatformImageacross MediaStore / filters / profile / story / camera review. -
UIApplication.shared.*(idle timer, applicationState, registerForRemoteNotifications, beginBackgroundTask, connectedScenes) → macOS equivalents / gated no-ops. -
UIPasteboard→PlatformPasteboard(NSPasteboard); haptics →PlatformHapticsno-op. - Gated iOS-only features on native macOS (kept Catalyst behavior via
#if os(iOS)): MediaPlayer library picker/playback (Music/AudioCoordinator — catalog via MusicKit still works), CallKit (CallManager keeps WebRTC + in-app flow, CallKit reporting gated), AVAudioSession, BGTaskScheduler, ReplayKit/broadcast, AVCaptureMultiCamSession, PushKit VoIP. - Native macOS app delegate (
NSApplicationDelegate+@NSApplicationDelegateAdaptor). - Functional macOS pickers via
NSOpenPanel(single-image + media); camera/QR/dual-cam/story capture views are macOS placeholders for now (Phase 2). - Goal met: non-UI code + plain SwiftUI views compile and the app launches to a window.
- Camera:
AVCaptureSession+NSViewRepresentablepreview (AVCaptureVideoPreviewLayerin anNSView); story/post capture review in AppKit. - WebRTC video:
⚠️ stasel/WebRTC's macOS slice ships theRTCMTLNSVideoViewheader but the class is not in the binary (linker can't find_OBJC_CLASS_$_RTCMTLNSVideoView). SoRTCVideoViewon macOS is currently a placeholder. Real options: implement a customNSViewconforming toRTCVideoRenderer(the protocol IS available), or switch to a WebRTC build whose macOS slice includes the Metal NS view. - In-app browser:
WKWebViewviaNSViewRepresentable. - Media pickers:
PHPickerViewController(has macOS support) orNSOpenPanelfallback. - Screen share: already
ScreenCaptureKiton the desktop path — drop the ReplayKit extension on macOS (it's iOS-only). - Photos save:
PHPhotoLibraryworks on macOS; verify album creation.
- Real
NSApplicationDelegatelifecycle: replaceMacAgent.swift's runtimesetActivationPolicybridge with the public AppKit API;applicationShouldTerminateAfter LastWindowClosed = falsefor the relay. -
MenuBarExtrafor the relay (status + quit) — the proper "invisible background relay". - Native menu bar +
Settingsscene; drop themacSheetCloseoverlay (sheets get native chrome). -
SMAppServicestart-at-login keeps working (it's already AppKit-friendly).
Catalyst dropped EARLY (user chose "drop Catalyst now, backfill native views later"). The native
HavenMac is now the ONLY macOS build; iOS + native macOS both build green.
- Removed
macCatalystfromHaven+HavenNotificationServicesupportedDestinations(now[iOS]); removedSUPPORTS_MACCATALYST/DERIVE_MACCATALYST_*/MACOSX_DEPLOYMENT_TARGET/EXCLUDED_ARCHS[sdk=macosx*]and the[sdk=macosx*]entitlements overrides from both. - Set
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD: NOon the iOS targets so the iOS app isn't ALSO offered on Mac (would clash withHavenMac'scom.blaineam.kithbundle id on ASC). - NOTE: the dead
#if targetEnvironment(macCatalyst)branches are left in place for now (they compile into nothing — no target builds Catalyst). Several (e.g. ScreenShare's ScreenCaptureKit path) are useful references for the Phase-2 native ports; clean up as each is ported.
-
.local-ci.conf:PLATFORMS="ios macos",MACOS_SCHEME="HavenMac"(wasios maccatalyst/MACCATALYST_SCHEME). The shared_shared/local-ci-archive.shalready supports nativemacos(platform=macOS,.pkg+ installer-cert export). rocket is platform-agnostic — it still uploads/submits under ASC platform MAC_OS, now from the native.pkg. - Remaining to actually ship: real
rocket build Havenarchive of the native macOS.pkg, screenshots, sandbox entitlements review, and backfilling the Phase-2 native views so the Mac app isn't shipping placeholders for camera/in-call video/Apple Music/screen share.
- rocket/ASC native macOS upload; screenshots; sandbox entitlements review.
- Toolbar placement on macOS: iOS
.topBarLeading/.topBarTrailingdon't exist on macOS. Map via theToolbarItemPlacement.haven*shims inPlatform.swift. CRUCIAL distinction: sheet/dialog dismiss buttons ("Done"/"Cancel") must usehavenConfirm*/havenCancel*(→ macOS.confirmationAction/.cancellationAction, which render as real sheet buttons that respond to Return/Esc). Using.primaryActionfor them floats the button in the title-bar toolbar OVER the content — that's what caused the doubled "Done" and the "Done" landing on the CircleView gear. Persistent nav actions (call/add/gear) usehavenTrailing/havenLeading(→.automatic). - Keep
#if os(macOS)vs#if targetEnvironment(macCatalyst)straight — during the parallel phase BOTH the CatalystHaventarget and nativeHavenMacexist; Catalyst code istargetEnvironment(macCatalyst), native isos(macOS) && !targetEnvironment(macCatalyst). RTCMTLNSVideoViewlives in the same WebRTC module; gate the view wrapper by platform.- ReplayKit
HavenBroadcastextension stays iOS-only (platformFilter: iOS), already the case. - The Rust core is platform-agnostic; only the xcframework packaging needs the darwin slice.