Swiftui,核心数据和摄影:当状态更改时,视图不会更新(状态管理地狱)
我正在通过构建照片组织者应用程序来学习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 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论