Leaks using onHover modifier

TLDR: Using the onHover modifier seems to create leaks for any @ObservedObject you have defined in the parent SwiftUI view.

Explanation

I have been developing a mac app with SwiftUI (using Cocoa, not Catalyst). I started to notice that some objects weren't being deinitialized. After some research, I realized that view hierarchies using onHover never release their @ObservedObject properties.

It is not about creating retain cycles with those objects, you may not use them whatsoever that the objects are still leaking when the view is released.

Sample Code

You can reproduce the effect by un/commenting the .onHover modifier. Please notice that the state object is not being used anywhere.

struct RootView: View {
    @ObservedObject private var state = State()
    
    var body: some View {
        Rectangle()
            .fill(Color.red)
            .frame(width: 100, height: 100)
            .onHover { print("onHover: \($0)") }
            .padding()
    }
}

private final class State: ObservableObject {
    @Published var value: Int = 0
    
    deinit {
        print("State deinitialized")
    }
}

I have test this simple view on top of a NSWindow, but it doesn't really matter where in your hierarchy is being used.
Wind

Question

Can someone reproduce it? Is it intended behavior?

2 Likes

What is it about that sample code that you expect to eventually deinit the object? It looks all correct to me including the behavior. The object should only be destroyed when RootView is recreated from some superview.

When the view is destroyed, I would expect the ObservableObject to be destroyed as well. For example, if the window hosting the view is closed and the NSWindow memory is reclaimed I expect the Observable object to be deinitialized as well. That doesn't occur there. If you close the window, the object lingers in memory.

I have created a bug report if anyone is interested: FB7494527

Having the same problem, although in my case I'm not even using @ObservedObject. The issue I think is in HSHostingView. The following code illustrates the problem.

When the window is opened a second time, the first NSHostingView should deallocate, but it doesn't. I subclassed NSHostingView to be able to log calls on deinit.

If onHover is removed from the code, NSHostingView deallocates properly.

let secondaryWindow = NSWindow(contentRect: NSRect(x: 100, y: 100, width: 100, height: 100), styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], backing: .buffered, defer: false)

struct ContentView: View {
    var body: some View {
        Button("Open Window") {
            secondaryWindow.isReleasedWhenClosed = false
            secondaryWindow.contentView = CustomNSHostingView(rootView: MySubView())
            secondaryWindow.center()
            secondaryWindow.makeKeyAndOrderFront(nil)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

struct MySubView: View {
    var body: some View {
        VStack {
            Text("Hello World")
                .onHover { print("\($0)") }
        }.frame(width: 100, height: 100)
        
    }
}

class CustomNSHostingView<Content>: NSHostingView<Content> where Content : View {
    deinit {
        print("DEINIT")
    }
}

My bug report number: FB7597514

I encountered the bug, too. My situation is much like @dehesa and @swiftuilab.
Tried to seek a workaround by using only SwiftUI, to no avail.

So I made a NSView wrapper: GitHub - aerobounce/HoverAwareView: SwiftUI `.onHover` memory leak workaround. NSView wrapper that works just like the modifier.
Hope this helps.

1 Like

Thanks, this works perfectly!

I encountered this bug today as well after hours of debugging. I think this definitely has something todo with NSHostingView. I really hope this gets fixed soon.