本篇目标

理解 iOS 沙盒目录结构,掌握文件路径获取、文本与 Data 读写、JSON 文件的编码与解码,能独立完成文件的保存、读取、删除与目录管理。

一、iOS 沙盒机制

iOS 应用运行在沙盒中,每个应用只能访问自己的目录,无法访问其他应用的数据。

沙盒的好处:

  • 数据隔离,安全性高

  • 卸载应用时自动清理数据

  • 不同应用之间互不干扰

沙盒目录结构:

App Sandbox/
├── Documents/          // 用户数据,iTunes 备份
├── Library/
│   ├── Caches/         // 缓存,可能被系统清理
│   ├── Preferences/    // UserDefaults 存储
│   └── Application Support/  // 应用支持文件
├── tmp/                // 临时文件,随时可能被清理
└── SystemData/         // 系统数据

各目录用途:

目录

用途

是否备份

Documents

用户生成的数据

是

Library/Caches

可重新生成的缓存

否

Library/Preferences

UserDefaults

是

Library/Application Support

应用支持文件

是

tmp

临时文件

否

二、获取目录路径

获取 Documents 目录:

let documentsURL = FileManager.default.urls(
    for: .documentDirectory,
    in: .userDomainMask
).first!

print(documentsURL.path)

获取 Caches 目录:

let cachesURL = FileManager.default.urls(
    for: .cachesDirectory,
    in: .userDomainMask
).first!

获取 tmp 目录:

let tmpURL = FileManager.default.temporaryDirectory

获取 Application Support 目录:

let supportURL = FileManager.default.urls(
    for: .applicationSupportDirectory,
    in: .userDomainMask
).first!

说明:

  • 返回的是 URL 数组,通常取第一个。

  • userDomainMask 表示用户目录。

  • Application Support 目录可能不存在,需要手动创建。

三、文件路径拼接

使用 URL 拼接:

let documentsURL = FileManager.default.urls(
    for: .documentDirectory,
    in: .userDomainMask
).first!

let fileURL = documentsURL.appendingPathComponent("notes.txt")
print(fileURL.path)

推荐用 URL:

  • 更安全,自动处理路径分隔符。

  • 支持中文、空格等特殊字符。

  • 配合 FileManager API 使用更方便。

避免手动拼接字符串:

// 不推荐
let path = documentsURL.path + "/notes.txt"

// 推荐
let fileURL = documentsURL.appendingPathComponent("notes.txt")

四、写入文本文件

写入 String:

let content = "Hello, iOS!"
let fileURL = documentsURL.appendingPathComponent("greeting.txt")

do {
    try content.write(to: fileURL, atomically: true, encoding: .utf8)
    print("写入成功")
} catch {
    print("写入失败:\(error)")
}

参数说明:

  • to:目标文件 URL。

  • atomically:是否原子写入,建议 true。

  • encoding:编码格式,通常用 .utf8。

原子写入的好处:

  • 先写临时文件,再替换目标文件。

  • 避免写入中途失败导致文件损坏。

五、读取文本文件

读取 String:

let fileURL = documentsURL.appendingPathComponent("greeting.txt")

do {
    let content = try String(contentsOf: fileURL, encoding: .utf8)
    print(content)
} catch {
    print("读取失败:\(error)")
}

读取失败常见原因:

  • 文件不存在

  • 编码不匹配

  • 权限问题

  • 路径错误

读取时判断文件是否存在:

if FileManager.default.fileExists(atPath: fileURL.path) {
    let content = try? String(contentsOf: fileURL, encoding: .utf8)
    print(content ?? "")
} else {
    print("文件不存在")
}

六、写入 Data

写入二进制数据:

let data = "Hello".data(using: .utf8)!
let fileURL = documentsURL.appendingPathComponent("data.bin")

do {
    try data.write(to: fileURL)
    print("写入成功")
} catch {
    print("写入失败:\(error)")
}

读取 Data:

do {
    let data = try Data(contentsOf: fileURL)
    if let content = String(data: data, encoding: .utf8) {
        print(content)
    }
} catch {
    print("读取失败:\(error)")
}

适用场景:

  • 图片、音频等二进制文件

  • 加密后的数据

  • 序列化后的对象

七、JSON 文件读写

JSON 是 iOS 中常用的数据交换格式。

定义 Codable 模型:

struct Note: Codable {
    var id: UUID
    var title: String
    var content: String
    var createdAt: Date
}

写入 JSON 文件:

let note = Note(
    id: UUID(),
    title: "第一条笔记",
    content: "这是笔记内容",
    createdAt: Date()
)

let fileURL = documentsURL.appendingPathComponent("notes.json")

do {
    let encoder = JSONEncoder()
    encoder.outputFormatting = .prettyPrinted
    encoder.dateEncodingStrategy = .iso8601

    let data = try encoder.encode([note])
    try data.write(to: fileURL)
    print("写入成功")
} catch {
    print("写入失败:\(error)")
}

