Swiftui,核心数据和摄影:当状态更改时,视图不会更新(状态管理地狱)

发布于 2025-02-13 09:50:48 字数 6882 浏览 1 评论 0原文

我正在通过构建照片组织者应用程序来学习Swift/Swiftui。它在网格中显示了用户的照片库,例如内置的照片应用程序,并且有一个详细的视图,您可以在其中做喜欢的照片或将其添加到垃圾箱中。

我的应用程序加载了所有数据并显示良好,但是当情况发生变化时,UI不会更新。我已经进行了足够的调试,以确认我的编辑应用于基础phassets和核心数据资产。 感觉问题是我的观点不是重新订阅。

我使用 dave deLong的方法创建一个将核心数据与Swiftui分开的抽象层。我有一个称为datastore的单例环境对象,可以处理与核心数据和phphotolibrary的所有交互。当应用程序运行时,创建了datastore。它制作AssetFetcher,从照片库中获取所有资产(并实现phphotolibraryChangeObserver)。 datastore在资产上迭代以在核心数据中创建索引。我的视图'viewModels查询索引项目的查询核心数据,并使用dave帖子中的@query属性包装器显示它们。

app.swift

@main
struct LbPhotos2App: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.dataStore, DataStore.shared)
        }
    }
}

photogridview.swift(这是contentview呈现的内容)

struct PhotoGridView: View {
    @Environment(\.dataStore) private var dataStore : DataStore
    @Query(.all) var indexAssets: QueryResults<IndexAsset>
    @StateObject var vm = PhotoGridViewModel() 
    
    func updateVm() {
        vm.createIndex(indexAssets)
    }
    
    var body: some View {
        GeometryReader { geo in
            VStack {
                HStack {
                    Text("\(indexAssets.count) assets")
                    Spacer()
                    TrashView()
                }.padding(EdgeInsets(top: 4, leading: 16, bottom: 4, trailing: 16))
                ScrollView {
                    ForEach(vm.sortedKeys, id: \.self) { key in
                        let indexAssets = vm.index[key]
                        let date = indexAssets?.first?.creationDate
                        GridSectionView(titleDate:date, indexAssets:indexAssets!, geoSize: geo.size)
                    }
                }.onTapGesture {
                    updateVm()
                }
            }.onAppear {
                updateVm()
            }
            .navigationDestination(for: IndexAsset.self) { indexAsset in
                AssetDetailView(indexAsset: indexAsset)
            }
        }
    }
    
}

photogridViewModel.swift

class PhotoGridViewModel: ObservableObject {
    @Published var index: [String:[IndexAsset]] = [:]
    var indexAssets: QueryResults<IndexAsset>?
        
    func createIndex() {
        guard let assets = self.indexAssets else {return}
        self.createIndex(assets)
    }
    
    func createIndex(_ queryResults: QueryResults<IndexAsset>) {
        indexAssets = queryResults
        if queryResults.count > 0 {
            var lastDate = Date.distantFuture
            
            for i in 0..<queryResults.count {
                let item = queryResults[i]
                let isSameDay = isSameDay(firstDate: lastDate, secondDate: item.creationDate!)
                if isSameDay {
                    self.index[item.creationDateKey!]?.append(item)
                } else {
                    self.index[item.creationDateKey!] = [item]
                }
                lastDate = item.creationDate!
            }
        }
        self.objectWillChange.send()
        
    }
    
    var sortedKeys: [String] {
        return index.keys.sorted().reversed()
    }
    
    private func isSameDay(firstDate:Date, secondDate:Date) -> Bool {
        return Calendar.current.isDate(
            firstDate,
            equalTo: secondDate,
            toGranularity: .day
        )
    }

 }

我实际上在GridSectionView.swift

LazyVGrid(columns: gridLayout, spacing: 2) {
                let size = geoSize.width/4

                ForEach(indexAssets, id:\.self) { indexAsset in
                    NavigationLink(
                        value: indexAsset,
                        label: {
                            AssetCellView(indexAsset: indexAsset, geoSize:geoSize)
                        }
                    ).frame(width: size, height: size)
                        .buttonStyle(.borderless)
                }
            }

