环境:
- 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 对象")
}
}
//
// 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 对象")
}
}
// // 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
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
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。