extension FileManager

iOS应用沙盒目录详解与优化

1. Bundle(应用程序包)目录

  • 路径[NSBundle mainBundle].bundlePath
  • 特点
    • 只读目录,包含应用二进制文件、资源文件(如图片、音频、nib文件等)。
    • 安装时由系统签名,修改会导致应用无法启动(越狱设备除外)。
    • 不可存储用户数据,否则会被拒绝上架。
  • 优化建议
    • 动态资源(如配置文件)应放在其他可写目录,通过NSBundle仅读取静态资源。

2. Documents

  • 路径NSHomeDirectory()/Documents
  • 特点
    • 用于存储用户生成的文件或应用独有的数据(如数据库、绘图作品、游戏存档)。
    • iTunes/iCloud自动备份(需注意用户隐私和数据大小)。
    • 通过NSFileManagerURLForDirectory:inDomain:appropriateForURL:create:error:获取。
  • 注意事项
    • 禁止缓存网络下载内容(否则违反Apple审核指南,需使用Library/Caches)。
    • 大文件建议标记为NSURLIsExcludedFromBackupKey避免占用iCloud空间。
  • 优化建议
    • 敏感数据应加密存储(如用户文档)。

3. Library

3.1 Library/Caches

  • 路径NSHomeDirectory()/Library/Caches
  • 特点
    • 存储临时文件(如缓存图片、离线地图),可重新下载或生成的数据。
    • 不备份到iTunes/iCloud,系统磁盘不足时可能被清理(但开发者需主动管理)。
    • 通过NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)获取。
  • 优化建议
    • 实现定期清理逻辑(如SDWebImage的缓存自动清理机制)。
    • 大文件建议分目录存储(如/Caches/Images/)。

3.2 Library/Preferences

  • 路径NSHomeDirectory()/Library/Preferences
  • 特点
    • 通过NSUserDefaults读写,存储用户设置(如主题、登录状态)。
    • 自动备份到iTunes/iCloud(需避免存储敏感信息)。
    • 文件格式为.plist,无需直接操作文件系统。
  • 优化建议
    • 调用synchronize方法强制写入(iOS 12+已优化,通常无需手动调用)。
    • 复杂数据建议使用Keychain(如密码)或数据库。

3.3 Library/Application Support(需补充)

  • 路径NSHomeDirectory()/Library/Application Support
  • 特点
    • 存储应用运行所需的持久化数据(如模板、语音模型)。
    • 默认备份,可通过NSURLIsExcludedFromBackupKey排除。
    • 需手动创建目录,适合存放不可再生数据。

4. tmp

  • 路径NSTemporaryDirectory()
  • 特点
    • 存储短期临时文件(如解压的ZIP包、拍摄的临时照片)。
    • 不备份,系统可能随时清空(如重启、空间不足)。
  • 优化建议
    • 文件使用后应立即删除,避免占用空间。
    • 高频操作建议使用内存缓存(如NSCache)替代。

补充说明

  1. iCloud备份规则

    • Documents、Library/Application Support默认备份,可通过addSkipBackupAttributeToItemAtURL排除。
    • 用户可通过系统设置关闭单个应用的iCloud备份。
  2. 目录选择原则

    • 用户可见文件Documents(如PDF导出)。
    • 内部缓存Library/Caches(如视频缓存)。
    • 临时处理tmp(如拍照编辑中间文件)。
  3. 安全建议

    • 敏感数据(如Token)应存于Keychain。
    • 使用FileProtectionAPI为文件添加加密(如NSFileProtectionComplete)。
  4. 调试技巧

    • 通过Xcode的Device File Explorer查看沙盒目录。
    • 使用lsof -p <pid>命令监控文件打开状态。

extension FileManager

extension FileManager {
    // MARK: 获取Home的完整路径名
    /// 获取Home的完整路径名
    /// - Returns: Home的完整路径名
    static func homeDirectory() -> String {
        //获取程序的Home目录
        let homeDirectory = NSHomeDirectory()
        return homeDirectory
    }
    
    // MARK: 获取Documnets的完整路径名
    /// 获取Documnets的完整路径名
    /// - Returns: Documnets的完整路径名
    static func DocumnetsDirectory() -> String {
        //获取程序的documentPaths目录
        //方法1
        // let documentPaths = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)
        // let documnetPath = documentPaths[0]
    
       //方法2
        let ducumentPath = NSHomeDirectory() + "/Documents"
        return ducumentPath
    }
    
