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查看:
- 连接真机,打开Xcode -> Window -> Devices and Simulators
- 选中你的App,点击
Download Container - 右键下载的容器文件,选择
Show Package Contents,进入AppData/Documents即可查看文件
内容的提问来源于stack exchange,提问作者Victor
相关产品推荐
相关产品推荐

