In this example a SwiftUI view's identity depends on the value of its @StateObject member variable. I don't think this should be possible because if a view doesn’t have an explicit identity, it has a structural identity based on its type and position in the view hierarchy. In the example below the ItemPopupView's identity depends on the value of its itemsWrapper member. Please help me to understand this and hopefully to change the view that its itemsWrapper member doesn't impact the view's identity.
Please note that in this Minimal Reproducible Example the itemsWrapper StateObject isn't used, but the point of the MRE is to understand and address this identity issue.
To reproduce:
- Add a core data model named
TestAppwith an entity namedItemthat has 1Dateattribute namedupdateTime. - Run the app
- Tap the row in the list
- Tap the Edit button in the sheet that opens
- Tap the Save button in the next sheet that opens
- Observe that
ItemPopupView: @self changed.was printed in the console - Comment out the
_itemsWrapper =line inItemPopupView'sinit() - Rerun the app, tap the row, tap Edit, tap Save
- Observe that
ItemPopupView: @self changed.wasn't printed in the console
Apologies this code is so long, I couldn't find a way to minimize it further:
import SwiftUI
import CoreData
@main
struct TestAppApp: App {
@StateObject private var manager: DataManager = DataManager()
var body: some Scene {
WindowGroup {
ItemListContentView()
.environmentObject(manager)
.environment(\.managedObjectContext, manager.container.viewContext)
.onAppear() {
let item = Item(context: manager.container.viewContext)
item.updateTime = Date()
try? manager.container.viewContext.save()
}
}
}
}
struct ChildContextAndObject<Object: NSManagedObject>: Identifiable {
let id = UUID()
let childContext: NSManagedObjectContext
var childObject: Object?
init(withExistingObject object: Object?, in parentContext: NSManagedObjectContext
) {
self.childContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
childContext.parent = parentContext
self.childObject = childContext.object(with: object!.objectID) as! Object
}
}
class DataManager: NSObject, ObservableObject {
private var containerImpl: NSPersistentContainer? = nil
var container: NSPersistentContainer {
if containerImpl == nil {
containerImpl = NSPersistentContainer(name: "TestApp")
containerImpl!.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
containerImpl!.viewContext.automaticallyMergesChangesFromParent = true
}
return containerImpl!
}
}
struct ItemPopupItem: Identifiable {
let id = UUID()
var item: Item?
}
struct ItemListContentView: View {
@FetchRequest(sortDescriptors: []) private var items: FetchedResults<Item>
@State var popupItem : ItemPopupItem?
var body: some View {
List {
ForEach(items) { item in
Text("\(item.updateTime!)")
.onTapGesture {
popupItem = ItemPopupItem(item: item)
}
}
}
.overlay(
EmptyView()
.sheet(item: $popupItem) { popupItem in
ItemPopupView(popupItem: popupItem)
}
)
}
}
class ItemsWrapper : ObservableObject {
public var items : [Item] = []
init() {
}
init(items: [Item]) {
self.items = items
}
}
struct ItemPopupView: View {
@Environment(\.managedObjectContext) private var viewContext
var popupItem : ItemPopupItem
@StateObject var itemsWrapper = ItemsWrapper()
@State public var updateOperation: ChildContextAndObject<Item>?
init(popupItem : ItemPopupItem) {
self.popupItem = popupItem
_itemsWrapper = StateObject(wrappedValue: ItemsWrapper(items: [popupItem.item!])) // effects identity
}
var body: some View {
let _ = Self._printChanges()
VStack {
Button("Edit") {
updateOperation = ChildContextAndObject(withExistingObject: popupItem.item!, in: viewContext)
}
Text("\(popupItem.item!.updateTime!)")
}
.overlay(
EmptyView()
.sheet(item: $updateOperation) { updateOperation in
EditItemView(item: updateOperation.childObject!)
.environment(\.managedObjectContext, updateOperation.childContext)
}
)
}
}
struct EditItemView: View {
@Environment(\.managedObjectContext) private var viewContext
@Environment(\.dismiss) var dismiss
@ObservedObject var item : Item
var body: some View {
Button("Save") {
print("save")
item.updateTime = Date()
try? viewContext.save()
try? viewContext.parent!.save()
dismiss()
}
}
}