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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 16:15:12