You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

macOS开发:如何获取桌面目录文件的永久访问权限

macOS应用永久保留目录访问权限实现方案

问题背景

开发一款统计macOS桌面目录下文件与文件夹总数的应用,已搭建最小可复现测试项目,需求如下:

  • 应用首次启动时引导用户选择桌面路径完成授权
  • 后续应用重启(进程被杀死后重新打开)无需再次引导选择路径,无需重复触发系统权限弹窗
  • 一次用户授权后永久保留目录访问权限,实现类似终端应用在系统隐私设置中常驻授权的效果

系统隐私设置常驻授权效果

当前已关闭应用Sandbox,测试代码如下:

struct ContentView: View {
    
    @State private var contents: [String]? = nil
    
    var body: some View {

        VStack(spacing: 10.0) {
            Text("Count of contents of Desktop is:")
            Text(contents != nil ? String(contents!.count) : "Unknown").bold()
        }
        .frame(width: 200.0, height: 200.0)
        .padding()
        .onAppear {
            contents = directoryContentsReader(folderLocation: "/Users/myNameHere/Desktop")
        }
    }
    
}

func directoryContentsReader(folderLocation: String) -> [String]? {
    do {
        return try FileManager.default.contentsOfDirectory(atPath: folderLocation)
    }
    catch(let error) {
        print(error.localizedDescription)
        return nil
    }
}

测试过程中macOS会随机弹出目录访问权限申请弹窗,频繁打扰用户,需要实现用户完成一次授权后,应用永久拥有对应目录访问权限,不再重复弹出权限申请弹窗。

重复弹出的目录访问权限申请弹窗


问题原因

当前代码硬编码桌面路径直接访问,触发的是macOS TCC(Transparency, Consent, Control,透明同意控制)安全机制的临时授权流程。这类临时授权不会被系统永久记录,在应用重启、TCC缓存重置、系统版本更新等场景下会重新触发权限判定,因此会反复弹出授权提示。


实现方案

方案1:使用安全范围书签(官方推荐,适配所有场景)

安全范围书签是苹果官方提供的用户目录访问权限持久化方案,无论应用是否开启沙盒都可使用,一次授权后权限永久生效,授权条目会自动同步到系统隐私设置列表,不会重复弹窗,实现步骤如下:

  1. 首次启动时不直接硬编码路径访问,通过NSOpenPanel引导用户主动选择目标目录,获取用户明确授权
  2. 对用户选中的目录URL生成安全范围书签数据,将书签数据持久化存储到本地(UserDefaults、应用配置文件均可)
  3. 后续应用启动时,直接读取本地存储的书签数据解析为可访问URL,开启安全范围访问权限后即可正常读写目录,无需重复授权

核心实现代码:

import SwiftUI
import Security

// 持久化书签存储键
private let desktopDirBookmarkKey = "com.yourApp.desktopAccessBookmark"

struct ContentView: View {
    @State private var contentsCount: String = "未知"
    
    var body: some View {
        VStack(spacing: 10) {
            Text("桌面目录文件/文件夹总数:")
            Text(contentsCount).bold()
            Button("重新选择目录授权") {
                triggerFolderSelectFlow()
            }
        }
        .frame(width: 250, height: 200)
        .padding()
        .onAppear {
            // 优先读取本地已存储的书签恢复权限
            guard let savedBookmark = UserDefaults.standard.data(forKey: desktopDirBookmarkKey) else {
                triggerFolderSelectFlow()
                return
            }
            var isStale = false
            do {
                let accessURL = try URL(
                    resolvingBookmarkData: savedBookmark,
                    options: [.withSecurityScope, .withoutUI],
                    relativeTo: nil,
                    bookmarkDataIsStale: &isStale
                )
                // 书签过期时重新走授权流程
                if isStale {
                    triggerFolderSelectFlow()
                    return
                }
                // 开启安全范围访问
                guard accessURL.startAccessingSecurityScopedResource() else {
                    triggerFolderSelectFlow()
                    return
                }
                // 读取目录内容
                if let contents = directoryContentsReader(folderLocation: accessURL.path) {
                    contentsCount = String(contents.count)
                }
                // 访问完成后可按需调用停止访问方法
                // accessURL.stopAccessingSecurityScopedResource()
            } catch {
                triggerFolderSelectFlow()
            }
        }
    }
    
    // 唤起目录选择面板,生成并持久化安全书签
    private func triggerFolderSelectFlow() {
        let openPanel = NSOpenPanel()
        openPanel.canChooseDirectories = true
        openPanel.canChooseFiles = false
        openPanel.directoryURL = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask).first
        openPanel.prompt = "授权访问"
        openPanel.begin { response in
            guard response == .OK, let selectedURL = openPanel.url else { return }
            do {
                // 生成带安全范围的永久书签
                let bookmarkData = try selectedURL.bookmarkData(
                    options: .withSecurityScope,
                    includingResourceValuesForKeys: nil,
                    relativeTo: nil
                )
                // 持久化存储到本地
                UserDefaults.standard.set(bookmarkData, forKey: desktopDirBookmarkKey)
                // 开启权限读取目录
                selectedURL.startAccessingSecurityScopedResource()
                if let contents = directoryContentsReader(folderLocation: selectedURL.path) {
                    contentsCount = String(contents.count)
                }
            } catch {
                print("书签生成失败:\(error.localizedDescription)")
            }
        }
    }
}

func directoryContentsReader(folderLocation: String) -> [String]? {
    do {
        return try FileManager.default.contentsOfDirectory(atPath: folderLocation)
    } catch {
        print("目录读取失败:\(error.localizedDescription)")
        return nil
    }
}

方案2:Info.plist声明固定目录权限(适配固定访问系统目录场景)

如果应用不需要支持用户自定义选择目录,仅固定访问桌面、文稿、下载这类系统默认受保护目录,可直接在Info.plist中添加对应目录的访问权限说明,配置完成后用户首次访问时仅会弹出一次授权提示,用户同意后权限永久生效,不会重复弹窗。
需要添加的权限键值:

  • 访问桌面目录:添加键NSDesktopFolderUsageDescription,值为权限用途说明,例如「需要访问桌面目录以统计文件与文件夹数量」
  • 其他常用受保护目录对应权限键:
    • NSDocumentsFolderUsageDescription:访问文稿目录
    • NSDownloadsFolderUsageDescription:访问下载目录
    • NSRemovableVolumesUsageDescription:访问外接存储设备
    • NSNetworkVolumesUsageDescription:访问网络存储

注意:该方案仅适用于访问系统默认枚举的受保护目录,若需要访问用户自定义的任意目录,必须使用方案1的安全范围书签实现。


内容的提问来源于stack exchange,提问作者swiftPunk

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 08:15:44