在代码中尝试在SwiftData中存储PhotosPickerItem类型的值:
@Model
class SinglePhoto {
var String = ""
var photo: PhotosPickerItem?
init(String: String = "", photo: PhotosPickerItem) {
self.String = String
self.photo = photo
}
}
预览发现,Xcode报错:
/var/folders/1y/t7s8thm536j0hgvd3fhdp7pr0000gn/T/swift-generated-sources/@__swiftmacro_16hackingwithswift11SinglePhotoC5photo18_PersistedPropertyfMa_.swift:9:21 Instance method 'getValue(forKey:)' requires that 'PhotosPickerItem' conform to 'PersistentModel'
经排查发现,这是因为PhotosPickerItem 类型无法被 SwiftData 存储。SwiftData 要求模型属性必须是可序列化的(即可以持久化到存储中,例如 Core Data)。但是,PhotosPickerItem 是一个临时值类型,设计用于处理照片选择操作,并不是持久化数据的一部分。
解决方案
1、替换 PhotosPickerItem 为可持久化的数据类型
因为 PhotosPickerItem 本身不能被存储,可以改为存储照片的元数据,例如文件路径(String)、照片的二进制数据(Data),或者照片的标识符。
@Model
class SinglePhoto {
var name: String = "" // 更有意义的属性名
var photoData: Data? // 用于存储照片的二进制数据
init(name: String = "", photoData: Data? = nil) {
self.name = name
self.photoData = photoData
}
}
然后,在使用 PhotosPickerItem 时,加载照片数据并存储为 Data:
if let data = try? await photoPickerItem.loadTransferable(type: Data.self) {
let singlePhoto = SinglePhoto(name: "Example Photo", photoData: data)
photoGallery.photos.append(singlePhoto)
}
2、使用文件路径存储照片
如果照片文件较大,不建议直接存储 Data,可以将照片保存到磁盘,并存储文件路径:
@Model
class SinglePhoto {
var name: String = ""
var photoFilePath: String? // 存储照片文件路径
init(name: String = "", photoFilePath: String? = nil) {
self.name = name
self.photoFilePath = photoFilePath
}
}
在保存照片时,将其写入本地文件系统:
if let data = try? await photoPickerItem.loadTransferable(type: Data.self) {
let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent("example.jpg")
try? data.write(to: fileURL)
let singlePhoto = SinglePhoto(name: "Example Photo", photoFilePath: fileURL.path)
photoGallery.photos.append(singlePhoto)
}
在读取时,可以使用 photoFilePath 加载照片:
if let path = singlePhoto.photoFilePath, let imageData = try? Data(contentsOf: URL(fileURLWithPath: path)) {
let uiImage = UIImage(data: imageData)
// 使用 uiImage
}
需要注意的是,代码中使用的是temporaryDirectory临时目录,仅适用于临时测试,临时目录中的文件可能会在应用重新启动或系统回收资源时删除,不适合长期保存数据。
同时文件名称可以考虑UUID、时间戳、自定义计时器等形式(在结尾展开说明)。
3、考虑存储 iCloud 兼容的数据
如果需要同步到 iCloud,可以选择以下方式:
存储文件标识符(如 UUID)。
结合 CloudKit,存储照片文件到 iCloud 中,并在 SinglePhoto 中存储照片的标识符或 URL。
完整代码
import SwiftUI
import SwiftData
import Foundation
import PhotosUI
@Model
class PhotoGallery {
var photos: [SinglePhoto] = []
init(photos: [SinglePhoto] = []) {
self.photos = photos
}
}
@Model
class SinglePhoto {
var name: String
var photoData: Data? // 存储照片数据,或改为 photoFilePath: String? 来存储路径
init(name: String = "", photoData: Data? = nil) {
self.name = name
self.photoData = photoData
}
}
在使用时:
// 从 PhotosPickerItem 获取照片数据并存储
if let data = try? await photoPickerItem.loadTransferable(type: Data.self) {
let singlePhoto = SinglePhoto(name: "New Photo", photoData: data)
photoGallery.photos.append(singlePhoto)
}
总结
1、问题来源:PhotosPickerItem 是一个临时值类型,不能被 SwiftData 持久化。
2、解决方案:使用持久化的数据类型替代,例如 Data(照片数据)或 String(文件路径)。
3、更好的设计:对于大文件,推荐使用文件路径存储以节省内存;对于 iCloud,同步照片标识符或 URL。
扩展知识
区分多个文件名称
如果需要保存多个图片,文件名不能固定为 “example.jpg”,否则每次保存都会覆盖之前的文件。为了区分每个文件,可以使用以下方法生成唯一的文件名:
方法 1:使用 UUID
UUID 是一种常用的方式,用于生成唯一的字符串。你可以用它作为文件名,确保不会重复。
let uniqueFileName = UUID().uuidString + ".jpg"
let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent(uniqueFileName)
try? data.write(to: fileURL)
优点:保证文件名唯一。
缺点:文件名较长且不易读。
方法 2:基于时间戳
可以使用当前的时间戳(精确到秒或毫秒)作为文件名。
let timestamp = Date().timeIntervalSince1970
let uniqueFileName = "\(timestamp).jpg"
let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent(uniqueFileName)
try? data.write(to: fileURL)
优点:文件名可读且有一定规律。
缺点:如果多张图片同时保存(同一秒内),可能会有冲突。
方法 3:使用自定义计数器
可以维护一个计数器(如保存在内存或持久化存储中),每次保存时递增计数器以生成新文件名。
var imageCounter = UserDefaults.standard.integer(forKey: "imageCounter") // 从 UserDefaults 获取计数
imageCounter += 1
UserDefaults.standard.set(imageCounter, forKey: "imageCounter") // 更新计数器
let uniqueFileName = "image_\(imageCounter).jpg"
let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent(uniqueFileName)
try? data.write(to: fileURL)
优点:文件名简单且有序。
缺点:需要维护计数器状态。