Make permission monitoring dependent on views

This commit is contained in:
Peter Steinberger 2025-06-22 10:27:56 +02:00
parent a121d09fee
commit 41d602d6d9
3 changed files with 103 additions and 15 deletions

View file

@ -5,6 +5,10 @@ import Foundation
import Observation
import OSLog
extension Notification.Name {
static let permissionsUpdated = Notification.Name("sh.vibetunnel.permissionsUpdated")
}
/// Types of system permissions that VibeTunnel requires
enum SystemPermission {
case appleScript
@ -63,8 +67,14 @@ final class SystemPermissionManager {
category: "SystemPermissions"
)
/// Timer for monitoring permission changes
private var monitorTimer: Timer?
/// Count of views that have registered for monitoring
private var monitorRegistrationCount = 0
private init() {
// No automatic monitoring - UI components will check when visible
// No automatic monitoring - UI components will register when visible
}
// MARK: - Public API
@ -127,13 +137,67 @@ final class SystemPermissionManager {
}
}
// MARK: - Permission Monitoring
/// Register for permission monitoring (call when a view appears)
func registerForMonitoring() {
monitorRegistrationCount += 1
logger.debug("Registered for monitoring, count: \(self.monitorRegistrationCount)")
if monitorRegistrationCount == 1 {
// First registration, start monitoring
startMonitoring()
}
}
/// Unregister from permission monitoring (call when a view disappears)
func unregisterFromMonitoring() {
monitorRegistrationCount = max(0, monitorRegistrationCount - 1)
logger.debug("Unregistered from monitoring, count: \(self.monitorRegistrationCount)")
if monitorRegistrationCount == 0 {
// No more registrations, stop monitoring
stopMonitoring()
}
}
private func startMonitoring() {
logger.info("Starting permission monitoring")
// Initial check
Task {
await checkAllPermissions()
}
// Start timer for periodic checks
monitorTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
guard let self else { return }
Task { @MainActor in
await self.checkAllPermissions()
}
}
}
private func stopMonitoring() {
logger.info("Stopping permission monitoring")
monitorTimer?.invalidate()
monitorTimer = nil
}
// MARK: - Permission Checking
func checkAllPermissions() async {
let oldPermissions = permissions
// Check each permission type
permissions[.appleScript] = await checkAppleScriptPermission()
permissions[.screenRecording] = checkScreenRecordingPermission()
permissions[.accessibility] = checkAccessibilityPermission()
// Post notification if any permissions changed
if oldPermissions != permissions {
NotificationCenter.default.post(name: .permissionsUpdated, object: nil)
}
}
// MARK: - AppleScript Permission

View file

@ -130,14 +130,20 @@ private struct PermissionsSection: View {
@State private var permissionManager = SystemPermissionManager.shared
@State private var permissionUpdateTrigger = 0
// IMPORTANT: These computed properties ensure the UI always shows current permission state.
// The permissionUpdateTrigger dependency forces SwiftUI to re-evaluate these properties
// when permissions change. Without this, the UI would not update when permissions are
// granted in System Settings while this view is visible.
//
// We use computed properties instead of @State to avoid UI flashing - the initial
// permission check in .task happens before the first render, ensuring correct state
// from the start.
private var hasAppleScriptPermission: Bool {
// This will cause a re-read whenever permissionUpdateTrigger changes
_ = permissionUpdateTrigger
return permissionManager.hasPermission(.appleScript)
}
private var hasAccessibilityPermission: Bool {
// This will cause a re-read whenever permissionUpdateTrigger changes
_ = permissionUpdateTrigger
return permissionManager.hasPermission(.accessibility)
}
@ -232,13 +238,19 @@ private struct PermissionsSection: View {
.multilineTextAlignment(.center)
}
}
.onReceive(Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()) { _ in
// Force a re-render to check permissions
permissionUpdateTrigger += 1
}
.task {
// Check all permissions
// Check permissions before first render to avoid UI flashing
await permissionManager.checkAllPermissions()
// Register for continuous monitoring
permissionManager.registerForMonitoring()
}
.onDisappear {
permissionManager.unregisterFromMonitoring()
}
.onReceive(NotificationCenter.default.publisher(for: .permissionsUpdated)) { _ in
// Increment trigger to force computed property re-evaluation
permissionUpdateTrigger += 1
}
}
}

View file

@ -22,14 +22,20 @@ struct RequestPermissionsPageView: View {
@State private var permissionManager = SystemPermissionManager.shared
@State private var permissionUpdateTrigger = 0
// IMPORTANT: These computed properties ensure the UI always shows current permission state.
// The permissionUpdateTrigger dependency forces SwiftUI to re-evaluate these properties
// when permissions change. Without this, the UI would not update when permissions are
// granted in System Settings while this view is visible.
//
// We use computed properties instead of @State to avoid UI flashing - the initial
// permission check in .task happens before the first render, ensuring correct state
// from the start.
private var hasAppleScriptPermission: Bool {
// This will cause a re-read whenever permissionUpdateTrigger changes
_ = permissionUpdateTrigger
return permissionManager.hasPermission(.appleScript)
}
private var hasAccessibilityPermission: Bool {
// This will cause a re-read whenever permissionUpdateTrigger changes
_ = permissionUpdateTrigger
return permissionManager.hasPermission(.accessibility)
}
@ -102,13 +108,19 @@ struct RequestPermissionsPageView: View {
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
.onReceive(Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()) { _ in
// Force a re-render to check permissions
permissionUpdateTrigger += 1
}
.task {
// Check all permissions
// Check permissions before first render to avoid UI flashing
await permissionManager.checkAllPermissions()
// Register for continuous monitoring
permissionManager.registerForMonitoring()
}
.onDisappear {
permissionManager.unregisterFromMonitoring()
}
.onReceive(NotificationCenter.default.publisher(for: .permissionsUpdated)) { _ in
// Increment trigger to force computed property re-evaluation
permissionUpdateTrigger += 1
}
}
}