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

Swift开发iOS App:真机无法保存JSON文件,模拟器正常

iOS 真机无法写入JSON文件的解决方案

问题根源

你遇到的NSCocoaErrorDomain Code=513错误,本质是真机上App的主Bundle目录是只读的——iOS系统为了安全性,禁止修改App安装包内的文件,而模拟器因为是开发环境,系统限制更宽松,所以允许写入Bundle目录,这就是真机和模拟器表现不一致的原因。

你的第一个save函数错误地将数据写入了Bundle.main中的文件路径,而非可读写的Documents目录,这在真机上必然触发权限错误。

现有代码的问题点

你尝试的第二个版本代码逻辑方向是对的,但存在几个隐蔽问题:

  • load函数中如果Documents目录没有目标文件(比如复制失败),直接触发fatalError,导致App崩溃且无法看到具体错误
  • 未处理FileManager获取Documents目录为空的极端情况
  • 复制文件时的日志仅用print,不容易排查问题

修正后的完整代码

import Foundation

@Observable
class ModelData {
    var landmarks: [Landmark] = []
    
    init() {
        // 先确保文件从Bundle复制到Documents
        let success = copyInitialFileToDocumentsIfNeeded(filename: "landmarkData.json")
        // 根据复制结果加载数据,失败则用默认值
        if success {
            landmarks = load(fromDocuments: "landmarkData.json") ?? []
        } else {
            // 如果复制失败,尝试从Bundle加载初始数据(仅读),同时创建空的Documents文件
            landmarks = load(fromBundle: "landmarkData.json") ?? []
            _ = save(landmarks)
        }
    }
}

/// 从Documents目录加载可解码数据
func load<T: Decodable>(fromDocuments filename: String) -> T? {
    guard let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
        print("Failed to get Documents directory URL")
        return nil
    }
    let fileURL = documentsURL.appendingPathComponent(filename)
    
    do {
        let data = try Data(contentsOf: fileURL)
        let decoder = JSONDecoder()
        return try decoder.decode(T.self, from: data)
    } catch {
        print("Failed to load or parse \(filename) from Documents: \(error.localizedDescription)")
        return nil
    }
}

/// 从Bundle目录加载初始可解码数据(只读)
func load<T: Decodable>(fromBundle filename: String) -> T? {
    guard let fileURL = Bundle.main.url(forResource: filename, withExtension: nil) else {
        print("Failed to find \(filename) in main bundle")
        return nil
    }
    
    do {
        let data = try Data(contentsOf: fileURL)
        let decoder = JSONDecoder()
        return try decoder.decode(T.self, from: data)
    } catch {
        print("Failed to load or parse \(filename) from bundle: \(error.localizedDescription)")
        return nil
    }
}

/// 将数据保存到Documents目录
func save(_ landmarks: [Landmark]) -> Bool {
    let encoder = JSONEncoder()
    encoder.outputFormatting = .prettyPrinted // 可选,格式化JSON便于查看
    
    do {
        let data = try encoder.encode(landmarks)
        guard let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
            print("Failed to get Documents directory URL")
            return false
        }
        let fileURL = documentsURL.appendingPathComponent("landmarkData.json")
        
        try data.write(to: fileURL, options: .atomic) // 原子写入,避免文件损坏
        print("Successfully saved data to: \(fileURL.path)")
        return true
    } catch {
        print("Failed to encode or save landmarks: \(error.localizedDescription)")
        return false
    }
}

/// 将Bundle中的初始文件复制到Documents目录(仅当目标不存在时)
func copyInitialFileToDocumentsIfNeeded(filename: String) -> Bool {
    let fileManager = FileManager.default
    guard let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else {
        print("Failed to get Documents directory URL")
        return false
    }
    let destinationURL = documentsURL.appendingPathComponent(filename)
    
    // 如果目标文件已存在,直接返回成功
    if fileManager.fileExists(atPath: destinationURL.path) {
        print("\(filename) already exists in Documents")
        return true
    }
    
    guard let sourceURL = Bundle.main.url(forResource: filename, withExtension: nil) else {
        print("\(filename) not found in main bundle")
        return false
    }
    
    do {
        try fileManager.copyItem(at: sourceURL, to: destinationURL)
        print("Successfully copied \(filename) to Documents")
        return true
    } catch {
        print("Failed to copy \(filename) to Documents: \(error.localizedDescription)")
        return false
    }
}

关键优化点

  • 区分Bundle和Documents的读写权限:Bundle仅用于加载初始只读数据,所有修改操作都指向Documents目录
  • 容错性处理:去掉fatalError,改用可选值返回和错误日志,避免App崩溃
  • 原子写入:使用.atomic选项写入文件,防止写入过程中App崩溃导致文件损坏
  • 双加载路径:如果Documents目录无文件,先尝试从Bundle加载初始数据,再自动保存到Documents,确保后续读写正常
  • 明确的日志输出:所有关键步骤都有打印日志,便于真机调试时查看问题

真机调试技巧

如果你想验证Documents目录的文件状态,可以通过Xcode的Devices and Simulators查看:

  1. 连接真机,打开Xcode -> Window -> Devices and Simulators
  2. 选中你的App,点击Download Container
  3. 右键下载的容器文件,选择Show Package Contents,进入AppData/Documents即可查看文件

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 15:23:12