    // MARK: 获取Library的完整路径名
    /**
     这个目录下有两个子目录:Caches 和 Preferences
     Library/Preferences目录,包含应用程序的偏好设置文件。不应该直接创建偏好设置文件,而是应该使用NSUserDefaults类来取得和设置应用程序的偏好。
     Library/Caches目录,主要存放缓存文件,iTunes不会备份此目录,此目录下文件不会再应用退出时删除
     */
    /// 获取Library的完整路径名
    /// - Returns: Library的完整路径名
    static func LibraryDirectory() -> String {
        //获取程序的documentPaths目录
        //Library目录-方法1
        // let libraryPaths = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.libraryDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)
        // let libraryPath = libraryPaths[0]
        //
        // Library目录-方法2
        let libraryPath = NSHomeDirectory() + "/Library"
        return libraryPath
    }
    
    // MARK: 获取/Library/Caches的完整路径名
    /// 获取/Library/Caches的完整路径名
    /// - Returns: /Library/Caches的完整路径名
    static func CachesDirectory() -> String {
        //获取程序的/Library/Caches目录
        let cachesPath = NSHomeDirectory() + "/Library/Caches"
        return cachesPath
    }
    
    // MARK: 获取Library/Preferences的完整路径名
    /// 获取Library/Preferences的完整路径名
    /// - Returns: Library/Preferences的完整路径名
    static func PreferencesDirectory() -> String {
        //Library/Preferences目录-方法2
        let preferencesPath = NSHomeDirectory() + "/Library/Preferences"
        return preferencesPath
    }
    
    // MARK: 获取Tmp的完整路径名
    /// 获取Tmp的完整路径名,用于存放临时文件,保存应用程序再次启动过程中不需要的信息,重启后清空
    /// - Returns: Tmp的完整路径名
    static func TmpDirectory() -> String {
        //方法1
        //let tmpDir = NSTemporaryDirectory()
        //方法2
        let tmpDir = NSHomeDirectory() + "/tmp"
        return tmpDir
    }
}
// MARK: - 二、文件以及文件夹的操作 扩展
public extension FileManager {
    // MARK: 文件写入的类型
    /// 文件写入的类型
    enum FileWriteType {
        case TextType
        case ImageType
        case ArrayType
        case DictionaryType
    }
    
    // MARK: 移动或者拷贝的类型
    /// 移动或者拷贝的类型
    enum MoveOrCopyType {
        case file
        case directory
    }
    /// 文件管理器
    static var fileManager: FileManager {
        return FileManager.default
    }
    
    // MARK: 创建文件夹(蓝色的,文件夹和文件是不一样的)
    /// 创建文件夹(蓝色的,文件夹和文件是不一样的)
    /// - Parameter folderName: 文件夹的名字
    /// - Returns: 返回创建的 创建文件夹路径
    @discardableResult
    static func createFolder(folderPath: String) -> (isSuccess: Bool, error: String) {
        if judgeFileOrFolderExists(filePath: folderPath) {
            return (true, "")
        }
        // 不存在的路径才会创建
        do {
            // withIntermediateDirectories为ture表示路径中间如果有不存在的文件夹都会创建
            try fileManager.createDirectory(atPath: folderPath, withIntermediateDirectories: true, attributes: nil)
            return (true, "")
        } catch _ {
            return (false, "创建失败")
        }
    }
    
    // MARK: 删除文件夹
    /// 删除文件夹
    /// - Parameter folderPath: 文件的路径
    @discardableResult
    static func removefolder(folderPath: String) -> (isSuccess: Bool, error: String) {
        let filePath = "\(folderPath)"
        guard judgeFileOrFolderExists(filePath: filePath) else {
            // 不存在就不做什么操作了
            return (true, "")
       }
        // 文件存在进行删除
        do {
            try fileManager.removeItem(atPath: filePath)
            return (true, "")
        } catch _ {
            return (false, "删除失败")
        }
    }
    
    // MARK: 创建文件
    /// 创建文件
    /// - Parameter filePath: 文件路径
    /// - Parameter contents: 文件内容
    /// - Returns: 返回创建的结果 和 路径
    @discardableResult
    static func createFile(filePath: String, contents: Data? = nil) -> (isSuccess: Bool, error: String) {
        guard judgeFileOrFolderExists(filePath: filePath) else {
            // 不存在的文件路径才会创建
            // 判断目标路径文件夹是否存在,不存在就进行创建
            let toFolderPath = directoryAtPath(path: filePath)
            if !judgeFileOrFolderExists(filePath: toFolderPath), !createFolder(folderPath: toFolderPath).isSuccess {
                return (false, "目标路径文件夹不存在且创建失败")
            }
            let createSuccess = fileManager.createFile(atPath: filePath, contents: contents, attributes: nil)
            return (createSuccess, "创建\(createSuccess ? "成功" : "失败")")
        }
        let remove = removefile(filePath: filePath)
        guard remove.isSuccess else {
            return remove
        }
        let createSuccess = fileManager.createFile(atPath: filePath, contents: contents, attributes: nil)
        return (createSuccess, "删除后创建\(createSuccess ? "成功" : "失败")")
    }
    
