jaywcjlove/permissionflow
[中文](./README.zh.md) • [Installation](#installation) • [Public API](#public-api) • [System Settings URL Scheme](#system-settings-url-scheme)
Features
- 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
Requirements
- macOS 13+
- Swift 6 package toolchain
- SwiftUI + AppKit host application
Installation
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.
Supported Permission Panes
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.
Quick Start
SwiftUI button
import PermissionFlow
import SwiftUI
struct ContentView: View {
var body: some View {
PermissionFlowButton(
title: "Grant Accessibility",
pane: .accessibility,
suggestedAppURLs: [Bundle.main.bundleURL]
)
}
}Enable optional status detection
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()
}
}
}Manual status display
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)
}
}Manual controller usage
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]
)
}
}Keep the launch animation
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
)
}
}Public API
PermissionFlowButton
Convenience SwiftUI button for launching a permission flow.
PermissionFlowButton(
title: "Open Screen Recording",
pane: .screenRecording,
suggestedAppURLs: [Bundle.main.bundleURL],
configuration: .init()
)PermissionFlow.makeController
Creates a reusable controller:
let controller = PermissionFlow.makeController(
configuration: .init(
requiredAppURLs: [Bundle.main.bundleURL],
promptForAccessibilityTrust: false
)
)PermissionFlowController
Main entry points:
authorize(pane:suggestedAppURLs:sourceFrameInScreen:)showPanel()closePanel()resetDroppedApps()registerDroppedApp(_:)
SystemSettings.open
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))System Settings URL Scheme
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.
URL format
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?ScreenSaverPackage type
public struct SystemSettingsDestination {
public let paneIdentifier: String
public let anchor: String?
public var url: URL { get }
}Convenience destinations
The package includes a few common helpers:
.wallpaper.displays.displays(anchor:).bluetooth.loginItems.privacy(anchor:)
Privacy anchors
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 anchors
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
Configuration
let configuration = PermissionFlowConfiguration(
requiredAppURLs: [Bundle.main.bundleURL],
promptForAccessibilityTrust: false
)Notes
requiredAppURLspreloads apps into the panelpromptForAccessibilityTrustcontrols whether AX trust is actively prompted
How It Works
- 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.
Example
The repository includes an Example macOS app that demonstrates all supported permission flows.
Notes and Limitations
- 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.
License
Licensed under the MIT License.
Package Metadata
Repository: jaywcjlove/permissionflow
Default branch: main
README: README.md