中文 • Installation • Public API • System Settings URL Scheme
PermissionFlow is a macOS permission-guidance library that opens the target System Settings privacy pane and, for supported drag-based authorization pages, shows a floating panel that follows the System Settings window and lets users drag the current .app into the permission list. It also includes SystemSettingsKit for strongly typed deeplinks into System Settings pages and subsections.
PermissionFlow: macOS-only floating guidance for drag-based privacy authorizationSystemSettingsKit: typed Settings deeplinks for macOS, with partial iOS support
It opens the correct privacy page automatically and, for panes that support drag-based authorization, shows a floating helper panel that follows the System Settings window and lets the user drag the current .app bundle into the permission list.
- Real-time permission status display: Buttons automatically show whether permissions are granted with visual feedback (green checkmark for granted, blue arrow for not granted)
- Opens the target
System Settingsprivacy pane automatically - Animates the floating panel from the click position to the
System Settingswindow - Follows the
System Settingswindow while it moves - Shows the current app as a native drag source
- Keeps only one active floating panel at a time
- Closes the floating panel automatically when
System Settingscloses - Supports adaptive floating panel height based on content
- Intelligent permission detection: Uses official Apple APIs for accurate permission status checking without triggering system prompts
- macOS 13+
- Swift 6 package toolchain
- SwiftUI + AppKit host application
Add the package to your app:
dependencies: [
.package(url: "https://github.com/jaywcjlove/PermissionFlow.git", from: "1.0.0")
]The package URL and installation entry stay the same as before. What changed is the product layout: permission status detection for some panes is now split into optional extensions instead of being linked by default.
This package now exposes these library products:
PermissionFlow: floating authorization guidance for supported privacy panes on macOSSystemSettingsKit: reusable deeplink API for arbitrary System Settings pagesPermissionFlowExtendedStatus: one-stop optional status detection for.bluetooth,.inputMonitoring,.mediaAppleMusic, and.screenRecordingPermissionFlowBluetoothStatus: optional status detection for.bluetoothPermissionFlowMediaStatus: optional status detection for.mediaAppleMusicPermissionFlowInputMonitoringStatus: optional status detection for.inputMonitoringPermissionFlowScreenRecordingStatus: optional status detection for.screenRecording
Then add the product you need to your target:
.target(
name: "YourApp",
dependencies: [
.product(name: "PermissionFlow", package: "PermissionFlow"),
.product(name: "SystemSettingsKit", package: "PermissionFlow")
]
)If you want status detection for .bluetooth, .inputMonitoring, .mediaAppleMusic, and .screenRecording, add the optional extension product as well:
.target(
name: "YourApp",
dependencies: [
.product(name: "PermissionFlow", package: "PermissionFlow"),
.product(name: "PermissionFlowExtendedStatus", package: "PermissionFlow")
]
)You can also depend on only the specific extension products you need:
.product(name: "PermissionFlowBluetoothStatus", package: "PermissionFlow")
.product(name: "PermissionFlowMediaStatus", package: "PermissionFlow")
.product(name: "PermissionFlowInputMonitoringStatus", package: "PermissionFlow")
.product(name: "PermissionFlowScreenRecordingStatus", package: "PermissionFlow")Why this split matters:
- Apps that only use
PermissionFlowkeep the original core integration and do not need to link optional status-detection modules by default. - This reduces unnecessary compile-time and link-time dependencies such as
CoreBluetooth,MusicKit, andCarbonwhen those permission states are not needed. - In practice, this usually keeps the final app product cleaner and can reduce the amount of optional code that ends up linked into your binary.
Platform support:
PermissionFlow:macOS 13+SystemSettingsKit:macOS 13+,iOS 16+
SystemSettingsKit is intentionally partial on iOS. The macOS deeplink-based pane and anchor APIs remain macOS-only, while iOS only exposes destinations that are publicly supported by UIKit, such as the current app's Settings page.
PermissionFlow covers these 8 privacy panes that support the floating drag-and-drop authorization workflow:
.accessibility: OpensPrivacy & Security > Accessibility. ✅ Status Detection Supported.fullDiskAccess: OpensPrivacy & Security > Full Disk Access. ✅ Status Detection Supported.inputMonitoring: OpensPrivacy & Security > Input Monitoring. ✅ Status Detection Supported.screenRecording: OpensPrivacy & Security > Screen Recording. ✅ Status Detection Supported.bluetooth: OpensPrivacy & Security > Bluetooth. ✅ Supports status detection.mediaAppleMusic: OpensPrivacy & Security > Media & Apple Music. ✅ Supports status detection.appManagement: OpensPrivacy & Security > App Management.⚠️ Status detection not available.developerTools: OpensPrivacy & Security > Developer Tools.⚠️ Status detection not available
Permission Status Display: For supported permissions, PermissionFlowButton automatically displays the current authorization status:
- ✅ Granted: Green checkmark icon with "Granted" text
- ➡️ Not Granted: Blue arrow icon with "Grant" text
- Built into
PermissionFlow:.accessibility,.fullDiskAccess - Available through optional status extensions:
.bluetooth,.inputMonitoring,.mediaAppleMusic,.screenRecording - 🔄 Checking: Clock icon with "Checking..." text
- ❓ Unknown: Blue arrow icon with "Open" text (for unsupported detection)
For every other System Settings page or privacy subsection, use SystemSettingsKit.
import PermissionFlow
import SwiftUI
struct ContentView: View {
var body: some View {
PermissionFlowButton(
title: "Grant Accessibility",
pane: .accessibility,
suggestedAppURLs: [Bundle.main.bundleURL]
)
}
}To enable status detection for .bluetooth, .inputMonitoring, .mediaAppleMusic, and .screenRecording, add the optional extension products and register them once at app startup:
import PermissionFlowExtendedStatus
import SwiftUI
@main
struct MyApp: App {
init() {
PermissionFlowExtendedStatus.register()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}import AppKit
import PermissionFlow
import SwiftUI
struct ManualPermissionButton: View {
@StateObject private var controller = PermissionFlow.makeController()
@State private var authorizationState: PermissionAuthorizationState = .checking
let didBecomeActive = NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)
var body: some View {
Button {
controller.authorize(
pane: .accessibility,
suggestedAppURLs: [Bundle.main.bundleURL],
sourceFrameInScreen: clickSourceFrameInScreen()
)
} label: {
Label {
Text(title(for: authorizationState))
} icon: {
let icon = PermissionFlowButtonState.make(from: authorizationState).systemImage
Image(systemName: icon)
}
}
.onAppear(perform: refreshStatus)
.onReceive(didBecomeActive) { _ in
refreshStatus()
}
}
private func refreshStatus() {
let provider = PermissionStatusRegistry.provider(for: .accessibility)
authorizationState = provider.authorizationState()
}
private func title(for state: PermissionAuthorizationState) -> String {
switch state {
case .granted:
"Granted"
case .notGranted:
"Grant"
case .unknown:
"Open"
case .checking:
"Checking..."
}
}
private func clickSourceFrameInScreen() -> CGRect {
let mouse = NSEvent.mouseLocation
return CGRect(x: mouse.x - 16, y: mouse.y - 16, width: 32, height: 32)
}
}Use PermissionFlowController when you want to control the flow yourself:
import PermissionFlow
import SwiftUI
@MainActor
final class PermissionViewModel: ObservableObject {
private let controller = PermissionFlow.makeController()
func requestFullDiskAccess() {
controller.authorize(
pane: .fullDiskAccess,
suggestedAppURLs: [Bundle.main.bundleURL]
)
}
}If you use PermissionFlowButton, the package captures the click position for you and the floating panel will animate from the button click to the System Settings window automatically.
If you call PermissionFlowController.authorize(...) manually, pass the click source frame yourself. Otherwise the panel will still appear, but it will skip the launch animation and jump directly to the target position.
import AppKit
import PermissionFlow
@MainActor
final class PermissionViewModel: ObservableObject {
private let controller = PermissionFlow.makeController()
func requestAccessibility() {
let mouseLocation = NSEvent.mouseLocation
let sourceFrame = CGRect(
x: mouseLocation.x - 16,
y: mouseLocation.y - 16,
width: 32,
height: 32
)
controller.authorize(
pane: .accessibility,
suggestedAppURLs: [Bundle.main.bundleURL],
sourceFrameInScreen: sourceFrame
)
}
}Convenience SwiftUI button for launching a permission flow.
PermissionFlowButton(
title: "Open Screen Recording",
pane: .screenRecording,
suggestedAppURLs: [Bundle.main.bundleURL],
configuration: .init()
)Creates a reusable controller:
let controller = PermissionFlow.makeController(
configuration: .init(
requiredAppURLs: [Bundle.main.bundleURL],
promptForAccessibilityTrust: false
)
)Main entry points:
authorize(pane:suggestedAppURLs:sourceFrameInScreen:)showPanel()closePanel()resetDroppedApps()registerDroppedApp(_:)
Open any System Settings page directly from a pane identifier and optional anchor:
import SystemSettingsKit
SystemSettings.open(
paneIdentifier: "com.apple.Wallpaper-Settings.extension"
)
SystemSettings.open(
paneIdentifier: "com.apple.settings.PrivacySecurity.extension",
anchor: "Privacy_Advertising"
)You can also use SystemSettingsDestination:
import SystemSettingsKit
SystemSettings.open(.wallpaper)
SystemSettings.open(.privacy(anchor: .privacyAllFiles))
SystemSettings.open(.displays(anchor: .resolutionSection))SystemSettingsKit exposes a lightweight API for opening arbitrary System Settings panes using the x-apple.systempreferences: URL scheme.
The behavior and examples are based on the identifiers and deeplink notes collected in SystemSettings-URLs-macOS.
x-apple.systempreferences:<pane-identifier>
x-apple.systempreferences:<pane-identifier>?<anchor>
Examples:
x-apple.systempreferences:com.apple.Wallpaper-Settings.extension
x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Advertising
x-apple.systempreferences:com.apple.Wallpaper-Settings.extension?ScreenSaver
public struct SystemSettingsDestination {
public let paneIdentifier: String
public let anchor: String?
public var url: URL { get }
}The package includes a few common helpers:
.wallpaper.displays.displays(anchor:).bluetooth.loginItems.privacy(anchor:)
For Privacy & Security subsections, use:
SystemSettings.open(.privacy(anchor: .privacyAllFiles))
SystemSettings.open(.privacy(anchor: .privacyAdvertising))
SystemSettings.open(.privacy(anchor: .privacyAccessibility))
SystemSettings.open(.privacy(anchor: .security))The existing PermissionFlowPane type continues to handle the privacy pages used by the floating authorization workflow.
.appManagement: OpensPrivacy & Security > App Management..accessibility: OpensPrivacy & Security > Accessibility..bluetooth: OpensPrivacy & Security > Bluetooth..developerTools: OpensPrivacy & Security > Developer Tools..fullDiskAccess: OpensPrivacy & Security > Full Disk Access..inputMonitoring: OpensPrivacy & Security > Input Monitoring..mediaAppleMusic: OpensPrivacy & Security > Media & Apple Music..screenRecording: OpensPrivacy & Security > Screen Recording.
Available typed privacy anchors and their destinations:
.advanced:Privacy & Security > Advanced.fileVault:Privacy & Security > FileVault.locationAccessReport:Privacy & Security > Location Access Report.lockdownMode:Privacy & Security > Lockdown Mode.privacyAccessibility:Privacy & Security > Accessibility.privacyAdvertising:Privacy & Security > Advertising.privacyAllFiles:Privacy & Security > Full Disk Access.privacyAnalytics:Privacy & Security > Analytics & Improvements.privacyAppBundles:Privacy & Security > App Management.privacyAudioCapture:Privacy & Security > Audio Capture.privacyAutomation:Privacy & Security > Automation.privacyBluetooth:Privacy & Security > Bluetooth.privacyCalendars:Privacy & Security > Calendars.privacyCamera:Privacy & Security > Camera.privacyContacts:Privacy & Security > Contacts.privacyDevTools:Privacy & Security > Developer Tools.privacyFilesAndFolders:Privacy & Security > Files & Folders.privacyFocus:Privacy & Security > Focus.privacyHomeKit:Privacy & Security > Home.privacyListenEvent:Privacy & Security > Input Monitoring.privacyLocationServices:Privacy & Security > Location Services.privacyMedia:Privacy & Security > Media & Apple Music.privacyMicrophone:Privacy & Security > Microphone.privacyMotion:Privacy & Security > Motion & Fitness.privacyNudityDetection:Privacy & Security > Sensitive Content Warning.privacyPasskeyAccess:Privacy & Security > Passkey Access.privacyPhotos:Privacy & Security > Photos.privacyReminders:Privacy & Security > Reminders.privacyRemoteDesktop:Privacy & Security > Remote Desktop.privacyScreenCapture:Privacy & Security > Screen Recording.privacySpeechRecognition:Privacy & Security > Speech Recognition.privacySystemServices:Privacy & Security > System Services.security:Privacy & Security > Security.securityImprovements:Privacy & Security > Security Improvements
Displays now has a typed helper instead of raw string anchors:
SystemSettings.open(.displays)
SystemSettings.open(.displays(anchor: .arrangementSection))
SystemSettings.open(.displays(anchor: .resolutionSection))
SystemSettings.open(.displays(anchor: .nightShiftSection))Available display anchors and their destinations:
.advancedSection:Displays > Advanced.ambienceSection:Displays > Ambience.arrangementSection:Displays > Arrangement.characteristicSection:Displays > Display Characteristics.displaysSection:Displays > Displays.miscellaneousSection:Displays > Miscellaneous.nightShiftSection:Displays > Night Shift.profileSection:Displays > Color Profile.resolutionSection:Displays > Resolution.sidecarSection:Displays > Sidecar
let configuration = PermissionFlowConfiguration(
requiredAppURLs: [Bundle.main.bundleURL],
promptForAccessibilityTrust: false
)requiredAppURLspreloads apps into the panelpromptForAccessibilityTrustcontrols whether AX trust is actively prompted
- Your app requests a permission pane.
PermissionFlowopens the matchingSystem Settingspage.- If that pane supports drag-based authorization, a floating panel appears.
- The panel animates from the click location to the
System Settingswindow. - The panel tracks the
System Settingswindow position. - The user drags the current
.appbundle into the permission list.
The repository includes an Example macOS app that demonstrates all supported permission flows.
- The floating helper is only shown for panes that support app-list style authorization.
- Permission status detection: Uses official Apple APIs (
CGPreflightListenEventAccess,CGPreflightScreenCaptureAccess,AXIsProcessTrusted) for accurate status checking without triggering system prompts. - Status refresh: Permission status is automatically refreshed when the app becomes active and when buttons appear on screen.
System Settingsbehavior is controlled by macOS and may vary slightly by OS version.- AX-based window tracking is used when available. Window Server frame lookup is used as fallback and bootstrap.
- The package does not bypass macOS security. It only guides the user through the system UI.
Licensed under the MIT License.