    // MARK: 删除文件
    /// 删除文件
    /// - Parameter filePath: 文件路径
    @discardableResult
    static func removefile(filePath: String) -> (isSuccess: Bool, error: String) {
        guard judgeFileOrFolderExists(filePath: filePath) else {
            // 不存在的文件路径就不需要要移除
            return (true, "文件不存在,无需移除")
        }
        // 移除文件
        do {
            try fileManager.removeItem(atPath: filePath)
            return (true, "移除文件成功")
        } catch _ {
            return (false, "移除文件失败")
        }
    }
    
    // MARK: 读取文件内容
    /// 读取文件内容
    /// - Parameter filePath: 文件路径
    /// - Returns: 文件内容
    @discardableResult
    static func readfile(filePath: String) -> String? {
        guard judgeFileOrFolderExists(filePath: filePath) else {
            // 不存在的文件路径就不需要要移除
            return nil
        }
        let data = fileManager.contents(atPath: filePath)
        return String(data: data!, encoding: String.Encoding.utf8)
    }
    
    // MARK: 把文字,图片,数组,字典写入文件
    /// 把文字,图片,数组,字典写入文件
    /// - Parameters:
    ///   - writeType: 写入类型
    ///   - content: 写入内容
    ///   - writePath: 写入路径
    /// - Returns: 写入的结果
    @discardableResult
    static func writeToFile(writeType: FileWriteType, content: Any, writePath: String) -> (isSuccess: Bool, error: String) {
        guard judgeFileOrFolderExists(filePath: writePath) else {
            // 不存在的文件路径
            return (false, "不存在的文件路径")
        }
        // 1、文字,2、图片,3、数组,4、字典写入文件
        switch writeType {
        case .TextType:
            let info = "\(content)"
            do {
                try info.write(toFile: writePath, atomically: true, encoding: String.Encoding.utf8)
                return (true, "")
            } catch _ {
                return (false, "写入失败")
            }
        case .ImageType:
            let data = content as! Data
            do {
                try data.write(to: URL(fileURLWithPath: writePath))
                return (true, "")
            } catch _ {
                return (false, "写入失败")
            }
        case .ArrayType:
            let array = content as! NSArray
            let result = array.write(toFile: writePath, atomically: true)
            if result {
                return (true, "")
            } else {
                return (false, "写入失败")
            }
        case .DictionaryType:
            let result = (content as! NSDictionary).write(toFile: writePath, atomically: true)
            if result {
                return (true, "")
            } else {
                return (false, "写入失败")
            }
        }
    }
    
    // MARK: 从文件 读取 文字,图片,数组,字典
    /// 从文件 读取 文字,图片,数组,字典
    /// - Parameters:
    ///   - readType: 读取的类型
    ///   - readPath: 读取文件路径
    /// - Returns: 返回读取的内容
    @discardableResult
    static func readFromFile(readType: FileWriteType, readPath: String) -> (isSuccess: Bool, content: Any?, error: String) {
        guard judgeFileOrFolderExists(filePath: readPath),  let readHandler =  FileHandle(forReadingAtPath: readPath) else {
            // 不存在的文件路径
            return (false, nil, "不存在的文件路径")
        }
        let data = readHandler.readDataToEndOfFile()
        // 1、文字,2、图片,3、数组,4、字典
        switch readType {
        case .TextType:
            let readString = String(data: data, encoding: String.Encoding.utf8)
            return (true, readString, "")
        case .ImageType:
            let image = UIImage(data: data)
            return (true, image, "")
        case .ArrayType:
            guard let readString = String(data: data, encoding: String.Encoding.utf8) else {
                return (false, nil, "读取内容失败")
            }
            return (true, JSON(readString).stringValue, "")
        case .DictionaryType:
            guard let readString = String(data: data, encoding: String.Encoding.utf8) else {
                return (false, nil, "读取内容失败")
            }
            return (true, JSON(readString).dictionaryObject, "")
        }
    }
    
