How do I track down the dependency that causes a refresh?

Viewed 64

I have a fairly complex document type to work with. It is basically a bundle containing a set of independent documents of the same type, with various pieces of metadata about the documents. The data structure that represents the bundle is an array of structs, similar to this (there are several more fields, but these are representative):

struct DocumentData: Equatable, Identifiable, Hashable {
    let id = UUID()
    var docData: DocumentDataClass
    var docName: String
    var docFileWrapper: FileWrapper?
    
    func hash(into hasher: inout Hasher) {
        id.hash(into: &hasher)
    }
    
    static func ==(lhs: KeyboardLayoutData, rhs: KeyboardLayoutData) -> Bool {
        return lhs.id == rhs.id
    }
}

The window for the bundle is a master-detail, with a list on the left and, when one is selected, there is an edit pane for the document on the right. The FileWrapper is used to keep track of which files need to be written for saving, so it gets initialised on reading the relevant file, and reset when an undoable change is made. That is largely the only way that the DocumentData structure gets changed (ignoring explicit things like changing the name).

I've reached a point where a lot of things are working, but I'm stuck on one. There's a view inside the edit pane, several levels deep, and when I double-click it, I want a sheet to appear. It does so, but then disappears by itself.

Searching for ways to work this out, I discovered by using print(Self._printChanges()) at various points that the edit pane was being refreshed after showing the sheet, which meant that the parent disappeared. What I found was that the dependency that changed was the DocumentData instance. But, I then added a print of the DocumentData instance before the _printChanges call, and it is identical. I have also put in didSet for each field of DocumentData to print when they get set, and nothing gets printed, so I'm not sure where the change is happening.

So the question comes down to how I can work out what is actually driving the refresh, since what is claimed to be different is identical in every field.

There are some other weird things happening, such as dragging and dropping text into the view causing the whole top-level document array of DocumentData items to change before the drop gets processed and the data structures get updated, so there are things I am not understanding as clearly as I might like. Any guidance is much appreciated.

ADDED: The view that triggers the sheet is fairly straightforward, especially compared to its enclosing view, which is where most of the interface code is. This is a slightly simplified version of it:

struct MyView: View, DropDelegate {
    @EnvironmentObject var keyboardStatus: KeyboardStatus
    @Environment(\.displayFont) var displayFont
    @Environment(\.undoManager) var undoManager

    var keyCode: Int
    @State var modifiers: NSEvent.ModifierFlags = []

    @State private var dragHighlight = false
    @State private var activeSheet: ActiveSheet?
    @State private var editPopoverIsPresented = false
    // State variables for double click and drop handling
    ...

    static let dropTypes = [UTType.utf8PlainText]
    
    var body: some View {
        ZStack {
            BackgroundView(...)
            Text(...)
        }
        .onAppear {
            modifiers = keyboardStatus.currentModifiers
        }
        .focusable(false)
        .allowsHitTesting(true)
        .contentShape(geometry.contentPath)
        .onHover { entered in
            // updates an inspector view
        }
        .onTapGesture(count: 2) {
            interactionType = .doubleClick
            activeSheet = .doubleClick
        }
        .onTapGesture(count: 1) {
            handleItemClick()
        }
        .sheet(item: $activeSheet, onDismiss: handleSheetReturn) { item in
            switch item {
            case .doubleClick:
                DoubleClickItem(...) ) {
                    activeSheet = nil
                }

            case .drop:
                DropItem(...) {
                    activeSheet = nil
                }
            }
        }
        .popover(isPresented: $editPopoverIsPresented) {
            EditPopup(...)
        }
        .onDrop(of: KeyCap.dropTypes, delegate: self)
        .contextMenu {
            ItemContextMenu(...)
        }
    }
    
    func handleItemClick() {
        NotificationCenter.default.post(name: .itemClick, object: nil, userInfo: [...])
    }
    
    func handleEvent(event: KeyEvent) {
        if event.eventKind == .dropText {
            interactionType = .drop
            activeSheet = .drop
        }
        else if event.eventKind == .replaceText {
            ...
            handleItemDoubleClick()
        }
    }
    
    func handleSheetReturn() {
        switch interactionType {
        case .doubleClick:
            handleItemDoubleClick()
            
        case .drop:
            handleItemDrop()
            
        case .none:
            break
        }
    }
    
    func handleItemDoubleClick() {
        switch itemAction {
        case .state1:
            ...
            
        case .state2:
            ...
            
        case .none:
            // User cancelled
            break
        }
        interactionType = nil
    }
    
    func handleItemDrop() {
        switch itemDropAction {
        case .action1:
            ...

        case .action2:
            ...

        case .none:
            // User cancelled
            break
        }
        interactionType = nil
    }

    // Drop delegate
    func dropEntered(info: DropInfo) {
        dragHighlight = true
    }
    
    func dropExited(info: DropInfo) {
        dragHighlight = false
    }
    
    func performDrop(info: DropInfo) -> Bool {
        if let item = info.itemProviders(for: MyView.dropTypes).first {
            item.loadItem(forTypeIdentifier: UTType.utf8PlainText.identifier, options: nil) { (textData, error) in
                if let textData = String(data: textData as! Data, encoding: .utf8) {
                    let event = ...
                    handleEvent(event: event)
                }
            }
            return true
        }
        return false
    }
}

Further edit: I ended up rewiring the code so that the sheet belongs to the higher level view, which makes everything work without solving the question. I still don't understand why I get a notification that a dependency has changed when it is identical to what it was before, and none of the struct's didSet blocks are called.

1 Answers

Try removing the class from the DocumentData. The use of objects in SwiftUI can cause these kind of bugs since it’s all designed for value types.

Try using ReferenceFileDocument to work with your model object instead of FileDocument which is designed for a model of value types.

Try using sheet(item:onDismiss:content:) for editing. I've seen people have the problem you describe when they try to hack the boolean sheet to work with editing an item.

Related