如何在SwiftUI生命周期中显示NSPopover?

8

我想通过点击按钮显示可分离的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)
    }
}

这个回答解决了你的问题吗?https://dev59.com/hr3pa4cB1Zd3GeqPZzZo#63862691 - Asperi
很遗憾,SwiftUI的弹出窗口(不是NSPopover)无法分离,而且还没有办法覆盖关闭请求。 因此,我认为NSPopover是唯一的选择。 - Roman Banks
2个回答

11

这是一种可能的简单方法的演示 - 将对本地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
        }
    }
}

3

在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) {
        context.coordinator.visibilityDidChange(isVisible, in: nsView)
        context.coordinator.contentDidChange(content: content)
    }
    
    func makeCoordinator() -> Coordinator {
        .init(isVisible: $isVisible)
    }
    
    @MainActor
    final class Coordinator: NSObject, NSPopoverDelegate {
        private let popover: NSPopover = .init()
        private let isVisible: Binding<Bool>
        
        init(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()
                }
            }
        }
        
        fileprivate func contentDidChange<T: View>(@ViewBuilder content: () -> T) {
            popover.contentViewController = NSHostingController(rootView: content())
        }
        
        func popoverDidClose(_ notification: Notification) {
            isVisible.wrappedValue = false
        }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接