    // MARK: 拷贝(文件夹/文件)的内容 到另外一个(文件夹/文件),新的(文件夹/文件)如果存在就先删除再 拷贝
    /**
     几个小注意点:
     1、目标路径,要带上文件夹名称,而不能只写父路径
     2、如果是覆盖拷贝,就是说目标路径已存在此文件夹,我们必须先删除,否则提示make directory error(当然这里最好做一个容错处理,比如拷贝前先转移到其他路径,如果失败,再拿回来)
     */
    /// 拷贝(文件夹/文件)的内容 到另外一个(文件夹/文件),新的(文件夹/文件)如果存在就先删除再 拷贝
    /// - Parameters:
    ///   - fromeFile: 拷贝的(文件夹/文件)路径
    ///   - toFile: 拷贝后的文件夹路径
    ///   - isOverwrite: 当要拷贝到的(文件夹/文件)路径存在,会拷贝失败,这里传入是否覆盖
    /// - Returns: 拷贝的结果
    @discardableResult
    static func copyFile(type: MoveOrCopyType, fromeFilePath: String, toFilePath: String, isOverwrite: Bool = true) -> (isSuccess: Bool, error: String) {
        // 1、先判断被拷贝路径是否存在
        guard judgeFileOrFolderExists(filePath: fromeFilePath) else {
            return (false, "被拷贝的(文件夹/文件)路径不存在")
        }
        // 2、判断目标路径文件夹是否存在,不存在就进行创建
        let toFileFolderPath = directoryAtPath(path: toFilePath)
        if !judgeFileOrFolderExists(filePath: toFileFolderPath), !createFolder(folderPath: toFileFolderPath).isSuccess {
            return (false, "拷贝后路径文件夹不存在且创建失败")
        }
        // 3、如果目标(文件夹/文件)已存在,先删除,否则拷贝不了
        if isOverwrite, judgeFileOrFolderExists(filePath: toFilePath) {
            do {
                try fileManager.removeItem(atPath: toFilePath)
            } catch _ {
                PrintLog(message: "删除目标(文件\\文件夹)失败")
                return (false, "拷贝失败")
            }
        }
        // 4、拷贝(文件夹/文件)
        do {
            try fileManager.copyItem(atPath: fromeFilePath, toPath: toFilePath)
        } catch _ {
            return (false, "拷贝失败")
        }
        return (true, "success")
    }
    
    // MARK: 移动(文件夹/文件)的内容 到另外一个(文件夹/文件),新的(文件夹/文件)如果存在就先删除再 移动
    /// 移动(文件夹/文件)的内容 到另外一个(文件夹/文件),新的(文件夹/文件)如果存在就先删除再 移动
    /// - Parameters:
    ///   - fromeFile: 被移动的文件路径
    ///   - toFile: 移动后的文件路径
    @discardableResult
    static func moveFile(type: MoveOrCopyType, fromeFilePath: String, toFilePath: String, isOverwrite: Bool = true) -> (isSuccess: Bool, error: String) {
        // 1、先判断被拷贝路径是否存在
        guard judgeFileOrFolderExists(filePath: fromeFilePath) else {
            return (false, "被移动的(文件夹/文件)路径不存在")
        }
        // 2、判断拷贝后的文件路径的前一个文件夹路径是否存在,不存在就进行创建
        let toFileFolderPath = directoryAtPath(path: toFilePath)
        if !judgeFileOrFolderExists(filePath: toFileFolderPath), type == .file ? !createFile(filePath: toFilePath).isSuccess : !createFolder(folderPath: toFileFolderPath).isSuccess {
            return (false, "移动后路径前一个文件夹不存在")
        }
        // 3、如果被移动的(文件夹/文件)已存在,先删除,否则拷贝不了
        if isOverwrite, judgeFileOrFolderExists(filePath: toFilePath) {
            do {
                try fileManager.removeItem(atPath: toFilePath)
            } catch _ {
                return (false, "移动失败")
            }
        }
        // 4、移动(文件夹/文件)
        do {
            try fileManager.moveItem(atPath: fromeFilePath, toPath: toFilePath)
        } catch _ {
            return (false, "移动失败")
        }
        return (true, "success")
    }
    
    // MARK: 判断 (文件夹/文件) 是否存在
    /** 判断文件或文件夹是否存在*/
    static func judgeFileOrFolderExists(filePath: String) -> Bool {
        let exist = fileManager.fileExists(atPath: filePath)
        // 查看文件夹是否存在,如果存在就直接读取,不存在就直接反空
        guard exist else {
            return false
        }
        return true
    }
    