AssetCellview.swift

struct AssetCellView: View {
    @StateObject var vm : AssetCellViewModel
    var indexAsset : IndexAsset
    var geoSize : CGSize
    
    init(indexAsset: IndexAsset, geoSize: CGSize) {
        self.indexAsset = indexAsset
        self.geoSize = geoSize
        _vm = StateObject(wrappedValue: AssetCellViewModel(indexAsset: indexAsset, geoSize: geoSize))
    }
    
    
    var body: some View {
        ZStack(alignment: .bottomTrailing) {
            if (vm.indexAsset != nil && vm.image != nil) {
                vm.image?
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .border(.blue, width: vm.indexAsset!.isSelected ? 4 : 0)
            }
            if (vm.indexAsset != nil && vm.indexAsset!.isFavorite) {
                Image(systemName:"heart.fill")
                    .resizable()
                    .frame(width: 20, height: 20)
                    .foregroundStyle(.ultraThickMaterial)
                    .shadow(color: .black, radius: 12)
                    .offset(x:-8, y:-8)
            }
        }
        
    }
}

AssetCellViewModel.swift
class AssetCellViewModel: ObservableObject{
    @Environment(\.dataStore) private var dataStore
    @Published var image : Image?
    var indexAsset : IndexAsset?
    var geoSize : CGSize
    
    init(indexAsset: IndexAsset? = nil, geoSize:CGSize) {
        self.indexAsset = indexAsset
        self.geoSize = geoSize
        self.requestImage(targetSize: CGSize(width: geoSize.width/4, height: geoSize.width/4))
    }
    
    func setIndexAsset(_ indexAsset:IndexAsset, targetSize: CGSize) {
        self.indexAsset = indexAsset
        self.requestImage(targetSize: targetSize)
    }
    
    func requestImage(targetSize: CGSize? = nil) {
        if (self.indexAsset != nil) {
            dataStore.fetchImageForLocalIdentifier(
                id: indexAsset!.localIdentifier!,
                targetSize: targetSize,
                completionHandler: { image in
                    withAnimation(Animation.easeInOut (duration:0.15)) {
                        self.image = image
                    }
                }
            )
        }
    }
}

dataTastore.swift

public class DataStore : ObservableObject {
    static let shared = DataStore()
    
    let persistenceController = PersistenceController.shared
    @ObservedObject var assetFetcher = AssetFetcher() 
    
    let dateFormatter = DateFormatter()
    var imageManager = PHCachingImageManager()
    let id = UUID().uuidString

    
    init() {
        print("
              

I'm learning Swift/SwiftUI by building a photo organizer app. It displays a user's photo library in a grid like the built-in photos app, and there's a detail view where you can do things like favorite a photo or add it to the trash.

My app loads all the data and displays it fine, but the UI doesn't update when things change. I've debugged enough to confirm that my edits are applied to the underlying PHAssets and Core Data assets. It feels like the problem is that my views aren't re-rendering.

I used Dave DeLong's approach to create an abstraction layer that separates Core Data from SwiftUI. I have a singleton environment object called DataStore that handles all interaction with Core Data and the PHPhotoLibrary. When the app runs, the DataStore is created. It makes an AssetFetcher that grabs all assets from the photo library (and implements PHPhotoLibraryChangeObserver). DataStore iterates over the assets to create an index in Core Data. My views' viewmodels query core data for the index items and display them using the @Query property wrapper from Dave's post.

App.swift

@main
struct LbPhotos2App: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.dataStore, DataStore.shared)
        }
    }
}

PhotoGridView.swift (this is what contentview presents)

struct PhotoGridView: View {
    @Environment(\.dataStore) private var dataStore : DataStore
    @Query(.all) var indexAssets: QueryResults<IndexAsset>
    @StateObject var vm = PhotoGridViewModel() 
    
    func updateVm() {
        vm.createIndex(indexAssets)
    }
    
