环境:
- 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。