    // MARK: 获取 (文件夹/文件) 的前一个路径
    /// 获取 (文件夹/文件) 的前一个路径
    /// - Parameter path: (文件夹/文件) 的路径
    /// - Returns: (文件夹/文件) 的前一个路径
    static func directoryAtPath(path: String) -> String {
        return (path as NSString).deletingLastPathComponent
    }
    
    // MARK: 判断目录是否可读
    static func judegeIsReadableFile(path: String) -> Bool {
        return fileManager.isReadableFile(atPath: path)
    }
    
    // MARK: 判断目录是否可写
    static func judegeIsWritableFile(path: String) -> Bool {
        return fileManager.isReadableFile(atPath: path)
    }
    
    // MARK: 根据文件路径获取文件扩展类型
    /// 根据文件路径获取文件扩展名
    /// - Parameter filePath: 文件路径
    /// - Returns: 文件扩展名(不带“.”)
    static func getFileExtension(filePath: String) -> String {
        return (filePath as NSString).pathExtension
    }
    
    // MARK: 根据文件路径获取文件名称,是否需要后缀
    /// 根据文件路径获取文件名称,是否需要后缀
    /// - Parameters:
    ///   - path: 文件路径
    ///   - suffix: 是否需要后缀,默认需要
    /// - Returns: 文件名称
    static func fileName(path: String, suffix: Bool = true) -> String {
        let fileName = (path as NSString).lastPathComponent
        guard suffix else {
            // 删除后缀
            return (fileName as NSString).deletingPathExtension
        }
        return fileName
    }
    // MARK: 对指定路径执行浅搜索,返回指定目录路径下的文件、子目录及符号链接的列表(只寻找一层)
    /// 对指定路径执行浅搜索,返回指定目录路径下的文件、子目录及符号链接的列表(只寻找一层)
    /// - Parameter folderPath: 建搜索的lujing
    /// - Returns: 指定目录路径下的文件、子目录及符号链接的列表
    static func shallowSearchAllFiles(folderPath: String) -> Array<String>? {
        do {
            let contentsOfDirectoryArray = try fileManager.contentsOfDirectory(atPath: folderPath)
            return contentsOfDirectoryArray
        } catch _ {
            return nil
        }
    }
    
    // MARK: 深度遍历,会递归遍历子文件夹(包括符号链接,所以要求性能的话用enumeratorAtPath)
    /**深度遍历,会递归遍历子文件夹(包括符号链接,所以要求性能的话用enumeratorAtPath)*/
    static func getAllFileNames(folderPath: String) -> Array<String>? {
        // 查看文件夹是否存在,如果存在就直接读取,不存在就直接反空
        if (judgeFileOrFolderExists(filePath: folderPath)) {
            guard let subPaths = fileManager.subpaths(atPath: folderPath) else {
                return nil
            }
            return subPaths
        } else {
            return nil
        }
    }
    
    // MARK: 深度遍历,会递归遍历子文件夹(但不会递归符号链接)
    /** 对指定路径深度遍历,会递归遍历子文件夹(但不会递归符号链接))*/
    static func deepSearchAllFiles(folderPath: String) -> Array<Any>? {
        // 查看文件夹是否存在,如果存在就直接读取,不存在就直接反空
        if (judgeFileOrFolderExists(filePath: folderPath)) {
            guard let contentsOfPathArray = fileManager.enumerator(atPath: folderPath) else {
                return nil
            }
            return contentsOfPathArray.allObjects
        }else{
            return nil
        }
    }
    
    // MARK: 计算单个 (文件夹/文件) 的大小,单位为字节(bytes) (没有进行转换的)
    /// 计算单个 (文件夹/文件) 的大小,单位为字节 (没有进行转换的)
    /// - Parameter filePath: (文件夹/文件) 路径
    /// - Returns: 单个文件或文件夹的大小
    static func fileOrDirectorySingleSize(filePath: String) -> UInt64 {
        // 1、先判断文件路径是否存在
        guard judgeFileOrFolderExists(filePath: filePath) else {
            return 0
        }
        // 2、读取文件大小
        do {
            let fileAttributes = try fileManager.attributesOfItem(atPath: filePath)
            guard let fileSizeValue = fileAttributes[FileAttributeKey.size] as? UInt64 else {
                return 0
            }
            return fileSizeValue
        } catch {
            return 0
        }
    }
    
