SwiftUI Bug – onHover() 导致内存泄漏(View 结构体实例不被释放)

环境:

  • macOS 10.15.5
  • Xcode 11.7

现象:

如果对一个 View 视图使用了 onHover() 修饰器,那么会导致该视图,乃至包裹该视图的最外层视图都不会被释放。

这应该是 SwiftUI 的一个 Bug,但是什么时候才能被修复啊 =。=# SwiftUI 也太多 Bug 了。。。文档又差。 欸

示例代码:

//
//  OnHoverLeak.swift
//  Countdown
//
//  Created by funway on 2020/10/2.
//  Copyright © 2020 funwaywang. All rights reserved.
//

import SwiftUI

struct OnHoverLeak: View {
    
    // 可以通过该对象来验证结构体实例有没有被释放
    private let deallocPrinter = DeallocPrinter()
    
    var body: some View {
        VStack {
            Text("onHover() modifier result in memory leak").font(.headline)
            
            Divider()
            
            Text("onHover() 会导致内存泄漏,使得 View 结构体实例无法被释放")
                .onHover(perform: { hovered in
                    log.debug(hovered)
                })
        }.frame(width: 500, height: 300)
    }
}


struct OnHoverLeak_Previews: PreviewProvider {
    static var previews: some View {
        OnHoverLeak()
    }
}


class DeallocPrinter {
    init() {
        log.verbose("构造 DeallocPrinter 对象")
    }
    
    deinit {
        log.verbose("析构 DeallocPrinter 对象")
    }
}

解决方法:

参考:https://github.com/aerobounce/HoverAwareView

import SwiftUI

#if os(macOS)

public extension View {
    func onHoverAware(_ perform: @escaping (Bool) -> Void) -> some View {
        background(HoverAwareView { (value: Bool) in
            perform(value)
        })
    }
}

public struct HoverAwareView: View {
    public let onHover: (Bool) -> Void

    public var body: some View {
        Representable(onHover: onHover)
    }
}

private extension HoverAwareView {
    final class Representable: NSViewRepresentable {
        let onHover: (Bool) -> Void

        init(onHover: @escaping (Bool) -> Void) {
            self.onHover = onHover
        }

        func makeNSView(context: Context) -> NSHoverAwareView {
            NSHoverAwareView(onHover: onHover)
        }

        func updateNSView(_ nsView: NSHoverAwareView, context: Context) {}
    }
}

private extension HoverAwareView {
    final class NSHoverAwareView: NSView {
        let onHover: (Bool) -> Void

        init(onHover: @escaping (Bool) -> Void) {
            self.onHover = onHover
            super.init(frame: .zero)
        }

        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }

        override func updateTrackingAreas() {
            for area in trackingAreas {
                removeTrackingArea(area)
            }

            guard bounds.size.width > 0, bounds.size.height > 0 else { return }

            let options: NSTrackingArea.Options = [
                .mouseEnteredAndExited,
                .activeInActiveApp,
                .assumeInside,
            ]

            let trackingArea: NSTrackingArea = NSTrackingArea(rect: bounds, options: options, owner: self, userInfo: nil)
            addTrackingArea(trackingArea)
        }

        override func mouseEntered(with event: NSEvent) {
            onHover(true)
        }

        override func mouseExited(with event: NSEvent) {
            onHover(false)
        }
    }
}

#endif

窗口 inactive 状态也能接收 hover 事件

上面的 HoverAwareView 中定义的 NSTrackingArea.Options 使用了 .activeInActiveApp。即是说只有在窗口为 active 状态时候,才会接收鼠标事件。有时候我们希望即使窗口为 inactive 状态也能接收 hover 事件,(比如说桌面便利贴程序的便利贴窗口)那么就得将 .activeInActiveApp 改为 .activeAlways。

 

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top