425 lines
13 KiB
Markdown
425 lines
13 KiB
Markdown
---
|
|
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/macos-system-integration.mdc
|
|
confidence: 0.9
|
|
namespace: work
|
|
last_synced: 2026-04-28
|
|
alwaysApply: false
|
|
---
|
|
|
|
# macOS System Integration Patterns
|
|
|
|
## Background Agents and Services
|
|
Use proper service management for background agents:
|
|
|
|
```swift
|
|
// ✅ CORRECT - Background service management
|
|
final class BackgroundServiceManager {
|
|
private let agentIdentifier = "com.duckduckgo.agent"
|
|
private let extensionIdentifier = "com.duckduckgo.extension"
|
|
|
|
func registerBackgroundAgent() throws {
|
|
let service = SMAppService.agent(plistName: "BackgroundAgent.plist")
|
|
|
|
do {
|
|
try service.register()
|
|
print("Background agent registered successfully")
|
|
} catch {
|
|
print("Failed to register background agent: \(error)")
|
|
throw error
|
|
}
|
|
}
|
|
|
|
func unregisterBackgroundAgent() throws {
|
|
let service = SMAppService.agent(plistName: "BackgroundAgent.plist")
|
|
|
|
do {
|
|
try service.unregister()
|
|
print("Background agent unregistered successfully")
|
|
} catch {
|
|
print("Failed to unregister background agent: \(error)")
|
|
throw error
|
|
}
|
|
}
|
|
|
|
func checkServiceStatus() -> SMAppService.Status {
|
|
let service = SMAppService.agent(plistName: "BackgroundAgent.plist")
|
|
return service.status
|
|
}
|
|
}
|
|
|
|
// ❌ INCORRECT - Direct background processing in main app
|
|
final class FeatureManager {
|
|
func startBackgroundWork() {
|
|
// Don't run continuous background work in main app
|
|
DispatchQueue.global().async {
|
|
while true {
|
|
// This will drain battery and violate sandboxing
|
|
self.performWork()
|
|
Thread.sleep(forTimeInterval: 60)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## System Extensions
|
|
Use proper system extension lifecycle management:
|
|
|
|
```swift
|
|
// ✅ CORRECT - System extension management
|
|
import SystemExtensions
|
|
|
|
final class SystemExtensionManager: NSObject {
|
|
private let extensionIdentifier = "com.duckduckgo.network-extension"
|
|
|
|
func installExtension() {
|
|
let request = OSSystemExtensionRequest.activationRequest(
|
|
forExtensionWithIdentifier: extensionIdentifier,
|
|
queue: .main
|
|
)
|
|
request.delegate = self
|
|
OSSystemExtensionManager.shared.submitRequest(request)
|
|
}
|
|
|
|
func uninstallExtension() {
|
|
let request = OSSystemExtensionRequest.deactivationRequest(
|
|
forExtensionWithIdentifier: extensionIdentifier,
|
|
queue: .main
|
|
)
|
|
request.delegate = self
|
|
OSSystemExtensionManager.shared.submitRequest(request)
|
|
}
|
|
|
|
func checkExtensionStatus() async -> OSSystemExtensionRequest.Result? {
|
|
// Check if extension is already installed
|
|
return await withCheckedContinuation { continuation in
|
|
let request = OSSystemExtensionRequest.propertiesRequest(
|
|
forExtensionWithIdentifier: extensionIdentifier,
|
|
queue: .main
|
|
)
|
|
|
|
// Handle the properties request to determine status
|
|
// Implementation details...
|
|
continuation.resume(returning: nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - OSSystemExtensionRequestDelegate
|
|
extension SystemExtensionManager: OSSystemExtensionRequestDelegate {
|
|
func request(
|
|
_ request: OSSystemExtensionRequest,
|
|
actionForReplacingExtension existing: OSSystemExtensionProperties,
|
|
withExtension extension: OSSystemExtensionProperties
|
|
) -> OSSystemExtensionRequest.ReplacementAction {
|
|
return .replace
|
|
}
|
|
|
|
func requestNeedsUserApproval(_ request: OSSystemExtensionRequest) {
|
|
print("System extension requires user approval")
|
|
// Show UI to guide user through approval process
|
|
showUserApprovalGuidance()
|
|
}
|
|
|
|
func request(
|
|
_ request: OSSystemExtensionRequest,
|
|
didFinishWithResult result: OSSystemExtensionRequest.Result
|
|
) {
|
|
switch result {
|
|
case .completed:
|
|
print("System extension request completed successfully")
|
|
handleExtensionActivated()
|
|
case .willCompleteAfterReboot:
|
|
print("System extension will be activated after reboot")
|
|
showRebootRequiredMessage()
|
|
@unknown default:
|
|
print("Unknown system extension result: \(result)")
|
|
}
|
|
}
|
|
|
|
func request(_ request: OSSystemExtensionRequest, didFailWithError error: Error) {
|
|
print("System extension request failed: \(error)")
|
|
handleExtensionError(error)
|
|
}
|
|
|
|
private func showUserApprovalGuidance() {
|
|
// Show UI to guide user through System Preferences
|
|
}
|
|
|
|
private func handleExtensionActivated() {
|
|
// Update UI to reflect extension is active
|
|
}
|
|
|
|
private func showRebootRequiredMessage() {
|
|
// Show UI indicating reboot is required
|
|
}
|
|
|
|
private func handleExtensionError(_ error: Error) {
|
|
// Handle extension installation errors
|
|
}
|
|
}
|
|
```
|
|
|
|
## Login Items Management
|
|
Use the modern SMAppService API for login items:
|
|
|
|
```swift
|
|
// ✅ CORRECT - Modern login items API
|
|
import ServiceManagement
|
|
|
|
final class LoginItemsManager {
|
|
func enableLoginItem() throws {
|
|
do {
|
|
try SMAppService.mainApp.register()
|
|
print("Login item enabled successfully")
|
|
} catch {
|
|
print("Failed to enable login item: \(error)")
|
|
throw LoginItemError.registrationFailed(error)
|
|
}
|
|
}
|
|
|
|
func disableLoginItem() throws {
|
|
do {
|
|
try SMAppService.mainApp.unregister()
|
|
print("Login item disabled successfully")
|
|
} catch {
|
|
print("Failed to disable login item: \(error)")
|
|
throw LoginItemError.unregistrationFailed(error)
|
|
}
|
|
}
|
|
|
|
var isLoginItemEnabled: Bool {
|
|
return SMAppService.mainApp.status == .enabled
|
|
}
|
|
|
|
var loginItemStatus: SMAppService.Status {
|
|
return SMAppService.mainApp.status
|
|
}
|
|
}
|
|
|
|
enum LoginItemError: LocalizedError {
|
|
case registrationFailed(Error)
|
|
case unregistrationFailed(Error)
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .registrationFailed(let error):
|
|
return "Failed to register login item: \(error.localizedDescription)"
|
|
case .unregistrationFailed(let error):
|
|
return "Failed to unregister login item: \(error.localizedDescription)"
|
|
}
|
|
}
|
|
}
|
|
|
|
// ❌ INCORRECT - Deprecated APIs
|
|
final class OldLoginItemsManager {
|
|
func enableLoginItem() {
|
|
// Don't use deprecated LSSharedFileList APIs
|
|
let loginItems = LSSharedFileListCreate(nil, kLSSharedFileListSessionLoginItems, nil)
|
|
// ... deprecated implementation
|
|
}
|
|
}
|
|
```
|
|
|
|
## Workspace Integration
|
|
Integrate properly with macOS workspace:
|
|
|
|
```swift
|
|
// ✅ CORRECT - Workspace integration
|
|
final class WorkspaceIntegration {
|
|
func openFileInFinder(at url: URL) {
|
|
NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: url.path)
|
|
}
|
|
|
|
func revealInFinder(fileAt url: URL) {
|
|
NSWorkspace.shared.selectFile(url.path, inFileViewerRootedAtPath: url.deletingLastPathComponent().path)
|
|
}
|
|
|
|
func openWithDefaultApplication(url: URL) {
|
|
NSWorkspace.shared.open(url)
|
|
}
|
|
|
|
func openWithApplication(url: URL, applicationURL: URL) {
|
|
NSWorkspace.shared.open([url], withApplicationAt: applicationURL, configuration: NSWorkspace.OpenConfiguration())
|
|
}
|
|
|
|
func getDefaultApplication(for url: URL) -> URL? {
|
|
return NSWorkspace.shared.urlForApplication(toOpen: url)
|
|
}
|
|
}
|
|
```
|
|
|
|
## Dock Integration
|
|
Handle dock interactions properly:
|
|
|
|
```swift
|
|
// ✅ CORRECT - Dock integration
|
|
final class DockIntegration {
|
|
func setBadgeCount(_ count: Int) {
|
|
NSApp.dockTile.badgeLabel = count > 0 ? "\(count)" : nil
|
|
}
|
|
|
|
func clearBadge() {
|
|
NSApp.dockTile.badgeLabel = nil
|
|
}
|
|
|
|
func setDockMenu(_ menu: NSMenu) {
|
|
NSApp.dockTile.contentView = nil
|
|
NSApp.dockTile.showsApplicationBadge = true
|
|
// Custom dock menu would be set through app delegate
|
|
}
|
|
}
|
|
|
|
// In AppDelegate
|
|
extension AppDelegate: NSApplicationDelegate {
|
|
func applicationDockMenu(_ sender: NSApplication) -> NSMenu? {
|
|
let dockMenu = NSMenu()
|
|
|
|
dockMenu.addItem(NSMenuItem(
|
|
title: "New Window",
|
|
action: #selector(newWindow),
|
|
keyEquivalent: ""
|
|
))
|
|
|
|
dockMenu.addItem(NSMenuItem(
|
|
title: "New Private Window",
|
|
action: #selector(newPrivateWindow),
|
|
keyEquivalent: ""
|
|
))
|
|
|
|
return dockMenu
|
|
}
|
|
|
|
@objc func newWindow() {
|
|
WindowsManager.openNewWindow()
|
|
}
|
|
|
|
@objc func newPrivateWindow() {
|
|
WindowsManager.openNewWindow(burnerMode: .burner)
|
|
}
|
|
}
|
|
```
|
|
|
|
## Notification Center Integration
|
|
Handle notifications properly:
|
|
|
|
```swift
|
|
// ✅ CORRECT - User notification handling
|
|
import UserNotifications
|
|
|
|
final class NotificationManager: NSObject {
|
|
func requestNotificationPermission() async -> Bool {
|
|
let center = UNUserNotificationCenter.current()
|
|
|
|
do {
|
|
let granted = try await center.requestAuthorization(options: [.alert, .sound, .badge])
|
|
return granted
|
|
} catch {
|
|
print("Failed to request notification permission: \(error)")
|
|
return false
|
|
}
|
|
}
|
|
|
|
func scheduleNotification(title: String, body: String, identifier: String) async {
|
|
let content = UNMutableNotificationContent()
|
|
content.title = title
|
|
content.body = body
|
|
content.sound = .default
|
|
|
|
let request = UNNotificationRequest(
|
|
identifier: identifier,
|
|
content: content,
|
|
trigger: nil
|
|
)
|
|
|
|
do {
|
|
try await UNUserNotificationCenter.current().add(request)
|
|
} catch {
|
|
print("Failed to schedule notification: \(error)")
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - UNUserNotificationCenterDelegate
|
|
extension NotificationManager: UNUserNotificationCenterDelegate {
|
|
func userNotificationCenter(
|
|
_ center: UNUserNotificationCenter,
|
|
didReceive response: UNNotificationResponse,
|
|
withCompletionHandler completionHandler: @escaping () -> Void
|
|
) {
|
|
// Handle notification tap
|
|
handleNotificationResponse(response)
|
|
completionHandler()
|
|
}
|
|
|
|
func userNotificationCenter(
|
|
_ center: UNUserNotificationCenter,
|
|
willPresent notification: UNNotification,
|
|
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
|
|
) {
|
|
// Show notification even when app is in foreground
|
|
completionHandler([.banner, .sound])
|
|
}
|
|
|
|
private func handleNotificationResponse(_ response: UNNotificationResponse) {
|
|
// Handle different notification actions
|
|
switch response.actionIdentifier {
|
|
case UNNotificationDefaultActionIdentifier:
|
|
// User tapped the notification
|
|
break
|
|
case UNNotificationDismissActionIdentifier:
|
|
// User dismissed the notification
|
|
break
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## App Group UserDefaults
|
|
Use app group UserDefaults for settings shared with system extensions:
|
|
|
|
```swift
|
|
// ✅ CORRECT - App group UserDefaults
|
|
extension AppUserDefaults {
|
|
private static let appGroupUserDefaults = UserDefaults(suiteName: "group.com.duckduckgo.app")
|
|
|
|
var networkProtectionEnabled: Bool {
|
|
get {
|
|
appGroupUserDefaults?.bool(forKey: "network_protection_enabled") ?? false
|
|
}
|
|
set {
|
|
appGroupUserDefaults?.set(newValue, forKey: "network_protection_enabled")
|
|
// Notify system extension of change
|
|
notifySystemExtension(of: .networkProtectionToggled(newValue))
|
|
}
|
|
}
|
|
|
|
var vpnServerLocation: String? {
|
|
get {
|
|
appGroupUserDefaults?.string(forKey: "vpn_server_location")
|
|
}
|
|
set {
|
|
appGroupUserDefaults?.set(newValue, forKey: "vpn_server_location")
|
|
}
|
|
}
|
|
|
|
private func notifySystemExtension(of change: SystemExtensionNotification) {
|
|
// Send notification to system extension via app group communication
|
|
let notificationName = "com.duckduckgo.settings.changed"
|
|
DistributedNotificationCenter.default().post(
|
|
name: Notification.Name(notificationName),
|
|
object: change.rawValue
|
|
)
|
|
}
|
|
}
|
|
|
|
enum SystemExtensionNotification: String {
|
|
case networkProtectionToggled = "network_protection_toggled"
|
|
case vpnServerChanged = "vpn_server_changed"
|
|
}
|
|
```
|
|
|
|
See `macos-window-management.md` for window management patterns and `macos-preferences.md` for preferences UI patterns.
|