13 KiB
13 KiB
source, confidence, namespace, last_synced, alwaysApply
| source | confidence | namespace | last_synced | alwaysApply |
|---|---|---|---|---|
| ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/macos-system-integration.mdc | 0.9 | work | 2026-04-28 | false |
macOS System Integration Patterns
Background Agents and Services
Use proper service management for background agents:
// ✅ 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:
// ✅ 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:
// ✅ 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:
// ✅ 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:
// ✅ 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:
// ✅ 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:
// ✅ 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.