我试图通过单击按钮来显示可拆卸的 NSPopover,但我被卡住了。我遵循了如何显示 NSPopover 但它们都围绕菜单栏应用程序。
我的 AppDelegate 看起来像这样
final class AppDelegate: NSObject, NSApplicationDelegate {
var popover: NSPopover!
func applicationDidFinishLaunching(_ notification: Notification) {
let popover = NSPopover()
let popoverView = PopoverView()
popover.contentSize = NSSize(width: 300, height: 200)
popover.contentViewController = NSHostingController(rootView: popoverView)
popover.behavior = .transient
self.popover = popover
}
func togglePopover(_ sender: AnyObject?) {
self.popover.show(relativeTo: (sender?.bounds)!, of: sender as! NSView, preferredEdge: NSRectEdge.minY)
}
}
这是一个可能方法的简单演示 - 将原生
NSPopover
的控制包装到可表示的背景视图中。
注意:下一步将背景包装到视图修饰符中或/并使其更具可配置性取决于您。
使用 Xcode 13 / macOS 11.5.1 进行准备和测试
struct ContentView: View {
@State private var isVisible = false
var body: some View {
Button("Test") {
isVisible.toggle()
}
.background(NSPopoverHolderView(isVisible: $isVisible) {
Text("I'm in NSPopover")
.padding()
})
}
}
struct NSPopoverHolderView<T: View>: NSViewRepresentable {
@Binding var isVisible: Bool
var content: () -> T
func makeNSView(context: Context) -> NSView {
NSView()
}
func updateNSView(_ nsView: NSView, context: Context) {
context.coordinator.setVisible(isVisible, in: nsView)
}
func makeCoordinator() -> Coordinator {
Coordinator(state: _isVisible, content: content)
}
class Coordinator: NSObject, NSPopoverDelegate {
private let popover: NSPopover
private let state: Binding<Bool>
init<V: View>(state: Binding<Bool>, content: @escaping () -> V) {
self.popover = NSPopover()
self.state = state
super.init()
popover.delegate = self
popover.contentViewController = NSHostingController(rootView: content())
popover.behavior = .transient
}
func setVisible(_ isVisible: Bool, in view: NSView) {
if isVisible {
popover.show(relativeTo: view.bounds, of: view, preferredEdge: .minY)
} else {
popover.close()
}
}
func popoverDidClose(_ notification: Notification) {
self.state.wrappedValue = false
}
func popoverShouldDetach(_ popover: NSPopover) -> Bool {
true
}
}
}
更新了 Asperi 的答案,添加了对内容更改的支持
struct PopoverView<T: View>: NSViewRepresentable {
@Binding private var isVisible: Bool
private let content: () -> T
init(isVisible: Binding<Bool>, @ViewBuilder content: @escaping () -> T) {
self._isVisible = isVisible
self.content = content
}
func makeNSView(context: Context) -> NSView {
.init()
}
func updateNSView(_ nsView: NSView, context: Context) {
let coordinator = context.coordinator
(coordinator.popover.contentViewController as? NSHostingController<T>)?.rootView = content()
coordinator.visibilityDidChange(isVisible, in: nsView)
}
func makeCoordinator() -> Coordinator {
let coordinator = Coordinator(popover: .init(), isVisible: $isVisible)
coordinator.popover.contentViewController = NSHostingController(rootView: content())
return coordinator
}
@MainActor
final class Coordinator: NSObject, NSPopoverDelegate {
fileprivate let popover: NSPopover = .init()
private let isVisible: Binding<Bool>
fileprivate init(popover: NSPopover, isVisible: Binding<Bool>) {
self.isVisible = isVisible
super.init()
popover.delegate = self
popover.behavior = .transient
}
fileprivate func visibilityDidChange(_ isVisible: Bool, in view: NSView) {
if isVisible {
if !popover.isShown {
popover.show(relativeTo: view.bounds, of: view, preferredEdge: .maxX)
}
} else {
if popover.isShown {
popover.close()
}
}
}
func popoverDidClose(_ notification: Notification) {
isVisible.wrappedValue = false
}
}
}