    var body: some View {
        GeometryReader { geo in
            VStack {
                HStack {
                    Text("\(indexAssets.count) assets")
                    Spacer()
                    TrashView()
                }.padding(EdgeInsets(top: 4, leading: 16, bottom: 4, trailing: 16))
                ScrollView {
                    ForEach(vm.sortedKeys, id: \.self) { key in
                        let indexAssets = vm.index[key]
                        let date = indexAssets?.first?.creationDate
                        GridSectionView(titleDate:date, indexAssets:indexAssets!, geoSize: geo.size)
                    }
                }.onTapGesture {
                    updateVm()
                }
            }.onAppear {
                updateVm()
            }
            .navigationDestination(for: IndexAsset.self) { indexAsset in
                AssetDetailView(indexAsset: indexAsset)
            }
        }
    }
    
}

PhotoGridViewModel.swift

class PhotoGridViewModel: ObservableObject {
    @Published var index: [String:[IndexAsset]] = [:]
    var indexAssets: QueryResults<IndexAsset>?
        
    func createIndex() {
        guard let assets = self.indexAssets else {return}
        self.createIndex(assets)
    }
    
    func createIndex(_ queryResults: QueryResults<IndexAsset>) {
        indexAssets = queryResults
        if queryResults.count > 0 {
            var lastDate = Date.distantFuture
            
            for i in 0..<queryResults.count {
                let item = queryResults[i]
                let isSameDay = isSameDay(firstDate: lastDate, secondDate: item.creationDate!)
                if isSameDay {
                    self.index[item.creationDateKey!]?.append(item)
                } else {
                    self.index[item.creationDateKey!] = [item]
                }
                lastDate = item.creationDate!
            }
        }
        self.objectWillChange.send()
        
    }
    
    var sortedKeys: [String] {
        return index.keys.sorted().reversed()
    }
    
    private func isSameDay(firstDate:Date, secondDate:Date) -> Bool {
        return Calendar.current.isDate(
            firstDate,
            equalTo: secondDate,
            toGranularity: .day
        )
    }

 }

Here's where I actually display the asset in GridSectionView.swift

LazyVGrid(columns: gridLayout, spacing: 2) {
                let size = geoSize.width/4

                ForEach(indexAssets, id:\.self) { indexAsset in
                    NavigationLink(
                        value: indexAsset,
                        label: {
                            AssetCellView(indexAsset: indexAsset, geoSize:geoSize)
                        }
                    ).frame(width: size, height: size)
                        .buttonStyle(.borderless)
                }
            }

AssetCellView.swift

struct AssetCellView: View {
    @StateObject var vm : AssetCellViewModel
    var indexAsset : IndexAsset
    var geoSize : CGSize
    
    init(indexAsset: IndexAsset, geoSize: CGSize) {
        self.indexAsset = indexAsset
        self.geoSize = geoSize
        _vm = StateObject(wrappedValue: AssetCellViewModel(indexAsset: indexAsset, geoSize: geoSize))
    }
    
    
    var body: some View {
        ZStack(alignment: .bottomTrailing) {
            if (vm.indexAsset != nil && vm.image != nil) {
                vm.image?
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .border(.blue, width: vm.indexAsset!.isSelected ? 4 : 0)
            }
            if (vm.indexAsset != nil && vm.indexAsset!.isFavorite) {
                Image(systemName:"heart.fill")
                    .resizable()
                    .frame(width: 20, height: 20)
                    .foregroundStyle(.ultraThickMaterial)
                    .shadow(color: .black, radius: 12)
                    .offset(x:-8, y:-8)
            }
        }
        
    }
}

AssetCellViewModel.swift
class AssetCellViewModel: ObservableObject{
    @Environment(\.dataStore) private var dataStore
    @Published var image : Image?
    var indexAsset : IndexAsset?
    var geoSize : CGSize
    
    init(indexAsset: IndexAsset? = nil, geoSize:CGSize) {
        self.indexAsset = indexAsset
        self.geoSize = geoSize
        self.requestImage(targetSize: CGSize(width: geoSize.width/4, height: geoSize.width/4))
    }
    
    func setIndexAsset(_ indexAsset:IndexAsset, targetSize: CGSize) {
        self.indexAsset = indexAsset
        self.requestImage(targetSize: targetSize)
    }
    
