SwiftUI+Core Data+PhotoKit状态变更后视图未更新问题
你的UI不刷新不是单一问题,是4个逻辑断层导致变更事件没有传递到渲染层:
- 第一,
AssetFetcher检测到系统照片库变更后,只更新了自己的fetchResults,根本没有同步更新Core Data里的IndexAssetEntity索引,底层存储的索引数据还是旧值,就算视图触发刷新也拿不到最新状态 - 第二,
PhotoGridView里的createIndex只在onAppear和手动点击时触发,@Query返回的结果更新时没有调用重建索引的方法,ViewModel里存的分组字典永远是首次加载的旧数据 - 第三,
AssetCellViewModel里持有的indexAsset是初始化传入的值快照,不是可观察的引用对象,就算Core Data里的isFavorite/isTrashed字段修改了,Cell里拿的还是初始化时的旧值,不会更新状态 - 第四,
DataStore里用@ObservedObject监听AssetFetcher是无效的,ObservableObject的属性观察只有在SwiftUI视图层级里才会自动触发,普通类里持有的@ObservedObject不会自动接收变更通知
1. 补全照片库变更后的Core Data同步逻辑
修改AssetFetcher的photoLibraryDidChange方法,拿到变更后同步更新Core Data索引,不要只发无意义的通知:
// 先给AssetFetcher加弱引用指向DataStore,避免循环引用 weak var dataStore: DataStore? func photoLibraryDidChange(_ changeInstance: PHChange) { print("🔶 photoLibraryDidChange") guard let fetchResults = self.fetchResults, let changeDetails = changeInstance.changeDetails(for: fetchResults) else { return } DispatchQueue.main.async { self.fetchResults = changeDetails.fetchResultAfterChanges // 同步删除的资源 changeDetails.removedIndexes?.forEach { index in let deletedAsset = changeDetails.fetchResultBeforeChanges.object(at: index) self.dataStore?.deleteAsset(localIdentifier: deletedAsset.localIdentifier) } // 同步新增的资源 changeDetails.insertedIndexes?.forEach { index in let newAsset = changeDetails.fetchResultAfterChanges.object(at: index) try? self.dataStore?.registerAsset( localIdentifier: newAsset.localIdentifier, creationDate: newAsset.creationDate ?? Date(), isFavorite: newAsset.isFavorite ) } // 同步内容变更的资源(比如收藏状态修改) changeDetails.changedIndexes?.forEach { index in let updatedAsset = changeDetails.fetchResultAfterChanges.object(at: index) self.dataStore?.updateAsset( localIdentifier: updatedAsset.localIdentifier, isFavorite: updatedAsset.isFavorite ) } // 保存Core Data上下文 try? self.dataStore?.viewContext.save() self.objectWillChange.send() } }
记得在DataStore初始化AssetFetcher时把自身赋值给assetFetcher.dataStore = self,同时补全deleteAsset和updateAsset两个方法,对应修改Core Data里的实体字段。
2. 让网格视图在Query结果更新时自动重建索引
不要只在onAppear和点击时调用updateVm,用onChange监听indexAssets的变更:
// 在PhotoGridView的VStack上补全onChange修饰符 .onAppear { updateVm() } .onChange(of: indexAssets) { newAssets in vm.createIndex(newAssets) }
同时修改PhotoGridViewModel的createIndex方法,每次重建索引前先清空旧的分组字典,避免出现重复数据:
func createIndex(_ queryResults: QueryResults<IndexAsset>) { indexAssets = queryResults // 先清空旧数据 self.index.removeAll() guard queryResults.count > 0 else { return } // 初始值改为distantPast,避免首次判断逻辑错误 var lastDate = Date.distantPast for i in 0..<queryResults.count { let item = queryResults[i] guard let creationDate = item.creationDate, let dateKey = item.creationDateKey else { continue } if isSameDay(firstDate: lastDate, secondDate: creationDate) { self.index[dateKey]?.append(item) } else { self.index[dateKey] = [item] } lastDate = creationDate } // 去掉冗余的objectWillChange.send(),@Published属性修改会自动触发刷新 }
3. 修复Cell不更新的问题
AssetCellView里不要把indexAsset存在ViewModel里做快照,直接使用视图层传入的值,同时让ViewModel监听传入值的变更:
struct AssetCellView: View { @StateObject var vm : AssetCellViewModel let indexAsset : IndexAsset let geoSize : CGSize init(indexAsset: IndexAsset, geoSize: CGSize) { self.indexAsset = indexAsset self.geoSize = geoSize _vm = StateObject(wrappedValue: AssetCellViewModel(geoSize: geoSize)) } var body: some View { ZStack(alignment: .bottomTrailing) { if vm.image != nil { vm.image? .resizable() .aspectRatio(contentMode: .fill) .clipped() .border(.blue, width: indexAsset.isSelected ? 4 : 0) } if indexAsset.isFavorite { Image(systemName:"heart.fill") .resizable() .frame(width: 20, height: 20) .foregroundStyle(.ultraThickMaterial) .shadow(color: .black, radius: 12) .offset(x:-8, y:-8) } } .onAppear { vm.loadImage(for: indexAsset, targetSize: CGSize(width: geoSize.width/4, height: geoSize.width/4)) } } }
修改对应的AssetCellViewModel,去掉@Environment的错误用法——@Environment只能在SwiftUI视图里使用,普通ObservableObject类里拿不到环境值,直接通过方法参数传入需要的参数即可:
class AssetCellViewModel: ObservableObject{ @Published var image : Image? private let imageManager = PHCachingImageManager() init(geoSize:CGSize) { // 初始化不需要加载图片,等onAppear拿到正确的asset再加载 } func loadImage(for indexAsset: IndexAsset, targetSize: CGSize) { guard let localId = indexAsset.localIdentifier, let phAsset = PHAsset.fetchAssets(withLocalIdentifiers: [localId], options: nil).firstObject else { return } let options = PHImageRequestOptions() options.deliveryMode = .highQualityFormat options.isNetworkAccessAllowed = true imageManager.requestImage( for: phAsset, targetSize: targetSize, contentMode: .aspectFill, options: options ) { uiImage, _ in guard let uiImage = uiImage else { return } DispatchQueue.main.async { withAnimation(.easeInOut(duration: 0.15)) { self.image = Image(uiImage: uiImage) } } } } }
4. 修复DataStore和AssetFetcher的观察链
把DataStore里对assetFetcher的@ObservedObject改成普通属性,在AssetFetcher检测到变更、Core Data保存完成后,调用DataStore的objectWillChange.send(),确保注入到环境里的DataStore能把变更通知传给所有持有它的视图。
另外要确认你使用的@Query属性包装器,是通过DataStore的viewContext绑定NSFetchedResultsController做监听的,能正确响应NSManagedObjectContextDidSave通知,不然Core Data保存后Query不会返回最新结果。
- 你之前
createIndex方法里的日期判断逻辑有bug,初始值设为Date.distantFuture会导致第一次分组判断逻辑错误,已经在修复代码里改为Date.distantPast ForEach用\.self做id如果IndexAsset是NSManagedObject子类是可行的,但要确保Core Data生成的类正确实现了Hashable协议- 不要在非SwiftUI视图的普通ObservableObject类里使用
@Environment,它的生命周期和视图不绑定,拿不到有效值还会导致初始化异常
内容的提问来源于stack exchange,提问作者Jake Z