    // MARK: - 获取文件或文件夹大小(转换为可读字符串)
    /// 获取指定路径的文件或文件夹大小(自动转换为 KB/MB/GB 等单位)
    /// - Parameter path: 文件或文件夹路径
    /// - Returns: 可读性强的字符串(如 "2.45 MB")
    static func fileOrDirectorySize(at path: String) -> String {
        // 校验路径是否有效
        guard !path.isEmpty, fileManager.fileExists(atPath: path) else {
            return "0 B"
        }
        
        let size = calculateItemSize(at: path)
        return convertUsingFormatter(size)
    }
    
    // MARK: 获取(文件夹/文件)属性集合
    ///  获取(文件夹/文件)属性集合
    /// - Parameter path: (文件夹/文件)路径
    /// - Returns: (文件夹/文件)属性集合
    @discardableResult
    static func fileAttributes(path: String) -> ([FileAttributeKey : Any]?) {
        do {
            let attributes = try fileManager.attributesOfItem(atPath: path)
            /*
            print("创建时间:\(attributes[FileAttributeKey.creationDate]!)")
            print("修改时间:\(attributes[FileAttributeKey.modificationDate]!)")
            print("文件大小:\(attributes[FileAttributeKey.size]!)")
            */
            return attributes
        } catch _ {
            return nil
        }
        /// key的列表如:
        /**
         public static let type:
         public static let size:
         public static let modificationDate:
         public static let referenceCount:
         public static let deviceIdentifier:
         public static let ownerAccountName:
         public static let groupOwnerAccountName:
         public static let posixPermissions:
         public static let systemNumber:
         public static let systemFileNumber:
         public static let extensionHidden:
         public static let hfsCreatorCode:
         public static let hfsTypeCode:
         public static let immutable:
         public static let appendOnly:
         public static let creationDate:
         public static let ownerAccountID:
         public static let groupOwnerAccountID:
         public static let busy:
         @available(iOS 4.0, *)
         public static let protectionKey:
         public static let systemSize:
         public static let systemFreeSize:
         public static let systemNodes:
         public static let systemFreeNodes:
         */
    }
}
// MARK:- fileprivate
extension FileManager {
    // MARK: - 递归计算文件/文件夹大小(以字节为单位)
    /// 递归计算给定路径下文件/文件夹的实际大小(单位:字节)
    /// - Parameter path: 文件或文件夹路径
    /// - Returns: 字节数(UInt64)
    private static func calculateItemSize(at path: String) -> UInt64 {
        var isDirectory: ObjCBool = false
        // 判断路径是否存在,是否是文件夹
        guard fileManager.fileExists(atPath: path, isDirectory: &isDirectory) else {
            return 0
        }
        
        // 是文件夹时,递归遍历所有内容
        if isDirectory.boolValue {
            var totalSize: UInt64 = 0
            let url = URL(fileURLWithPath: path)
            
            // 遍历目录下所有文件(包含子目录)
            if let enumerator = fileManager.enumerator(at: url,
                                                       includingPropertiesForKeys: [.fileSizeKey],
                                                       options: [],
                                                       errorHandler: nil) {
                for case let fileURL as URL in enumerator {
                    do {
                        // 获取文件大小属性
                        let resourceValues = try fileURL.resourceValues(forKeys: [.isRegularFileKey, .fileSizeKey])
                        if resourceValues.isRegularFile ?? false,
                           let fileSize = resourceValues.fileSize {
                            totalSize += UInt64(fileSize)
                        }
                    } catch {
                        print("无法读取 \(fileURL.path) 的大小: \(error)")
                    }
                }
            }
            return totalSize
        } else {
            // 是单个文件,直接获取大小
            do {
                let attributes = try fileManager.attributesOfItem(atPath: path)
                return (attributes[.size] as? UInt64) ?? 0
            } catch {
                print("获取文件大小失败: \(error)")
                return 0
            }
        }
    }
    
    // MARK: - 可选:使用系统内建 ByteCountFormatter 进行格式化
    /// 使用 Apple 提供的 ByteCountFormatter 进行格式化(更国际化)
    /// - Parameter size: 字节数
    /// - Returns: 格式化后的字符串(如 "2.1 MB")
    private static func convertUsingFormatter(_ size: UInt64) -> String {
        let formatter = ByteCountFormatter()
        formatter.allowedUnits = [.useKB, .useMB, .useGB, .useBytes] // 可自定义
        formatter.countStyle = .file
        return formatter.string(fromByteCount: Int64(size))
    }
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。