读取 JSON 文件:

do {
    let data = try Data(contentsOf: fileURL)
    let decoder = JSONDecoder()
    decoder.dateDecodingStrategy = .iso8601

    let notes = try decoder.decode([Note].self, from: data)
    print(notes)
} catch {
    print("读取失败:\(error)")
}

日期编码策略:

策略

说明

.deferredToDate

默认,时间戳

.iso8601

ISO 8601 字符串

.secondsSince1970

秒级时间戳

.millisecondsSince1970

毫秒级时间戳

八、文件管理操作

判断文件是否存在:

FileManager.default.fileExists(atPath: fileURL.path)

删除文件:

do {
    try FileManager.default.removeItem(at: fileURL)
    print("删除成功")
} catch {
    print("删除失败:\(error)")
}

移动文件:

let newURL = documentsURL.appendingPathComponent("new.txt")
try FileManager.default.moveItem(at: oldURL, to: newURL)

复制文件:

try FileManager.default.copyItem(at: sourceURL, to: destinationURL)

创建目录:

let folderURL = documentsURL.appendingPathComponent("Notes")

do {
    try FileManager.default.createDirectory(
        at: folderURL,
        withIntermediateDirectories: true
    )
} catch {
    print("创建失败:\(error)")
}

说明:

  • withIntermediateDirectories: true 会自动创建中间目录。

  • 目录已存在时不会报错。

列出目录内容:

do {
    let contents = try FileManager.default.contentsOfDirectory(
        at: documentsURL,
        includingPropertiesForKeys: nil
    )
    for url in contents {
        print(url.lastPathComponent)
    }
} catch {
    print("读取失败:\(error)")
}

获取文件属性:

let attributes = try FileManager.default.attributesOfItem(atPath: fileURL.path)
let size = attributes[.size] as? Int64 ?? 0
print("文件大小:\(size) 字节")

九、封装文件管理类

class FileStorage {
    static let shared = FileStorage()
    private let fileManager = FileManager.default

    private init() {}

    private var documentsURL: URL {
        fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
    }

    func fileURL(for name: String) -> URL {
        documentsURL.appendingPathComponent(name)
    }

    func save<T: Encodable>(_ object: T, to name: String) throws {
        let url = fileURL(for: name)
        let encoder = JSONEncoder()
        encoder.dateEncodingStrategy = .iso8601
        let data = try encoder.encode(object)
        try data.write(to: url, options: .atomic)
    }

    func load<T: Decodable>(_ type: T.Type, from name: String) throws -> T {
        let url = fileURL(for: name)
        let data = try Data(contentsOf: url)
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601
        return try decoder.decode(T.self, from: data)
    }

    func delete(_ name: String) throws {
        let url = fileURL(for: name)
        try fileManager.removeItem(at: url)
    }

    func exists(_ name: String) -> Bool {
        let url = fileURL(for: name)
        return fileManager.fileExists(atPath: url.path)
    }
}

使用:

let notes = [Note(id: UUID(), title: "标题", content: "内容", createdAt: Date())]

try FileStorage.shared.save(notes, to: "notes.json")

let loaded = try FileStorage.shared.load([Note].self, from: "notes.json")

十、文件保护与安全

文件保护等级:

try (data as NSData).write(to: fileURL, options: .completeFileProtection)

常用保护等级:

等级

说明

.completeFileProtection

锁屏后完全不可访问

.completeUnlessOpen

锁屏后已打开的可继续访问

.completeUntilFirstUserAuthentication

首次解锁后可访问

.none

无保护

适用场景:

  • 用户敏感数据使用 .completeFileProtection

  • 后台任务需要访问的使用 .completeUntilFirstUserAuthentication

不建议直接存敏感数据:

  • 密码、Token 使用 Keychain。

  • 文件加密后再写入。

十一、使用注意事项

注意事项:

  • tmp 目录可能被系统清理。

  • Caches 目录也可能被清理。

  • 重要数据放 Documents。

  • 大文件不要放 UserDefaults。

十二、避坑指南

  • 不要手动拼路径:用 URL 拼接。

  • 写入要捕获异常:文件操作可能失败。

  • 原子写入:避免文件损坏。

  • tmp 目录不持久:重要数据不要放 tmp。

  • Caches 会被清理:可重新生成的缓存才放这里。

  • JSON 日期格式:编码和解码策略要一致。

  • 文件操作不要放主线程:大文件异步处理。

  • 敏感数据加密:不要明文存储。

十三、检查清单

  • [ ] 理解 iOS 沙盒目录结构

  • [ ] 能获取 Documents、Caches、tmp 目录

  • [ ] 会使用 URL 拼接文件路径

  • [ ] 能读写文本文件

  • [ ] 能读写 Data

  • [ ] 能读写 JSON 文件

  • [ ] 会判断、删除、移动、复制文件

  • [ ] 会创建目录并列出内容

  • [ ] 能封装文件管理类

  • [ ] 了解文件保护等级