    func requestImage(targetSize: CGSize? = nil) {
        if (self.indexAsset != nil) {
            dataStore.fetchImageForLocalIdentifier(
                id: indexAsset!.localIdentifier!,
                targetSize: targetSize,
                completionHandler: { image in
                    withAnimation(Animation.easeInOut (duration:0.15)) {
                        self.image = image
                    }
                }
            )
        }
    }
}

some of DataStore.swift

public class DataStore : ObservableObject {
    static let shared = DataStore()
    
    let persistenceController = PersistenceController.shared
    @ObservedObject var assetFetcher = AssetFetcher() 
    
    let dateFormatter = DateFormatter()
    var imageManager = PHCachingImageManager()
    let id = UUID().uuidString

    
    init() {
        print("???? init dataStore: \(self.id)")        
        dateFormatter.dateFormat = "yyyy-MM-dd"
        assetFetcher.iterateResults{ asset in
            do {
                try self.registerAsset(
                    localIdentifier: asset.localIdentifier,
                    creationDate: asset.creationDate!,
                    isFavorite: asset.isFavorite
                )
            } catch {
                print("Error registering asset \(asset)")
            }
        }
    }

    func registerAsset(localIdentifier:String, creationDate:Date, isFavorite:Bool) throws {
        let alreadyExists = indexAssetEntityWithLocalIdentifier(localIdentifier)
        if alreadyExists != nil {
//            print("???? Asset already registered: \(localIdentifier)")
//            print(alreadyExists![0])
            return
        }
        
        let iae = IndexAssetEntity(context: self.viewContext)
        iae.localIdentifier = localIdentifier
        iae.creationDate = creationDate
        iae.creationDateKey = dateFormatter.string(from: creationDate)
        iae.isFavorite = isFavorite
        iae.isSelected = false
        iae.isTrashed = false
        
        self.viewContext.insert(iae)
        try self.viewContext.save()
        print("???? Registered asset: \(localIdentifier)")
    }

And AssetFetcher.swift

class AssetFetcher:NSObject, PHPhotoLibraryChangeObserver, ObservableObject {
    @Published var fetchResults : PHFetchResult<PHAsset>? = nil 
    let id = UUID().uuidString

    override init() {
        super.init()
        print("???? init assetfetcher: \(id)")
        self.startFetchingAllPhotos()
    }
        
    deinit {
        PHPhotoLibrary.shared().unregisterChangeObserver(self)
    }
       
    func startFetchingAllPhotos() {
        getPermissionIfNecessary(completionHandler: {result in
            print(result)
        })
        let fetchOptions = PHFetchOptions()
        var datecomponents = DateComponents()
        datecomponents.month = -3
        
        //TODO: request assets dynamically
        let threeMonthsAgo = Calendar.current.date(byAdding: datecomponents, to:Date())
        
        fetchOptions.predicate = NSPredicate(format: "creationDate > %@ AND creationDate < %@", threeMonthsAgo! as NSDate, Date() as NSDate)
        fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
        fetchOptions.wantsIncrementalChangeDetails = true
        //        fetchOptions.fetchLimit = 1000
        let results = PHAsset.fetchAssets(with: .image, options: fetchOptions)
        PHPhotoLibrary.shared().register(self)
        print("???? \(PHPhotoLibrary.shared())")
        self.fetchResults = results
    }
    
    func iterateResults(_ callback:(_ asset: PHAsset) -> Void) {
        print("iterateResults")
        guard let unwrapped = self.fetchResults else {
            return
        }
        for i in 0..<unwrapped.count {
            callback(unwrapped.object(at: i))
        }
    }

    
    func photoLibraryDidChange(_ changeInstance: PHChange) {
        print("???? photoLibraryDidChange")
        DispatchQueue.main.async {
            if let changeResults = changeInstance.changeDetails(for: self.fetchResults!) {
                self.fetchResults = changeResults.fetchResultAfterChanges
//                self.dataStore.photoLibraryDidChange(changeInstance)
//                self.updateImages()
                self.objectWillChange.send()
            }
        }
    }
    
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文